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!
""" A traits UI editor for editing tabular data (arrays, list of tuples, lists
of objects, etc).
"""
import wx
import wx.lib.mixins.listctrl as listmix
from traits.api import (
HasStrictTraits,
Int,
List,
Bool,
Instance,
Any,
Event,
Property,
TraitListEvent,
Dict,
)
from traitsui.ui_traits import Image
from traitsui.tabular_adapter import TabularAdapter
from traitsui.wx.editor import Editor
from pyface.image_resource import ImageResource
from pyface.timer.api import do_later
from .constants import is_mac, scrollbar_dx
try:
from pyface.wx.drag_and_drop import PythonDropSource, PythonDropTarget
except:
PythonDropSource = PythonDropTarget = None
# Mapping for trait alignment values to wx alignment values:
alignment_map = {
"left": wx.LIST_FORMAT_LEFT,
"center": wx.LIST_FORMAT_CENTRE,
"right": wx.LIST_FORMAT_RIGHT,
}
class TextEditMixin(listmix.TextEditMixin):
def __init__(self, edit_labels):
"""edit_labels controls whether the first column is editable"""
self.edit_labels = edit_labels
listmix.TextEditMixin.__init__(self)
def OpenEditor(self, col, row):
if col == 0 and not self.edit_labels:
return
else:
return listmix.TextEditMixin.OpenEditor(self, col, row)
class wxListCtrl(wx.ListCtrl, TextEditMixin):
"""Subclass of wx.ListCtrl to provide correct virtual list behavior."""
def __init__(
self,
parent,
ID,
pos=wx.DefaultPosition,
size=wx.DefaultSize,
style=0,
can_edit=False,
edit_labels=False,
):
wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
# if the selected is editable, then we have to init the mixin
if can_edit:
TextEditMixin.__init__(self, edit_labels)
def make_editor(self, col_style=wx.LIST_FORMAT_LEFT):
# override implementation in base class due to issue with destroying
# editor
style = wx.TE_PROCESS_ENTER | wx.TE_PROCESS_TAB | wx.TE_RICH2
style |= {
wx.LIST_FORMAT_LEFT: wx.TE_LEFT,
wx.LIST_FORMAT_RIGHT: wx.TE_RIGHT,
wx.LIST_FORMAT_CENTRE: wx.TE_CENTRE,
}[col_style]
editor = wx.TextCtrl(self, -1, style=style)
editor.SetBackgroundColour(self.editorBgColour)
editor.SetForegroundColour(self.editorFgColour)
font = self.GetFont()
editor.SetFont(font)
self.curRow = 0
self.curCol = 0
editor.Hide()
# Base class does explicit Destroy call here. Should not be needed.
# Excised code is as follows:
# if hasattr(self, 'editor'):
# self.editor.Destroy()
# Dropping the reference to the editor should result in the
# destruction of the underlying C++ widget just fine.
self.editor = editor
self.col_style = col_style
self.editor.Bind(wx.EVT_CHAR, self.OnChar)
self.editor.Bind(wx.EVT_KILL_FOCUS, self.CloseEditor)
def SetVirtualData(self, row, col, text):
# this method is called but the job is already done by
# the _end_label_edit method. Commmented code is availabed
# if needed
pass
# edit = self._editor
# return editor.adapter.set_text( editor.object, editor.name,
# row, col, text )
def OnGetItemAttr(self, row):
"""Returns the display attributes to use for the specified list item."""
# fixme: There appears to be a bug in wx in that they do not correctly
# manage the reference count for the returned object, and it seems to be
# gc'ed before they finish using it. So we store an object reference to
# it to prevent it from going away too soon...
self._attr = attr = wx.ItemAttr()
editor = self._editor
object, name = editor.object, editor.name
color = editor.adapter.get_bg_color(object, name, row)
if color is not None:
attr.SetBackgroundColour(color)
color = editor.adapter.get_text_color(object, name, row)
if color is not None:
attr.SetTextColour(color)
font = editor.adapter.get_font(object, name, row)
if font is not None:
attr.SetFont(font)
return attr
def OnGetItemImage(self, row):
"""Returns the image index to use for the specified list item."""
editor = self._editor
image = editor._get_image(
editor.adapter.get_image(editor.object, editor.name, row, 0)
)
if image is not None:
return image
return -1
def OnGetItemColumnImage(self, row, column):
"""Returns the image index to use for the specified list item."""
editor = self._editor
image = editor._get_image(
editor.adapter.get_image(editor.object, editor.name, row, column)
)
if image is not None:
return image
return -1
def OnGetItemText(self, row, column):
"""Returns the text to use for the specified list item."""
editor = self._editor
return editor.adapter.get_text(editor.object, editor.name, row, column)
class TabularEditor(Editor):
"""A traits UI editor for editing tabular data (arrays, list of tuples,
lists of objects, etc).
"""
# -- Trait Definitions ----------------------------------------------------
#: The event fired when a table update is needed:
update = Event()
#: The event fired when a simple repaint is needed:
refresh = Event()
#: The current set of selected items (which one is used depends upon the
#: initial state of the editor factory 'multi_select' trait):
selected = Any()
multi_selected = List()
#: The current set of selected item indices (which one is used depends upon
#: the initial state of the editor factory 'multi_select' trait):
selected_row = Int(-1)
multi_selected_rows = List(Int)
#: The most recently actived item and its index:
activated = Any()
activated_row = Int()
#: The most recent left click data:
clicked = Instance("TabularEditorEvent")
#: The most recent left double click data:
dclicked = Instance("TabularEditorEvent")
#: The most recent right click data:
right_clicked = Instance("TabularEditorEvent")
#: The most recent right double click data:
right_dclicked = Instance("TabularEditorEvent")
#: The most recent column click data:
column_clicked = Instance("TabularEditorEvent")
#: Is the tabular editor scrollable? This value overrides the default.
scrollable = True
#: Row index of item to select after rebuilding editor list:
row = Any()
#: Should the selected item be edited after rebuilding the editor list:
edit = Bool(False)
#: The adapter from trait values to editor values:
adapter = Instance(TabularAdapter)
#: Dictionary mapping image names to wx.ImageList indices:
images = Dict()
#: Dictionary mapping ImageResource objects to wx.ImageList indices:
image_resources = Dict()
#: An image being converted:
image = Image
#: Flag for marking whether the update was within the visible area
_update_visible = Bool(False)
def init(self, parent):
"""Finishes initializing the editor by creating the underlying toolkit
widget.
"""
factory = self.factory
# Set up the adapter to use:
self.adapter = factory.adapter
# Determine the style to use for the list control:
style = wx.LC_REPORT | wx.LC_VIRTUAL | wx.BORDER_NONE
if factory.editable_labels:
style |= wx.LC_EDIT_LABELS
if factory.horizontal_lines:
style |= wx.LC_HRULES
if factory.vertical_lines:
style |= wx.LC_VRULES
if not factory.multi_select:
style |= wx.LC_SINGLE_SEL
if not factory.show_titles:
style |= wx.LC_NO_HEADER
# Create the list control and link it back to us:
self.control = control = wxListCtrl(
parent,
-1,
style=style,
can_edit=factory.editable,
edit_labels=factory.editable_labels,
)
control._editor = self
# Create the list control column:
# fixme: what do we do here?
# control.InsertColumn( 0, '' )
# Set up the list control's event handlers:
id = control.GetId()
parent.Bind(wx.EVT_LIST_BEGIN_DRAG, self._begin_drag, id=id)
parent.Bind(
wx.EVT_LIST_BEGIN_LABEL_EDIT, self._begin_label_edit, id=id
)
parent.Bind(wx.EVT_LIST_END_LABEL_EDIT, self._end_label_edit, id=id)
parent.Bind(wx.EVT_LIST_ITEM_SELECTED, self._item_selected, id=id)
parent.Bind(wx.EVT_LIST_ITEM_DESELECTED, self._item_selected, id=id)
parent.Bind(wx.EVT_LIST_KEY_DOWN, self._key_down, id=id)
parent.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self._item_activated, id=id)
parent.Bind(wx.EVT_LIST_COL_END_DRAG, self._size_modified, id=id)
parent.Bind(
wx.EVT_LIST_COL_RIGHT_CLICK, self._column_right_clicked, id=id
)
parent.Bind(wx.EVT_LIST_COL_CLICK, self._column_clicked, id=id)
control.Bind(wx.EVT_LEFT_DOWN, self._left_down)
control.Bind(wx.EVT_LEFT_DCLICK, self._left_dclick)
control.Bind(wx.EVT_RIGHT_DOWN, self._right_down)
control.Bind(wx.EVT_RIGHT_DCLICK, self._right_dclick)
control.Bind(wx.EVT_MOTION, self._motion)
control.Bind(wx.EVT_SIZE, self._size_modified)
# Set up the drag and drop target:
if PythonDropTarget is not None:
control.SetDropTarget(PythonDropTarget(self))
# Set up the selection listener (if necessary):
if factory.multi_select:
self.sync_value(
factory.selected, "multi_selected", "both", is_list=True
)
self.sync_value(
factory.selected_row,
"multi_selected_rows",
"both",
is_list=True,
)
else:
self.sync_value(factory.selected, "selected", "both")
self.sync_value(factory.selected_row, "selected_row", "both")
# Synchronize other interesting traits as necessary:
self.sync_value(factory.update, "update", "from", is_event=True)
self.sync_value(factory.refresh, "refresh", "from", is_event=True)
self.sync_value(factory.activated, "activated", "to")
self.sync_value(factory.activated_row, "activated_row", "to")
self.sync_value(factory.clicked, "clicked", "to")
self.sync_value(factory.dclicked, "dclicked", "to")
self.sync_value(factory.right_clicked, "right_clicked", "to")
self.sync_value(factory.right_dclicked, "right_dclicked", "to")
self.sync_value(factory.column_clicked, "column_clicked", "to")
# Make sure we listen for 'items' changes as well as complete list
# replacements:
try:
self.context_object.on_trait_change(
self.update_editor,
self.extended_name + "_items",
dispatch="ui",
)
except:
pass
# If the user has requested automatic update, attempt to set up the
# appropriate listeners:
if factory.auto_update:
self.context_object.on_trait_change(
self.refresh_editor, self.extended_name + ".-", dispatch="ui"
)
# Create the mapping from user supplied images to wx.ImageList indices:
for image_resource in factory.images:
self._add_image(image_resource)
# Refresh the editor whenever the adapter changes:
self.on_trait_change(self._refresh, "adapter.+update", dispatch="ui")
# Rebuild the editor columns and headers whenever the adapter's
# 'columns' changes:
self.on_trait_change(
self._rebuild_all, "adapter.columns", dispatch="ui"
)
# Make sure the tabular view gets initialized:
self._rebuild()
# Set the list control's tooltip:
self.set_tooltip()
def dispose(self):
"""Disposes of the contents of an editor."""
# Remove all of the wx event handlers:
control = self.control
parent = control.GetParent()
id = control.GetId()
parent.Bind(wx.EVT_LIST_BEGIN_DRAG, None, id=id)
parent.Bind(wx.EVT_LIST_BEGIN_LABEL_EDIT, None, id=id)
parent.Bind(wx.EVT_LIST_END_LABEL_EDIT, None, id=id)
parent.Bind(wx.EVT_LIST_ITEM_SELECTED, None, id=id)
parent.Bind(wx.EVT_LIST_ITEM_DESELECTED, None, id=id)
parent.Bind(wx.EVT_LIST_KEY_DOWN, None, id=id)
parent.Bind(wx.EVT_LIST_ITEM_ACTIVATED, None, id=id)
parent.Bind(wx.EVT_LIST_COL_END_DRAG, None, id=id)
parent.Bind(wx.EVT_LIST_COL_RIGHT_CLICK, None, id=id)
parent.Bind(wx.EVT_LIST_COL_CLICK, None, id=id)
control.Unbind(wx.EVT_LEFT_DOWN)
control.Unbind(wx.EVT_LEFT_DCLICK)
control.Unbind(wx.EVT_RIGHT_DOWN)
control.Unbind(wx.EVT_RIGHT_DCLICK)
control.Unbind(wx.EVT_MOTION)
control.Unbind(wx.EVT_SIZE)
self.context_object.on_trait_change(
self.update_editor, self.extended_name + "_items", remove=True
)
if self.factory.auto_update:
self.context_object.on_trait_change(
self.refresh_editor, self.extended_name + ".-", remove=True
)
self.on_trait_change(self._refresh, "adapter.+update", remove=True)
self.on_trait_change(self._rebuild_all, "adapter.columns", remove=True)
super().dispose()
def _update_changed(self, event):
"""Handles the 'update' event being fired."""
if event is True:
self.update_editor()
elif isinstance(event, int):
self._refresh_row(event)
else:
self._refresh_editor(event)
def refresh_editor(self, item, name, old, new):
"""Handles a table item attribute being changed."""
self._refresh_editor(item)
def _refresh_editor(self, item):
"""Handles a table item being changed."""
adapter = self.adapter
object, name = self.object, self.name
agi = adapter.get_item
for row in range(adapter.len(object, name)):
if item is agi(object, name, row):
self._refresh_row(row)
return
self.update_editor()
def _refresh_row(self, row):
"""Updates the editor control when a specified table row changes."""
self.control.RefreshRect(
self.control.GetItemRect(row, wx.LIST_RECT_BOUNDS)
)
def _update_editor(self, object, name, old_value, new_value):
"""Performs updates when the object trait changes.
Overloads traitsui.editor.UIEditor
"""
self._update_visible = True
super()._update_editor(object, name, old_value, new_value)
def update_editor(self):
"""Updates the editor when the object trait changes externally to the
editor.
"""
control = self.control
n = self.adapter.len(self.object, self.name)
top = control.GetTopItem()
pn = control.GetCountPerPage()
bottom = min(top + pn - 1, n)
control.SetItemCount(n)
if self._update_visible:
control.RefreshItems(0, n - 1)
self._update_visible = False
if len(self.multi_selected_rows) > 0:
self._multi_selected_rows_changed(self.multi_selected_rows)
if len(self.multi_selected) > 0:
self._multi_selected_changed(self.multi_selected)
edit, self.edit = self.edit, False
row, self.row = self.row, None
if row is not None:
if row >= n:
row -= 1
if row < 0:
row = None
if row is None:
visible = bottom
if visible >= 0 and visible < control.GetItemCount():
control.EnsureVisible(visible)
return
if 0 <= (row - top) < pn:
control.EnsureVisible(
min(top + pn - 2, control.GetItemCount() - 1)
)
elif row < top:
control.EnsureVisible(
min(row + pn - 1, control.GetItemCount() - 1)
)
else:
control.EnsureVisible(row)
control.SetItemState(
row,
wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED,
wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED,
)
if edit:
control.EditLabel(row)
# -- Trait Event Handlers -------------------------------------------------
def _selected_changed(self, selected):
"""Handles the editor's 'selected' trait being changed."""
if not self._no_update:
if selected is None:
for row in self._get_selected():
self.control.SetItemState(row, 0, wx.LIST_STATE_SELECTED)
else:
try:
self.control.SetItemState(
self.value.index(selected),
wx.LIST_STATE_SELECTED,
wx.LIST_STATE_SELECTED,
)
except:
pass
def _selected_row_changed(self, old, new):
"""Handles the editor's 'selected_index' trait being changed."""
if not self._no_update:
if new < 0:
if old >= 0:
self.control.SetItemState(old, 0, wx.LIST_STATE_SELECTED)
else:
self.control.SetItemState(
new, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED
)
def _multi_selected_changed(self, selected):
"""Handles the editor's 'multi_selected' trait being changed."""
if not self._no_update:
values = self.value
try:
self._multi_selected_rows_changed(
[values.index(item) for item in selected]
)
except:
pass
def _multi_selected_items_changed(self, event):
"""Handles the editor's 'multi_selected' trait being modified."""
values = self.value
try:
self._multi_selected_rows_items_changed(
TraitListEvent(
index=0,
removed=[values.index(item) for item in event.removed],
added=[values.index(item) for item in event.added],
)
)
except:
pass
def _multi_selected_rows_changed(self, selected_rows):
"""Handles the editor's 'multi_selected_rows' trait being changed."""
if not self._no_update:
control = self.control
selected = self._get_selected()
# Select any new items that aren't already selected:
for row in selected_rows:
if row in selected:
selected.remove(row)
else:
control.SetItemState(
row, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED
)
# Unselect all remaining selected items that aren't selected now:
for row in selected:
control.SetItemState(row, 0, wx.LIST_STATE_SELECTED)
def _multi_selected_rows_items_changed(self, event):
"""Handles the editor's 'multi_selected_rows' trait being modified."""
control = self.control
# Remove all items that are no longer selected:
for row in event.removed:
control.SetItemState(row, 0, wx.LIST_STATE_SELECTED)
# Select all newly added items:
for row in event.added:
control.SetItemState(
row, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED
)
def _refresh_changed(self):
self.update_editor()
# -- List Control Event Handlers ------------------------------------------
def _left_down(self, event):
"""Handles the left mouse button being pressed."""
self._mouse_click(event, "clicked")
def _left_dclick(self, event):
"""Handles the left mouse button being double clicked."""
self._mouse_click(event, "dclicked")
def _right_down(self, event):
"""Handles the right mouse button being pressed."""
self._mouse_click(event, "right_clicked")
def _right_dclick(self, event):
"""Handles the right mouse button being double clicked."""
self._mouse_click(event, "right_dclicked")
def _begin_drag(self, event):
"""Handles the user beginning a drag operation with the left mouse
button.
"""
if PythonDropSource is not None:
adapter = self.adapter
object, name = self.object, self.name
selected = self._get_selected()
drag_items = []
# Collect all of the selected items to drag:
for row in selected:
drag = adapter.get_drag(object, name, row)
if drag is None:
return
drag_items.append(drag)
# Save the drag item indices, so that we can later handle a
# completed 'move' operation:
self._drag_rows = selected
try:
# If only one item is being dragged, drag it as an item, not a
# list:
if len(drag_items) == 1:
drag_items = drag_items[0]
# Perform the drag and drop operation:
ds = PythonDropSource(self.control, drag_items)
# If moves are allowed and the result was a drag move:
if (ds.result == wx.DragMove) and (
self._drag_local or self.factory.drag_move
):
# Then delete all of the original items (in reverse order
# from highest to lowest, so the indices don't need to be
# adjusted):
rows = self._drag_rows
rows.reverse()
for row in rows:
adapter.delete(object, name, row)
finally:
self._drag_rows = None
self._drag_local = False
def _begin_label_edit(self, event):
"""Handles the user starting to edit an item label."""
if not self.adapter.get_can_edit(
self.object, self.name, event.GetIndex()
):
event.Veto()
def _end_label_edit(self, event):
"""Handles the user finishing editing an item label."""
self.adapter.set_text(
self.object,
self.name,
event.GetIndex(),
event.GetColumn(),
event.GetText(),
)
self.row = event.GetIndex() + 1
def _item_selected(self, event):
"""Handles an item being selected."""
self._no_update = True
try:
get_item = self.adapter.get_item
object, name = self.object, self.name
selected_rows = self._get_selected()
if self.factory.multi_select:
self.multi_selected_rows = selected_rows
self.multi_selected = [
get_item(object, name, row) for row in selected_rows
]
elif len(selected_rows) == 0:
self.selected_row = -1
self.selected = None
else:
self.selected_row = selected_rows[0]
self.selected = get_item(object, name, selected_rows[0])
finally:
self._no_update = False
def _item_activated(self, event):
"""Handles an item being activated (double-clicked or enter pressed)."""
self.activated_row = event.GetIndex()
self.activated = self.adapter.get_item(
self.object, self.name, self.activated_row
)
def _key_down(self, event):
"""Handles the user pressing a key in the list control."""
key = event.GetKeyCode()
if key == wx.WXK_PAGEDOWN:
self._append_new()
elif key in (wx.WXK_BACK, wx.WXK_DELETE):
self._delete_current()
elif key == wx.WXK_INSERT:
self._insert_current()
elif key == wx.WXK_LEFT:
self._move_up_current()
elif key == wx.WXK_RIGHT:
self._move_down_current()
elif key in (wx.WXK_RETURN, wx.WXK_ESCAPE):
self._edit_current()
else:
event.Skip()
def _column_right_clicked(self, event):
"""Handles the user right-clicking a column header."""
column = event.GetColumn()
if (self._cached_widths is not None) and (
0 <= column < len(self._cached_widths)
):
self._cached_widths[column] = None
self._size_modified(event)
def _column_clicked(self, event):
"""Handles the right mouse button being double clicked."""
editor_event = TabularEditorEvent(
editor=self, row=0, column=event.GetColumn()
)
setattr(self, "column_clicked", editor_event)
event.Skip()
def _size_modified(self, event):
"""Handles the size of the list control being changed."""
control = self.control
n = control.GetColumnCount()
if n == 1:
dx, dy = control.GetClientSize()
control.SetColumnWidth(0, dx - 1)
elif n > 1:
do_later(self._set_column_widths)
event.Skip()
def _motion(self, event):
"""Handles the user moving the mouse."""
x = event.GetX()
column = self._get_column(x)
row, flags = self.control.HitTest(wx.Point(x, event.GetY()))
if (row != self._last_row) or (column != self._last_column):
self._last_row, self._last_column = row, column
if (row == -1) or (column is None):
tooltip = ""
else:
tooltip = self.adapter.get_tooltip(
self.object, self.name, row, column
)
if tooltip != self._last_tooltip:
self._last_tooltip = tooltip
wx.ToolTip.Enable(False)
wx.ToolTip.Enable(True)
self.control.SetToolTip(wx.ToolTip(tooltip))
# -- Drag and Drop Event Handlers -----------------------------------------
def wx_dropped_on(self, x, y, data, drag_result):
"""Handles a Python object being dropped on the list control."""
row, flags = self.control.HitTest(wx.Point(x, y))
# If the user dropped it on an empty list, set the target as past the
# end of the list:
if (
(row == -1)
and ((flags & wx.LIST_HITTEST_NOWHERE) != 0)
and (self.control.GetItemCount() == 0)
):
row = 0
# If we have a valid drop target row, proceed:
if row != -1:
if not isinstance(data, list):
# Handle the case of just a single item being dropped:
self._wx_dropped_on(row, data)
else:
# Handles the case of a list of items being dropped, being
# careful to preserve the original order of the source items if
# possible:
data.reverse()
for item in data:
self._wx_dropped_on(row, item)
# If this was an inter-list drag, mark it as 'local':
if self._drag_indices is not None:
self._drag_local = True
# Return a successful drop result:
return drag_result
# Indicate we could not process the drop:
return wx.DragNone
def _wx_dropped_on(self, row, item):
"""Helper method for handling a single item dropped on the list
control.
"""
adapter = self.adapter
object, name = self.object, self.name
# Obtain the destination of the dropped item relative to the target:
destination = adapter.get_dropped(object, name, row, item)
# Adjust the target index accordingly:
if destination == "after":
row += 1
# Insert the dropped item at the requested position:
adapter.insert(object, name, row, item)
# If the source for the drag was also this list control, we need to
# adjust the original source indices to account for their new position
# after the drag operation:
rows = self._drag_rows
if rows is not None:
for i in range(len(rows) - 1, -1, -1):
if rows[i] < row:
break
rows[i] += 1
def wx_drag_over(self, x, y, data, drag_result):
"""Handles a Python object being dragged over the tree."""
if isinstance(data, list):
rc = wx.DragNone
for item in data:
rc = self.wx_drag_over(x, y, item, drag_result)
if rc == wx.DragNone:
break
return rc
row, flags = self.control.HitTest(wx.Point(x, y))
# If the user is dragging over an empty list, set the target to the end
# of the list:
if (
(row == -1)
and ((flags & wx.LIST_HITTEST_NOWHERE) != 0)
and (self.control.GetItemCount() == 0)
):
row = 0
# If the drag target index is valid and the adapter says it is OK to
# drop the data here, then indicate the data can be dropped:
if (row != -1) and self.adapter.get_can_drop(
self.object, self.name, row, data
):
return drag_result
# Else indicate that we will not accept the data:
return wx.DragNone
# -- UI preference save/restore interface ---------------------------------
def restore_prefs(self, prefs):
"""Restores any saved user preference information associated with the
editor.
"""
self._cached_widths = cws = prefs.get("cached_widths")
if cws is not None:
set_column_width = self.control.SetColumnWidth
for i, width in enumerate(cws):
if width is not None:
set_column_width(i, width)
def save_prefs(self):
"""Returns any user preference information associated with the editor."""
cws = self._cached_widths
if cws is not None:
cws = [cw if cw is not None and cw >= 0 else None for cw in cws]
return {"cached_widths": cws}
# -- Private Methods ------------------------------------------------------
def _refresh(self):
"""Refreshes the contents of the editor's list control."""
n = self.adapter.len(self.object, self.name)
if n > 0:
self.control.RefreshItems(0, n - 1)
def _rebuild(self):
"""Rebuilds the contents of the editor's list control."""
control = self.control
control.ClearAll()
adapter, object, name = self.adapter, self.object, self.name
adapter.object, adapter.name = object, name
get_alignment = adapter.get_alignment
get_width = adapter.get_width
for i, label in enumerate(adapter.label_map):
control.InsertColumn(
i,
label,
alignment_map.get(
get_alignment(object, name, i), wx.LIST_FORMAT_LEFT
),
)
self._set_column_widths()
def _rebuild_all(self):
"""Rebuilds the structure of the list control, then refreshes its
contents.
"""
self._rebuild()
self.update_editor()
def _set_column_widths(self):
"""Set the column widths for the current set of columns."""
control = self.control
if control is None:
return
object, name = self.object, self.name
dx, dy = control.GetClientSize()
if is_mac:
dx -= scrollbar_dx
n = control.GetColumnCount()
get_width = self.adapter.get_width
pdx = 0
wdx = 0.0
widths = []
cached = self._cached_widths
current = [control.GetColumnWidth(i) for i in range(n)]
if (cached is None) or (len(cached) != n):
self._cached_widths = cached = [None] * n
for i in range(n):
cw = cached[i]
if (cw is None) or (-cw == current[i]):
width = float(get_width(object, name, i))
if width <= 0.0:
width = 0.1
if width <= 1.0:
wdx += width
cached[i] = -1
else:
width = int(width)
pdx += width
if cw is None:
cached[i] = width
else:
cached[i] = width = current[i]
pdx += width
widths.append(width)
adx = max(0, dx - pdx)
control.Freeze()
for i in range(n):
width = cached[i]
if width < 0:
width = widths[i]
if width <= 1.0:
widths[i] = w = max(30, int(round((adx * width) / wdx)))
wdx -= width
width = w
adx -= width
cached[i] = -w
control.SetColumnWidth(i, width)
control.Thaw()
def _add_image(self, image_resource):
"""Adds a new image to the wx.ImageList and its associated mapping."""
bitmap = image_resource.create_image().ConvertToBitmap()
image_list = self._image_list
if image_list is None:
self._image_list = image_list = wx.ImageList(
bitmap.GetWidth(), bitmap.GetHeight()
)
self.control.AssignImageList(image_list, wx.IMAGE_LIST_SMALL)
self.image_resources[image_resource] = self.images[
image_resource.name
] = row = image_list.Add(bitmap)
return row
def _get_image(self, image):
"""Converts a user specified image to a wx.ListCtrl image index."""
if isinstance(image, str):
self.image = image
image = self.image
if isinstance(image, ImageResource):
result = self.image_resources.get(image)
if result is not None:
return result
return self._add_image(image)
return self.images.get(image)
def _get_selected(self):
"""Returns a list of the rows of all currently selected list items."""
selected = []
item = -1
control = self.control
# Handle case where the list is cleared
if len(self.value) == 0:
return selected
while True:
item = control.GetNextItem(
item, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED
)
if item == -1:
break
selected.append(item)
return selected
def _append_new(self):
"""Append a new item to the end of the list control."""
if "append" in self.factory.operations:
adapter = self.adapter
self.row = self.control.GetItemCount()
self.edit = True
adapter.insert(
self.object,
self.name,
self.row,
adapter.get_default_value(self.object, self.name),
)
def _insert_current(self):
"""Inserts a new item after the currently selected list control item."""
if "insert" in self.factory.operations:
selected = self._get_selected()
if len(selected) == 1:
adapter = self.adapter
adapter.insert(
self.object,
self.name,
selected[0],
adapter.get_default_value(self.object, self.name),
)
self.row = selected[0]
self.edit = True
def _delete_current(self):
"""Deletes the currently selected items from the list control."""
if "delete" in self.factory.operations:
selected = self._get_selected()
if len(selected) == 0:
return
delete = self.adapter.delete
selected.reverse()
for row in selected:
delete(self.object, self.name, row)
n = self.adapter.len(self.object, self.name)
if not self.factory.multi_select:
self.selected_row = self.row = n - 1 if row >= n else row
else:
# FIXME: What should the selection be?
self.multi_selected = []
self.multi_selected_rows = []
def _move_up_current(self):
"""Moves the currently selected item up one line in the list control."""
if "move" in self.factory.operations:
selected = self._get_selected()
if len(selected) == 1:
row = selected[0]
if row > 0:
adapter = self.adapter
object, name = self.object, self.name
item = adapter.get_item(object, name, row)
adapter.delete(object, name, row)
adapter.insert(object, name, row - 1, item)
self.row = row - 1
def _move_down_current(self):
"""Moves the currently selected item down one line in the list control."""
if "move" in self.factory.operations:
selected = self._get_selected()
if len(selected) == 1:
row = selected[0]
if row < (self.control.GetItemCount() - 1):
adapter = self.adapter
object, name = self.object, self.name
item = adapter.get_item(object, name, row)
adapter.delete(object, name, row)
adapter.insert(object, name, row + 1, item)
self.row = row + 1
def _edit_current(self):
"""Allows the user to edit the current item in the list control."""
if "edit" in self.factory.operations and self.factory.editable_labels:
selected = self._get_selected()
if len(selected) == 1:
self.control.EditLabel(selected[0])
def _get_column(self, x, translate=False):
"""Returns the column index corresponding to a specified x position."""
if x >= 0:
control = self.control
for i in range(control.GetColumnCount()):
x -= control.GetColumnWidth(i)
if x < 0:
if translate:
return self.adapter.get_column(
self.object, self.name, i
)
return i
return None
def _mouse_click(self, event, trait):
"""Generate a TabularEditorEvent event for a specified mouse event and
editor trait name.
"""
x = event.GetX()
row, flags = self.control.HitTest(wx.Point(x, event.GetY()))
if row == wx.NOT_FOUND:
if self.factory.multi_select:
self.multi_selected = []
self.multi_selected_rows = []
else:
self.selected = None
self.selected_row = -1
else:
if self.factory.multi_select and event.ShiftDown():
# Handle shift-click multi-selections because the wx.ListCtrl
# does not (by design, apparently).
# We must append this to the event queue because the
# multi-selection will not be recorded until this event handler
# finishes and lets the widget actually handle the event.
do_later(self._item_selected, None)
setattr(
self,
trait,
TabularEditorEvent(
editor=self,
row=row,
column=self._get_column(x, translate=True),
),
)
# wx should continue with additional event handlers. Skip(False)
# actually means to skip looking, skip(True) means to keep looking.
# This seems backwards to me...
event.Skip(True)
class TabularEditorEvent(HasStrictTraits):
#: The index of the row:
row = Int()
#: The id of the column (either a string or an integer):
column = Any()
#: The row item:
item = Property()
# -- Private Traits -------------------------------------------------------
#: The editor the event is associated with:
editor = Instance(TabularEditor)
# -- Property Implementations ---------------------------------------------
def _get_item(self):
editor = self.editor
return editor.adapter.get_item(editor.object, editor.name, self.row)