Repository URL to install this package:
|
Version:
7.4.3 ▾
|
# (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 for the wxPython user interface toolkit.
"""
from operator import itemgetter
import wx
from pyface.dock.api import (
DockWindow,
DockSizer,
DockSection,
DockRegion,
DockControl,
)
from pyface.image_resource import ImageResource
from pyface.timer.api import do_later
from pyface.ui.wx.grid.api import Grid
from traits.api import (
Int,
List,
Instance,
Str,
Any,
Button,
Tuple,
HasPrivateTraits,
Bool,
Event,
Property,
)
from traitsui.api import (
View,
Item,
UI,
InstanceEditor,
EnumEditor,
Handler,
SetEditor,
ListUndoItem,
)
from traitsui.editors.table_editor import BaseTableEditor, customize_filter
from traitsui.menu import Action, ToolBar
from traitsui.table_column import TableColumn, ObjectColumn
from traitsui.table_filter import TableFilter
from traitsui.ui_traits import SequenceTypes
from .constants import (
TableCellBackgroundColor,
TableCellColor,
TableLabelBackgroundColor,
TableLabelColor,
TableReadOnlyBackgroundColor,
TableSelectionBackgroundColor,
TableSelectionTextColor,
)
from .editor import Editor
from .table_model import TableModel, TraitGridSelection
from .helper import TraitsUIPanel
#: Mapping from TableEditor selection modes to Grid selection modes:
GridModes = {
"row": "rows",
"rows": "rows",
"column": "cols",
"columns": "cols",
"cell": "cell",
"cells": "cell",
}
def _get_color(color, default_color):
"""Return the color if it is not None, otherwise use default."""
if color is not None:
return color
return default_color
class TableEditor(Editor, BaseTableEditor):
"""Editor that presents data in a table. Optionally, tables can have
a set of filters that reduce the set of data displayed, according to
their criteria.
"""
# -------------------------------------------------------------------------
# Trait definitions:
# -------------------------------------------------------------------------
#: The set of columns currently defined on the editor:
columns = List(Instance(TableColumn))
#: Index of currently edited (i.e., selected) table item(s):
selected_row_index = Int(-1)
selected_row_indices = List(Int)
selected_indices = Property()
selected_column_index = Int(-1)
selected_column_indices = List(Int)
selected_cell_index = Tuple(Int, Int)
selected_cell_indices = List(Tuple(Int, Int))
#: The currently selected table item(s):
selected_row = Any()
selected_rows = List()
selected_items = Property()
selected_column = Any()
selected_columns = List()
selected_cell = Tuple(Any, Str)
selected_cells = List(Tuple(Any, Str))
selected_values = Property()
#: The indices of the table items currently passing the table filter:
filtered_indices = List(Int)
#: The event fired when a cell is clicked on:
click = Event()
#: The event fired when a cell is double-clicked on:
dclick = Event()
#: Is the editor in row mode (i.e. not column or cell mode)?
in_row_mode = Property()
#: Is the editor in column mode (i.e. not row or cell mode)?
in_column_mode = Property()
#: Current filter object (should be a TableFilter or callable or None):
filter = Any()
#: The grid widget associated with the editor:
grid = Instance(Grid)
#: The table model associated with the editor:
model = Instance(TableModel)
#: TableEditorToolbar associated with the editor:
toolbar = Any()
#: The Traits UI associated with the table editor toolbar:
toolbar_ui = Instance(UI)
#: Is the table editor scrollable? This value overrides the default.
scrollable = True
#: Is 'auto_add' mode in effect? (I.e., new rows are automatically added to
#: the end of the table when the user modifies current last row.)
auto_add = Bool(False)
def init(self, parent):
"""Finishes initializing the editor by creating the underlying toolkit
widget.
"""
factory = self.factory
self.filter = factory.filter
self.auto_add = factory.auto_add and (factory.row_factory is not None)
columns = factory.columns[:]
if (len(columns) == 0) and (len(self.value) > 0):
columns = [
ObjectColumn(name=name)
for name in self.value[0].editable_traits()
]
self.columns = columns
self.model = model = TableModel(editor=self, reverse=factory.reverse)
model.on_trait_change(self._model_sorted, "sorted", dispatch="ui")
mode = factory.selection_mode
row_mode = mode in ("row", "rows")
selected = None
items = model.get_filtered_items()
if factory.editable and (len(items) > 0):
selected = items[0]
if (factory.edit_view == " ") or (not row_mode):
self.control = panel = TraitsUIPanel(parent, -1)
sizer = wx.BoxSizer(wx.VERTICAL)
self._create_toolbar(panel, sizer)
# Create the table (i.e. grid) control:
hsizer = wx.BoxSizer(wx.HORIZONTAL)
self._create_grid(panel, hsizer)
sizer.Add(hsizer, 1, wx.EXPAND)
else:
item = self.item
name = item.get_label(self.ui)
theme = factory.dock_theme or item.container.dock_theme
self.control = dw = DockWindow(parent, theme=theme).control
panel = TraitsUIPanel(dw, -1, size=(300, 300))
sizer = wx.BoxSizer(wx.VERTICAL)
dc = DockControl(
name=name + " Table", id="table", control=panel, style="fixed"
)
contents = [DockRegion(contents=[dc])]
self._create_toolbar(panel, sizer)
selected = None
items = model.get_filtered_items()
if factory.editable and (len(items) > 0):
selected = items[0]
# Create the table (i.e. grid) control:
hsizer = wx.BoxSizer(wx.HORIZONTAL)
self._create_grid(panel, hsizer)
sizer.Add(hsizer, 1, wx.EXPAND)
# Assign the initial object here, so a valid editor will be built
# when the 'edit_traits' call is made:
self.selected_row = selected
self._ui = ui = self.edit_traits(
parent=dw,
kind="subpanel",
view=View(
[
Item(
"selected_row",
style="custom",
editor=InstanceEditor(
view=factory.edit_view, kind="subpanel"
),
resizable=True,
width=factory.edit_view_width,
height=factory.edit_view_height,
),
"|<>",
],
resizable=True,
handler=factory.edit_view_handler,
),
)
# Set the parent UI of the new UI to our own UI:
ui.parent = self.ui
# Reset the object so that the sub-sub-view will pick up the
# correct history also:
self.selected_row = None
self.selected_row = selected
dc.style = item.dock
contents.append(
DockRegion(
contents=[
DockControl(
name=name + " Editor",
id="editor",
control=ui.control,
style=item.dock,
)
]
)
)
# Finish setting up the DockWindow:
dw.SetSizer(
DockSizer(
contents=DockSection(
contents=contents,
is_row=(factory.orientation == "horizontal"),
)
)
)
# Set up the required externally synchronized traits (if any):
sv = self.sync_value
is_list = mode[-1] == "s"
sv(factory.click, "click", "to")
sv(factory.dclick, "dclick", "to")
sv(factory.filter_name, "filter", "from")
sv(factory.columns_name, "columns", is_list=True)
sv(factory.filtered_indices, "filtered_indices", "to")
sv(factory.selected, "selected_%s" % mode, is_list=is_list)
if is_list:
sv(
factory.selected_indices,
"selected_%s_indices" % mode[:-1],
is_list=True,
)
else:
sv(factory.selected_indices, "selected_%s_index" % mode)
# Listen for the selection changing on the grid:
self.grid.on_trait_change(
getattr(self, "_selection_%s_updated" % mode),
"selection_changed",
dispatch="ui",
)
# Set the min height of the grid panel to 0, this will provide
# a scrollbar if the window is resized such that only the first row
# is visible
panel.SetMinSize((-1, 0))
# Finish the panel layout setup:
panel.SetSizer(sizer)
def _create_grid(self, parent, sizer):
"""Creates the associated grid control used to implement the table."""
factory = self.factory
selection_mode = GridModes[factory.selection_mode]
if factory.selection_bg_color is None:
selection_mode = ""
cell_color = _get_color(factory.cell_color, TableCellColor)
cell_bg_color = _get_color(
factory.cell_bg_color, TableCellBackgroundColor
)
cell_read_only_bg_color = _get_color(
factory.cell_read_only_bg_color, TableReadOnlyBackgroundColor
)
label_bg_color = _get_color(
factory.label_bg_color, TableLabelBackgroundColor
)
label_color = _get_color(factory.label_color, TableLabelColor)
selection_text_color = _get_color(
factory.selection_color, TableSelectionTextColor
)
selection_bg_color = _get_color(
factory.selection_bg_color, TableSelectionBackgroundColor
)
self.grid = grid = Grid(
parent,
model=self.model,
enable_lines=factory.show_lines,
grid_line_color=factory.line_color,
show_row_headers=factory.show_row_labels,
show_column_headers=factory.show_column_labels,
default_cell_font=factory.cell_font,
default_cell_text_color=cell_color,
default_cell_bg_color=cell_bg_color,
default_cell_read_only_color=cell_read_only_bg_color,
default_label_font=factory.label_font,
default_label_text_color=label_color,
default_label_bg_color=label_bg_color,
selection_bg_color=selection_bg_color,
selection_text_color=selection_text_color,
autosize=factory.auto_size,
read_only=not factory.editable,
edit_on_first_click=factory.edit_on_first_click,
selection_mode=selection_mode,
allow_column_sort=factory.sortable,
allow_row_sort=False,
column_label_height=factory.column_label_height,
row_label_width=factory.row_label_width,
)
_grid = grid._grid
_grid.SetScrollLineY(factory.scroll_dy)
# Set the default size for each table row:
height = factory.row_height
if height <= 0:
height = _grid.GetTextExtent("My")[1] + 9
_grid.SetDefaultRowSize(height)
# Allow the table to be resizable if the user did not explicitly
# specify a number of rows to display:
self.scrollable = factory.rows == 0
# Calculate a reasonable default size for the table:
if len(self.model.get_filtered_items()) > 0:
height = _grid.GetRowSize(0)
max_rows = factory.rows or 15
min_width = max(150, 80 * len(self.columns))
if factory.show_column_labels:
min_height = _grid.GetColLabelSize() + (max_rows * height)
else:
min_height = max_rows * height
_grid.SetMinSize(wx.Size(min_width, min_height))
# On Linux, there is what appears to be a bug in wx in which the
# vertical scrollbar will not be sized properly if the TableEditor is
# sized to be shorter than the minimum height specified above. Since
# this height is only set to ensure that the TableEditor is sized
# correctly during the initial UI layout, we un-set it after this takes
# place (addresses ticket 1810)
def clear_minimum_height(info):
min_size = _grid.GetMinSize()
min_size.height = 0
_grid.SetMinSize(min_size)
self.ui.add_defined(clear_minimum_height)
sizer.Add(grid.control, 1, wx.EXPAND)
return grid.control
def _create_toolbar(self, parent, sizer):
"""Creates the table editing toolbar."""
factory = self.factory
if not factory.show_toolbar:
return
toolbar = TableEditorToolbar(parent=parent, editor=self)
if (toolbar.control is not None) or (len(factory.filters) > 0):
tb_sizer = wx.BoxSizer(wx.HORIZONTAL)
if len(factory.filters) > 0:
view = View(
[
Item(
"filter<250>{View}", editor=factory._filter_editor
),
"_",
Item(
"filter_summary<100>{Results}~",
object="model",
resizable=False,
),
"_",
"-",
],
resizable=True,
)
self.toolbar_ui = ui = view.ui(
context={"object": self, "model": self.model},
parent=parent,
kind="subpanel",
).trait_set(parent=self.ui)
tb_sizer.Add(ui.control, 0)
if toolbar.control is not None:
self.toolbar = toolbar
# add padding so the toolbar is right aligned
tb_sizer.Add((1, 1), 1, wx.EXPAND)
tb_sizer.Add(toolbar.control, 0)
sizer.Add(tb_sizer, 0, wx.EXPAND)
def dispose(self):
"""Disposes of the contents of an editor."""
if self.toolbar_ui is not None:
self.toolbar_ui.dispose()
if self._ui is not None:
self._ui.dispose()
self.grid.on_trait_change(
getattr(
self, "_selection_%s_updated" % self.factory.selection_mode
),
"selection_changed",
remove=True,
)
self.model.on_trait_change(self._model_sorted, "sorted", remove=True)
self.grid.dispose()
self.model.dispose()
# Break any links needed to allow garbage collection:
self.grid = self.model = self.toolbar = None
super().dispose()
def update_editor(self):
"""Updates the editor when the object trait changes externally to the
editor.
"""
# fixme: Do we need to override this method?
pass
def refresh(self):
"""Refreshes the editor control."""
self.grid._grid.Refresh()
def set_selection(self, objects=[], notify=True):
"""Sets the current selection to a set of specified objects."""
if not isinstance(objects, SequenceTypes):
objects = [objects]
self.grid.set_selection(
[TraitGridSelection(obj=object) for object in objects],
notify=notify,
)
def set_extended_selection(self, *pairs):
"""Sets the current selection to a set of specified object/column
pairs.
"""
if (len(pairs) == 1) and isinstance(pairs[0], list):
pairs = pairs[0]
grid_selections = [
TraitGridSelection(obj=object, name=name) for object, name in pairs
]
self.grid.set_selection(grid_selections)
def create_new_row(self):
"""Creates a new row object using the provided factory."""
factory = self.factory
kw = factory.row_factory_kw.copy()
if "__table_editor__" in kw:
kw["__table_editor__"] = self
return self.ui.evaluate(
factory.row_factory, *factory.row_factory_args, **kw
)
def add_row(self, object=None, index=None):
"""Adds a specified object as a new row after the specified index."""
filtered_items = self.model.get_filtered_items
if index is None:
indices = self.selected_indices
if len(indices) == 0:
indices = [len(filtered_items()) - 1]
indices.reverse()
else:
indices = [index]
if object is None:
objects = []
for index in indices:
object = self.create_new_row()
if object is None:
if self.in_row_mode:
self.set_selection()
return
objects.append(object)
else:
objects = [object]
items = []
insert_item_after = self.model.insert_filtered_item_after
in_row_mode = self.in_row_mode
for i, index in enumerate(indices):
object = objects[i]
index, extend = insert_item_after(index, object)
if in_row_mode and (object in filtered_items()):
items.append(object)
self._add_undo(
ListUndoItem(
object=self.object,
name=self.name,
index=index,
added=[object],
),
extend,
)
if in_row_mode:
self.set_selection(items)
def move_column(self, from_column, to_column):
"""Moves the specified **from_column** from its current position to
just preceding the specified **to_column**.
"""
columns = self.columns
frm = columns.index(from_column)
if to_column is None:
to = len(columns)
else:
to = columns.index(to_column)
del columns[frm]
columns.insert(to - (frm < to), from_column)
return True
# -- Property Implementations ---------------------------------------------
def _get_selected_indices(self):
sm = self.factory.selection_mode
if sm == "rows":
return self.selected_row_indices
elif sm == "row":
index = self.selected_row_index
if index >= 0:
return [index]
elif sm == "cells":
return list({row_col[0] for row_col in self.selected_cell_indices})
elif sm == "cell":
index = self.selected_cell_index[0]
if index >= 0:
return [index]
return []
def _get_selected_items(self):
sm = self.factory.selection_mode
if sm == "rows":
return self.selected_rows
elif sm == "row":
item = self.selected_row
if item is not None:
return [item]
elif sm == "cells":
return list({item_name[0] for item_name in self.selected_cells})
elif sm == "cell":
item = self.selected_cell[0]
if item is not None:
return [item]
return []
def _get_selected_values(self):
if self.in_row_mode:
return [(item, "") for item in self.selected_items]
if self.in_column_mode:
if self.factory.selection_mode == "columns":
return [(None, column) for column in self.selected_columns]
column = self.selected_column
if column != "":
return [(None, column)]
return []
if self.factory.selection_mode == "cells":
return self.selected_cells
item = self.selected_cell
if item[0] is not None:
return [item]
return []
def _get_in_row_mode(self):
return self.factory.selection_mode in ("row", "rows")
def _get_in_column_mode(self):
return self.factory.selection_mode in ("column", "columns")
# -- UI preference save/restore interface ---------------------------------
def restore_prefs(self, prefs):
"""Restores any saved user preference information associated with the
editor.
"""
factory = self.factory
try:
filters = prefs.get("filters", None)
if filters is not None:
factory.filters = [
f for f in factory.filters if f.template
] + [f for f in filters if not f.template]
columns = prefs.get("columns")
if columns is not None:
new_columns = []
all_columns = self.columns + factory.other_columns
for column in columns:
for column2 in all_columns:
if column == column2.get_label():
new_columns.append(column2)
break
self.columns = new_columns
# Restore the column sizes if possible:
if not factory.auto_size:
widths = prefs.get("widths")
if widths is not None:
# fixme: Talk to Jason about a better way to do this:
self.grid._user_col_size = True
set_col_size = self.grid._grid.SetColSize
for i, width in enumerate(widths):
if width >= 0:
set_col_size(i, width)
structure = prefs.get("structure")
if (structure is not None) and (factory.edit_view != " "):
self.control.GetSizer().SetStructure(self.control, structure)
except Exception:
pass
def save_prefs(self):
"""Returns any user preference information associated with the editor."""
get_col_size = self.grid._grid.GetColSize
result = {
"filters": [f for f in self.factory.filters if not f.template],
"columns": [c.get_label() for c in self.columns],
"widths": [get_col_size(i) for i in range(len(self.columns))],
}
if self.factory.edit_view != " ":
result["structure"] = self.control.GetSizer().GetStructure()
return result
# -- Public Methods -------------------------------------------------------
def filter_modified(self):
"""Handles updating the selection when some aspect of the current
filter has changed.
"""
values = self.selected_values
if len(values) > 0:
if self.in_column_mode:
self.set_extended_selection(values)
else:
items = self.model.get_filtered_items()
self.set_extended_selection(
[item for item in values if item[0] in items]
)
# -- Event Handlers -------------------------------------------------------
def _selection_row_updated(self, event):
"""Handles the user selecting items (rows, columns, cells) in the
table.
"""
gfi = self.model.get_filtered_item
rio = self.model.raw_index_of
tl = self.grid._grid.GetSelectionBlockTopLeft()
br = iter(self.grid._grid.GetSelectionBlockBottomRight())
rows = len(self.model.get_filtered_items())
if self.auto_add:
rows -= 1
# Get the row items and indices in the selection:
values = []
for row0, col0 in tl:
row1, col1 = next(br)
for row in range(row0, row1 + 1):
if row < rows:
values.append((rio(row), gfi(row)))
if len(values) > 0:
# Sort by increasing row index:
values.sort(key=itemgetter(0))
index, row = values[0]
else:
index, row = -1, None
# Save the new selection information:
self.trait_set(selected_row_index=index, trait_change_notify=False)
self.setx(selected_row=row)
# Update the toolbar status:
self._update_toolbar(row is not None)
# Invoke the user 'on_select' handler:
self.ui.evaluate(self.factory.on_select, row)
def _selection_rows_updated(self, event):
"""Handles multiple row selection changes."""
gfi = self.model.get_filtered_item
rio = self.model.raw_index_of
tl = self.grid._grid.GetSelectionBlockTopLeft()
br = iter(self.grid._grid.GetSelectionBlockBottomRight())
rows = len(self.model.get_filtered_items())
if self.auto_add:
rows -= 1
# Get the row items and indices in the selection:
values = []
for row0, col0 in tl:
row1, col1 = next(br)
for row in range(row0, row1 + 1):
if row < rows:
values.append((rio(row), gfi(row)))
# Sort by increasing row index:
values.sort(key=itemgetter(0))
# Save the new selection information:
self.trait_set(
selected_row_indices=[v[0] for v in values],
trait_change_notify=False,
)
rows = [v[1] for v in values]
self.setx(selected_rows=rows)
# Update the toolbar status:
self._update_toolbar(len(values) > 0)
# Invoke the user 'on_select' handler:
self.ui.evaluate(self.factory.on_select, rows)
def _selection_column_updated(self, event):
"""Handles single column selection changes."""
cols = self.columns
tl = self.grid._grid.GetSelectionBlockTopLeft()
br = iter(self.grid._grid.GetSelectionBlockBottomRight())
# Get the column items and indices in the selection:
values = []
for row0, col0 in tl:
row1, col1 = next(br)
for col in range(col0, col1 + 1):
values.append((col, cols[col].name))
if len(values) > 0:
# Sort by increasing column index:
values.sort(key=itemgetter(0))
index, column = values[0]
else:
index, column = -1, ""
# Save the new selection information:
self.trait_set(selected_column_index=index, trait_change_notify=False)
self.setx(selected_column=column)
# Invoke the user 'on_select' handler:
self.ui.evaluate(self.factory.on_select, column)
def _selection_columns_updated(self, event):
"""Handles multiple column selection changes."""
cols = self.columns
tl = self.grid._grid.GetSelectionBlockTopLeft()
br = iter(self.grid._grid.GetSelectionBlockBottomRight())
# Get the column items and indices in the selection:
values = []
for row0, col0 in tl:
row1, col1 = next(br)
for col in range(col0, col1 + 1):
values.append((col, cols[col].name))
# Sort by increasing row index:
values.sort(key=itemgetter(0))
# Save the new selection information:
self.trait_set(
selected_column_indices=[v[0] for v in values],
trait_change_notify=False,
)
columns = [v[1] for v in values]
self.setx(selected_columns=columns)
# Invoke the user 'on_select' handler:
self.ui.evaluate(self.factory.on_select, columns)
def _selection_cell_updated(self, event):
"""Handles single cell selection changes."""
tl = self.grid._grid.GetSelectionBlockTopLeft()
if len(tl) == 0:
return
gfi = self.model.get_filtered_item
rio = self.model.raw_index_of
cols = self.columns
br = iter(self.grid._grid.GetSelectionBlockBottomRight())
# Get the column items and indices in the selection:
values = []
for row0, col0 in tl:
row1, col1 = next(br)
for row in range(row0, row1 + 1):
item = gfi(row)
for col in range(col0, col1 + 1):
values.append(((rio(row), col), (item, cols[col].name)))
if len(values) > 0:
# Sort by increasing row, column index:
values.sort(key=itemgetter(0))
index, cell = values[0]
else:
index, cell = (-1, -1), (None, "")
# Save the new selection information:
self.trait_set(selected_cell_index=index, trait_change_notify=False)
self.setx(selected_cell=cell)
# Update the toolbar status:
self._update_toolbar(len(values) > 0)
# Invoke the user 'on_select' handler:
self.ui.evaluate(self.factory.on_select, cell)
def _selection_cells_updated(self, event):
"""Handles multiple cell selection changes."""
gfi = self.model.get_filtered_item
rio = self.model.raw_index_of
cols = self.columns
tl = self.grid._grid.GetSelectionBlockTopLeft()
br = iter(self.grid._grid.GetSelectionBlockBottomRight())
# Get the column items and indices in the selection:
values = []
for row0, col0 in tl:
row1, col1 = next(br)
for row in range(row0, row1 + 1):
item = gfi(row)
for col in range(col0, col1 + 1):
values.append(((rio(row), col), (item, cols[col].name)))
# Sort by increasing row, column index:
values.sort(key=itemgetter(0))
# Save the new selection information:
self.setx(selected_cell_indices=[v[0] for v in values])
cells = [v[1] for v in values]
self.setx(selected_cells=cells)
# Update the toolbar status:
self._update_toolbar(len(cells) > 0)
# Invoke the user 'on_select' handler:
self.ui.evaluate(self.factory.on_select, cells)
def _selected_row_changed(self, item):
if not self._no_notify:
if item is None:
self.set_selection(notify=False)
else:
self.set_selection(item, notify=False)
def _selected_row_index_changed(self, row):
if not self._no_notify:
if row < 0:
self.set_selection(notify=False)
else:
self.set_selection(self.value[row], notify=False)
def _selected_rows_changed(self, items):
if not self._no_notify:
self.set_selection(items, notify=False)
def _selected_row_indices_changed(self, indices):
if not self._no_notify:
value = self.value
self.set_selection([value[i] for i in indices], notify=False)
def _selected_column_changed(self, name):
if not self._no_notify:
self.set_extended_selection((None, name))
def _selected_column_index_changed(self, index):
if not self._no_notify:
if index < 0:
self.set_extended_selection()
else:
self.set_extended_selection(
(None, self.model.get_column_name(index))
)
def _selected_columns_changed(self, names):
if not self._no_notify:
self.set_extended_selection([(None, name) for name in names])
def _selected_column_indices_changed(self, indices):
if not self._no_notify:
gcn = self.model.get_column_name
self.set_extended_selection([(None, gcn(i)) for i in indices])
def _selected_cell_changed(self, cell):
if not self._no_notify:
self.set_extended_selection([cell])
def _selected_cell_index_changed(self, pair):
if not self._no_notify:
row, column = pair
if (row < 0) or (column < 0):
self.set_extended_selection()
else:
self.set_extended_selection(
(self.value[row], self.model.get_column_name(column))
)
def _selected_cells_changed(self, cells):
if not self._no_notify:
self.set_extended_selection(cells)
def _selected_cell_indices_changed(self, pairs):
if not self._no_notify:
value = self.value
gcn = self.model.get_column_name
new_selection = [(value[row], gcn(col)) for row, col in pairs]
self.set_extended_selection(new_selection)
def _update_toolbar(self, has_selection):
"""Updates the toolbar after a selection change."""
toolbar = self.toolbar
if toolbar is not None:
no_filter = self.filter is None
if has_selection:
indices = self.selected_indices
start = indices[0]
n = len(self.model.get_filtered_items()) - 1
delete = toolbar.delete
if self.auto_add:
n -= 1
delete.enabled = start <= n
else:
delete.enabled = True
deletable = self.factory.deletable
if delete.enabled and callable(deletable):
delete.enabled = all(
deletable(item) for item in self.selected_items
)
toolbar.add.enabled = toolbar.search.enabled = no_filter
toolbar.move_up.enabled = no_filter and (start > 0)
toolbar.move_down.enabled = no_filter and (indices[-1] < n)
else:
toolbar.add.enabled = toolbar.search.enabled = no_filter
toolbar.delete.enabled = (
toolbar.move_up.enabled
) = toolbar.move_down.enabled = False
def _model_sorted(self):
"""Handles the contents of the model being resorted."""
if self.toolbar is not None:
self.toolbar.no_sort.enabled = True
values = self.selected_values
if len(values) > 0:
do_later(self.set_extended_selection, values)
def _filter_changed(self, old_filter, new_filter):
"""Handles the current filter being changed."""
if new_filter is customize_filter:
do_later(self._customize_filters, old_filter)
elif self.model is not None:
if (new_filter is not None) and (
not isinstance(new_filter, TableFilter)
):
new_filter = TableFilter(allowed=new_filter)
self.model.filter = new_filter
self.filter_modified()
def _refresh_filters(self, filters):
factory = self.factory
# hack: The following line forces the 'filters' to be changed...
factory.filters = []
factory.filters = filters
def _customize_filters(self, filter):
"""Allows the user to customize the current set of table filters."""
factory = self.factory
filter_editor = TableFilterEditor(editor=self, filter=filter)
enum_editor = EnumEditor(values=factory.filters[:], mode="list")
ui = filter_editor.edit_traits(
parent=self.control,
view=View(
[
[
Item(
"filter<200>@", editor=enum_editor, resizable=True
),
"|<>",
],
["edit:edit", "new", "apply", "delete:delete", "|<>"],
"-",
],
title="Customize Filters",
kind="livemodal",
height=0.25,
buttons=["OK", "Cancel"],
),
)
if ui.result:
self._refresh_filters(enum_editor.values)
self.filter = filter_editor.filter
else:
self.filter = filter
def on_no_sort(self):
"""Handles the user requesting that columns not be sorted."""
self.model.no_column_sort()
self.toolbar.no_sort.enabled = False
values = self.selected_values
if len(values) > 0:
self.set_extended_selection(values)
def on_move_up(self):
"""Handles the user requesting to move the current item up one row."""
model = self.model
objects = []
for index in self.selected_indices:
objects.append(model.get_filtered_item(index))
index -= 1
object = model.get_filtered_item(index)
model.delete_filtered_item_at(index)
model.insert_filtered_item_after(index, object)
if self.in_row_mode:
self.set_selection(objects)
else:
self.set_extended_selection(self.selected_values)
def on_move_down(self):
"""Handles the user requesting to move the current item down one row."""
model = self.model
objects = []
indices = self.selected_indices[:]
indices.reverse()
for index in indices:
object = model.get_filtered_item(index)
objects.append(object)
model.delete_filtered_item_at(index)
model.insert_filtered_item_after(index, object)
if self.in_row_mode:
self.set_selection(objects)
else:
self.set_extended_selection(self.selected_values)
def on_search(self):
"""Handles the user requesting a table search."""
self.factory.search.edit_traits(
parent=self.control,
view="searchable_view",
handler=TableSearchHandler(editor=self),
)
def on_add(self):
"""Handles the user requesting to add a new row to the table."""
self.add_row()
def on_delete(self):
"""Handles the user requesting to delete the currently selected items
of the table.
"""
# Get the selected row indices:
indices = self.selected_indices[:]
values = self.selected_values[:]
indices.reverse()
# Make sure that we don't delete any rows while an editor is open in it
self.grid.stop_editing_indices(indices)
# Delete the selected rows:
for i in indices:
index, object = self.model.delete_filtered_item_at(i)
self._add_undo(
ListUndoItem(
object=self.object,
name=self.name,
index=index,
removed=[object],
)
)
# Compute the new selection and set it:
items = self.model.get_filtered_items()
n = len(items) - 1
indices.reverse()
for i in range(len(indices) - 1, -1, -1):
if indices[i] > n:
indices[i] = n
if indices[i] < 0:
del indices[i]
del values[i]
n = len(indices)
if n > 0:
if self.in_row_mode:
self.set_selection(list({items[i] for i in indices}))
else:
self.set_extended_selection(
list({(items[indices[i]], values[i][1]) for i in range(n)})
)
else:
self._update_toolbar(False)
def on_prefs(self):
"""Handles the user requesting to set the user preference items for the
table.
"""
columns = self.columns[:]
columns.extend(
[
c
for c in (self.factory.columns + self.factory.other_columns)
if c not in columns
]
)
self.edit_traits(
parent=self.control,
view=View(
[
Item(
"columns",
resizable=True,
editor=SetEditor(
values=columns, ordered=True, can_move_all=False
),
),
"|<>",
],
title="Select and Order Columns",
width=0.3,
height=0.3,
resizable=True,
buttons=["Undo", "OK", "Cancel"],
kind="livemodal",
),
)
def prepare_menu(self, row, column):
"""Prepares to have a context menu action called."""
object = self.model.get_filtered_item(row)
selection = [x.obj for x in self.grid.get_selection()]
if object not in selection:
self.set_selection(object)
selection = [object]
self.set_menu_context(selection, object, column)
def setx(self, **keywords):
"""Set one or more attributes without notifying the grid model."""
self._no_notify = True
for name, value in keywords.items():
setattr(self, name, value)
self._no_notify = False
# -- Private Methods: -----------------------------------------------------
def _add_undo(self, undo_item, extend=False):
history = self.ui.history
if history is not None:
history.add(undo_item, extend)
class TableFilterEditor(Handler):
"""Editor that manages table filters."""
#: TableEditor this editor is associated with
editor = Instance(TableEditor)
#: Current filter
filter = Instance(TableFilter, allow_none=True)
#: Edit the current filter
edit = Button()
#: Create a new filter and edit it
new = Button()
#: Apply the current filter to the editor's table
apply = Button()
#: Delete the current filter
delete = Button()
# -------------------------------------------------------------------------
# 'Handler' interface:
# -------------------------------------------------------------------------
def init(self, info):
"""Initializes the controls of a user interface."""
# Save both the original filter object reference and its contents:
if self.filter is None:
self.filter = info.filter.factory.values[0]
self._filter = self.filter
self._filter_copy = self.filter.clone_traits()
return True
def closed(self, info, is_ok):
"""Handles a dialog-based user interface being closed by the user."""
if not is_ok:
# Restore the contents of the original filter:
self._filter.copy_traits(self._filter_copy)
# -------------------------------------------------------------------------
# Event handlers:
# -------------------------------------------------------------------------
def object_filter_changed(self, info):
"""Handles a new filter being selected."""
filter = info.object.filter
info.edit.enabled = not filter.template
info.delete.enabled = (not filter.template) and (
len(info.filter.factory.values) > 1
)
def object_edit_changed(self, info):
"""Handles the user clicking the **Edit** button."""
if info.initialized:
items = self.editor.model.get_filtered_items()
if len(items) > 0:
item = items[0]
else:
item = None
# `item` is now either the first item in the table, or None if
# the table is empty.
ui = self.filter.edit(item)
if ui.result:
self._refresh_filters(info)
def object_new_changed(self, info):
"""Handles the user clicking the **New** button."""
if info.initialized:
# Get list of available filters and find the current filter in it:
factory = info.filter.factory
filters = factory.values
filter = self.filter
index = filters.index(filter) + 1
n = len(filters)
while (index < n) and filters[index].template:
index += 1
# Create a new filter based on the current filter:
new_filter = filter.clone_traits()
new_filter.template = False
new_filter.name = new_filter._name = "New filter"
# Add it to the list of filters:
filters.insert(index, new_filter)
self._refresh_filters(info)
# Set up the new filter as the current filter and edit it:
self.filter = new_filter
do_later(self._delayed_edit, info)
def object_apply_changed(self, info):
"""Handles the user clicking the **Apply** button."""
if info.initialized:
self.init(info)
self.editor._refresh_filters(info.filter.factory.values)
self.editor.filter = self.filter
def object_delete_changed(self, info):
"""Handles the user clicking the **Delete** button."""
# Get the list of available filters:
filters = info.filter.factory.values
if info.initialized:
# Delete the current filter:
index = filters.index(self.filter)
del filters[index]
# Select a new filter:
if index >= len(filters):
index -= 1
self.filter = filters[index]
self._refresh_filters(info)
# -------------------------------------------------------------------------
# Private methods:
# -------------------------------------------------------------------------
def _refresh_filters(self, info):
"""Refresh the filter editor's list of filters."""
factory = info.filter.factory
values, factory.values = factory.values, []
factory.values = values
def _delayed_edit(self, info):
"""Edits the current filter, and deletes it if the user cancels the
edit.
"""
ui = self.filter.edit(self.editor.model.get_filtered_item(0))
if not ui.result:
self.object_delete_changed(info)
else:
self._refresh_filters(info)
# Allow deletion as long as there is more than 1 filter:
if (not self.filter.template) and len(info.filter.factory.values) > 1:
info.delete.enabled = True
class TableEditorToolbar(HasPrivateTraits):
"""Toolbar displayed in table editors."""
# -------------------------------------------------------------------------
# Trait definitions:
# -------------------------------------------------------------------------
#: Do not sort columns:
no_sort = Instance(
Action,
{
"name": "No Sorting",
"tooltip": "Do not sort columns",
"action": "on_no_sort",
"enabled": False,
"image": ImageResource("table_no_sort.png"),
},
)
#: Move current object up one row:
move_up = Instance(
Action,
{
"name": "Move Up",
"tooltip": "Move current item up one row",
"action": "on_move_up",
"enabled": False,
"image": ImageResource("table_move_up.png"),
},
)
#: Move current object down one row:
move_down = Instance(
Action,
{
"name": "Move Down",
"tooltip": "Move current item down one row",
"action": "on_move_down",
"enabled": False,
"image": ImageResource("table_move_down.png"),
},
)
#: Search the table:
search = Instance(
Action,
{
"name": "Search",
"tooltip": "Search table",
"action": "on_search",
"image": ImageResource("table_search.png"),
},
)
#: Add a row:
add = Instance(
Action,
{
"name": "Add",
"tooltip": "Insert new item",
"action": "on_add",
"image": ImageResource("table_add.png"),
},
)
#: Delete selected row:
delete = Instance(
Action,
{
"name": "Delete",
"tooltip": "Delete current item",
"action": "on_delete",
"enabled": False,
"image": ImageResource("table_delete.png"),
},
)
#: Edit the user preferences:
prefs = Instance(
Action,
{
"name": "Preferences",
"tooltip": "Set user preferences for table",
"action": "on_prefs",
"image": ImageResource("table_prefs.png"),
},
)
#: The table editor that this is the toolbar for:
editor = Instance(TableEditor)
#: The toolbar control:
control = Any()
def __init__(self, parent=None, **traits):
super().__init__(**traits)
editor = self.editor
factory = editor.factory
actions = []
if factory.sortable and (not factory.sort_model):
actions.append(self.no_sort)
if (not editor.in_column_mode) and factory.reorderable:
actions.append(self.move_up)
actions.append(self.move_down)
if editor.in_row_mode and (factory.search is not None):
actions.append(self.search)
if factory.editable:
if (factory.row_factory is not None) and (not factory.auto_add):
actions.append(self.add)
if (factory.deletable) and (not editor.in_column_mode):
actions.append(self.delete)
if factory.configurable:
actions.append(self.prefs)
if len(actions) > 0:
toolbar = ToolBar(
image_size=(16, 16),
show_tool_names=False,
show_divider=False,
*actions,
)
self.control = toolbar.create_tool_bar(parent, self)
self.control.SetBackgroundColour(parent.GetBackgroundColour())
# fixme: Why do we have to explictly set the size of the toolbar?
# Is there some method that needs to be called to do the
# layout?
self.control.SetSize(wx.Size(23 * len(actions), 16))
# -------------------------------------------------------------------------
# Pyface/Traits menu/toolbar controller interface:
# -------------------------------------------------------------------------
def add_to_menu(self, menu_item):
"""Adds a menu item to the menu bar being constructed."""
pass
def add_to_toolbar(self, toolbar_item):
"""Adds a toolbar item to the too bar being constructed."""
pass
def can_add_to_menu(self, action):
"""Returns whether the action should be defined in the user interface."""
return True
def can_add_to_toolbar(self, action):
"""Returns whether the toolbar action should be defined in the user
interface.
"""
return True
def perform(self, action, action_event=None):
"""Performs the action described by a specified Action object."""
getattr(self.editor, action.action)()
class TableSearchHandler(Handler):
"""Handler for saerching a table."""
# -------------------------------------------------------------------------
# Trait definitions:
# -------------------------------------------------------------------------
#: The editor that this handler is associated with
editor = Instance(TableEditor)
#: Find next matching item
find_next = Button("Find Next")
#: Find previous matching item
find_previous = Button("Find Previous")
#: Select all matching items
select = Button()
#: The user is finished searching
OK = Button("Close")
#: Search status message:
status = Str()
def handler_find_next_changed(self, info):
"""Handles the user clicking the **Find** button."""
if info.initialized:
editor = self.editor
items = editor.model.get_filtered_items()
for i in range(editor.selected_row_index + 1, len(items)):
if info.object.filter(items[i]):
self.status = "Item %d matches" % (i + 1)
editor.set_selection(items[i])
editor.selected_row_index = i
break
else:
self.status = "No more matches found"
def handler_find_previous_changed(self, info):
"""Handles the user clicking the **Find previous** button."""
if info.initialized:
editor = self.editor
items = editor.model.get_filtered_items()
for i in range(editor.selected_row_index - 1, -1, -1):
if info.object.filter(items[i]):
self.status = "Item %d matches" % (i + 1)
editor.set_selection(items[i])
editor.selected_row_index = i
break
else:
self.status = "No more matches found"
def handler_select_changed(self, info):
"""Handles the user clicking the **Select** button."""
if info.initialized:
editor = self.editor
filter = info.object.filter
items = [
item
for item in editor.model.get_filtered_items()
if filter(item)
]
editor.set_selection(items)
if len(items) == 1:
self.status = "1 item selected"
else:
self.status = "%d items selected" % len(items)
def handler_OK_changed(self, info):
"""Handles the user clicking the OK button."""
if info.initialized:
self.close(info, True)
# Define the SimpleEditor class.
SimpleEditor = TableEditor
# Define the ReadonlyEditor class.
ReadonlyEditor = TableEditor