Repository URL to install this package:
Version:
7.2.1 ▾
|
# (C) Copyright 2009-2021 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!
# ------------------------------------------------------------------------------
# Copyright (c) 2008, Riverbank Computing Limited
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD license.
# However, when used with the GPL version of PyQt the additional terms described
# in the PyQt GPL exception also apply.
#
# Author: Riverbank Computing Limited
# ------------------------------------------------------------------------------
""" Defines the table model used by the table editor.
"""
import logging
from pyface.qt import QtCore, QtGui
from traitsui.ui_traits import SequenceTypes
from .clipboard import PyMimeData
# set up logging for the module
logger = logging.getLogger(__name__)
# Mapping for trait alignment values to qt4 horizontal alignment constants
h_alignment_map = {
"left": QtCore.Qt.AlignLeft,
"center": QtCore.Qt.AlignHCenter,
"right": QtCore.Qt.AlignRight,
}
# Mapping for trait alignment values to qt4 vertical alignment constants
v_alignment_map = {
"top": QtCore.Qt.AlignTop,
"center": QtCore.Qt.AlignVCenter,
"bottom": QtCore.Qt.AlignBottom,
}
# MIME type for internal table drag/drop operations
mime_type = "traits-ui-table-editor"
def as_qcolor(color):
""" Convert a color specification (maybe a tuple) into a QColor.
"""
if isinstance(color, SequenceTypes):
return QtGui.QColor(*color)
else:
return QtGui.QColor(color)
class TableModel(QtCore.QAbstractTableModel):
"""The model for table data."""
def __init__(self, editor, parent=None):
"""Initialise the object."""
QtCore.QAbstractTableModel.__init__(self, parent)
self._editor = editor
# -------------------------------------------------------------------------
# QAbstractTableModel interface:
# -------------------------------------------------------------------------
def rowCount(self, mi):
"""Reimplemented to return the number of rows."""
return len(self._editor.items())
def columnCount(self, mi):
"""Reimplemented to return the number of columns."""
return len(self._editor.columns)
def data(self, mi, role):
"""Reimplemented to return the data."""
obj = self._editor.items()[mi.row()]
column = self._editor.columns[mi.column()]
if self._editor.factory is None:
# XXX This should never happen, but it does,
# probably during shutdown, but I haven't investigated
return None
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
text = column.get_value(obj)
if text is not None:
return text
elif role == QtCore.Qt.DecorationRole:
image = self._editor._get_image(column.get_image(obj))
if image is not None:
return image
elif role == QtCore.Qt.ToolTipRole:
tooltip = column.get_tooltip(obj)
if tooltip:
return tooltip
elif role == QtCore.Qt.FontRole:
font = column.get_text_font(obj)
if font is None:
font = self._editor.factory.cell_font
if font is not None:
return QtGui.QFont(font)
elif role == QtCore.Qt.TextAlignmentRole:
string = column.get_horizontal_alignment(obj)
h_alignment = h_alignment_map.get(string, QtCore.Qt.AlignLeft)
string = column.get_vertical_alignment(obj)
v_alignment = v_alignment_map.get(string, QtCore.Qt.AlignVCenter)
return int(h_alignment | v_alignment)
elif role == QtCore.Qt.BackgroundRole:
color = column.get_cell_color(obj)
if color is None:
if column.is_editable(obj):
color = self._editor.factory.cell_bg_color_
else:
color = self._editor.factory.cell_read_only_bg_color_
if color is None:
# FIXME: Yes, this is weird. It should work fine to fall through
# to the catch-all None at the end, but it doesn't.
return None
q_color = as_qcolor(color)
return QtGui.QBrush(q_color)
elif role == QtCore.Qt.ForegroundRole:
color = column.get_text_color(obj)
if color is None:
color = self._editor.factory.cell_color_
if color is not None:
q_color = as_qcolor(color)
return QtGui.QBrush(q_color)
elif role == QtCore.Qt.UserRole:
return obj
elif role == QtCore.Qt.CheckStateRole:
if column.get_type(obj) == "bool" and column.show_checkbox:
if column.get_raw_value(obj):
return QtCore.Qt.Checked
else:
return QtCore.Qt.Unchecked
return None
def flags(self, mi):
"""Reimplemented to set editable and movable status."""
editor = self._editor
if not mi.isValid():
if editor.factory.reorderable:
return QtCore.Qt.ItemIsDropEnabled
else:
return QtCore.Qt.NoItemFlags
flags = (
QtCore.Qt.ItemIsSelectable
| QtCore.Qt.ItemIsEnabled
| QtCore.Qt.ItemIsDragEnabled
)
obj = editor.items()[mi.row()]
column = editor.columns[mi.column()]
if editor.factory:
if editor.factory.editable and column.is_editable(obj):
flags |= QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsDropEnabled
if editor.factory.reorderable:
flags |= QtCore.Qt.ItemIsDropEnabled
if column.get_type(obj) == "bool" and column.show_checkbox:
flags |= QtCore.Qt.ItemIsUserCheckable
return flags
def headerData(self, section, orientation, role):
"""Reimplemented to return the header data."""
editor = self._editor
if orientation == QtCore.Qt.Horizontal:
column = editor.columns[section]
if role == QtCore.Qt.DisplayRole:
return column.get_label()
elif orientation == QtCore.Qt.Vertical:
if role == QtCore.Qt.DisplayRole:
return str(section + 1)
if editor.factory is None:
# XXX This should never happen, but it does,
# probably during shutdown, but I haven't investigated
return None
if role == QtCore.Qt.FontRole:
font = editor.factory.label_font
if font is not None:
return QtGui.QFont(font)
elif role == QtCore.Qt.BackgroundRole:
color = editor.factory.label_bg_color_
if color is not None:
return color
elif role == QtCore.Qt.ForegroundRole:
color = editor.factory.label_color_
if color is not None:
return color
return None
def insertRow(self, row, parent=QtCore.QModelIndex(), obj=None):
"""Reimplemented to allow creation of new rows. Added an optional
arg to allow the insertion of an existing row object."""
editor = self._editor
if obj is None:
obj = editor.create_new_row()
self.beginInsertRows(parent, row, row)
editor.callx(editor.items().insert, row, obj)
self.endInsertRows()
return True
def insertRows(self, row, count, parent=QtCore.QModelIndex()):
"""Reimplemented to allow creation of new rows."""
editor = self._editor
items = editor.items()
self.beginInsertRows(parent, row, row + count - 1)
for i in range(count):
editor.callx(items.insert, row + i, editor.create_new_row())
self.endInsertRows()
return True
def removeRows(self, row, count, parent=QtCore.QModelIndex()):
"""Reimplemented to allow row deletion, as well as reordering via drag
and drop."""
editor = self._editor
items = editor.items()
self.beginRemoveRows(parent, row, row + count - 1)
for i in range(count):
editor.callx(items.pop, row + i)
self.endRemoveRows()
return True
def mimeTypes(self):
"""Reimplemented to expose our internal MIME type for drag and drop
operations."""
return [mime_type, PyMimeData.MIME_TYPE, PyMimeData.NOPICKLE_MIME_TYPE]
def mimeData(self, indexes):
"""Reimplemented to generate MIME data containing the rows of the
current selection."""
editor = self._editor
selection_mode = editor.factory.selection_mode
if selection_mode.startswith("cell"):
data = [
self._get_cell_drag_value(index.row(), index.column())
for index in indexes
]
elif selection_mode.startswith("column"):
columns = sorted(set(index.column() for index in indexes))
data = self._get_columns_drag_value(columns)
else:
rows = sorted(set(index.row() for index in indexes))
data = self._get_rows_drag_value(rows)
mime_data = PyMimeData.coerce(data)
# handle re-ordering via internal drags
if editor.factory.reorderable:
rows = sorted({index.row() for index in indexes})
data = QtCore.QByteArray(str(id(self)).encode("utf8"))
for row in rows:
data.append((" %i" % row).encode("utf8"))
mime_data.setData(mime_type, data)
return mime_data
def dropMimeData(self, mime_data, action, row, column, parent):
"""Reimplemented to allow items to be moved."""
if action == QtCore.Qt.IgnoreAction:
return False
# this is a drag from a table model?
data = mime_data.data(mime_type)
if not data.isNull() and action == QtCore.Qt.MoveAction:
id_and_rows = [
int(s) for s in data.data().decode("utf8").split(" ")
]
table_id = id_and_rows[0]
# is it from ourself?
if table_id == id(self):
current_rows = id_and_rows[1:]
if not parent.isValid():
row = len(self._editor.items()) - 1
else:
row = parent.row()
self.moveRows(current_rows, row)
return True
data = PyMimeData.coerce(mime_data).instance()
if data is not None:
editor = self._editor
if row == -1 and column == -1 and parent.isValid():
row = parent.row()
column = parent.column()
if row != -1 and column != -1:
object = editor.items()[row]
column = editor.columns[column]
if column.is_droppable(object, data):
column.set_value(object, data)
return True
return False
def supportedDropActions(self):
"""Reimplemented to allow items to be moved."""
return QtCore.Qt.MoveAction
# -------------------------------------------------------------------------
# Utility methods
# -------------------------------------------------------------------------
def _get_columns_drag_value(self, columns):
""" Returns the value to use when the specified columns are dragged or
copied and pasted. The parameter *cols* is a list of column indexes.
"""
return [self._get_column_data(column) for column in columns]
def _get_column_data(self, column):
""" Return the model data for the column as a list """
editor = self._editor
column_obj = editor.columns[column]
return [column_obj.get_value(item) for item in editor.items()]
def _get_rows_drag_value(self, rows):
""" Returns the value to use when the specified rows are dragged or
copied and pasted. The parameter *rows* is a list of row indexes.
Return a list of objects.
"""
items = self._editor.items()
return [items[row] for row in rows]
def _get_cell_drag_value(self, row, column):
""" Returns the value to use when the specified cell is dragged or
copied and pasted.
"""
editor = self._editor
item = editor.items()[row]
drag_value = editor.columns[column].get_drag_value(item)
return drag_value
# -------------------------------------------------------------------------
# TableModel interface:
# -------------------------------------------------------------------------
def moveRow(self, old_row, new_row):
"""Convenience method to move a single row."""
return self.moveRows([old_row], new_row)
def moveRows(self, current_rows, new_row):
"""Moves a sequence of rows (provided as a list of row indexes) to a new
row."""
# Sort rows in descending order so they can be removed without
# invalidating the indices.
current_rows.sort()
current_rows.reverse()
# If the the highest selected row is lower than the destination, do an
# insertion before rather than after the destination.
if current_rows[-1] < new_row:
new_row += 1
# Remove selected rows...
items = self._editor.items()
objects = []
for row in current_rows:
if row <= new_row:
new_row -= 1
objects.insert(0, items[row])
self.removeRow(row)
# ...and add them at the new location.
for i, obj in enumerate(objects):
self.insertRow(new_row + i, obj=obj)
# Update the selection for the new location.
self._editor.set_selection(objects)
class SortFilterTableModel(QtGui.QSortFilterProxyModel):
"""A wrapper for the TableModel which provides sorting and filtering
capability."""
def __init__(self, editor, parent=None):
"""Initialise the object."""
QtGui.QSortFilterProxyModel.__init__(self, parent)
self._editor = editor
# -------------------------------------------------------------------------
# QSortFilterProxyModel interface:
# -------------------------------------------------------------------------
def filterAcceptsRow(self, source_row, source_parent):
""""Reimplemented to use a TableFilter for filtering rows."""
if self._editor._filtered_cache is None:
return True
else:
return self._editor._filtered_cache[source_row]
def filterAcceptsColumn(self, source_column, source_parent):
"""Reimplemented to save time, because we always return True."""
return True
def lessThan(self, left_mi, right_mi):
"""Reimplemented to sort according to the 'key' method defined for
TableColumn."""
try:
editor = self._editor
column = editor.columns[left_mi.column()]
items = editor.items()
left, right = items[left_mi.row()], items[right_mi.row()]
return column.key(left) < column.key(right)
except Exception as exc:
logger.exception(exc)
# This will almost certainly segfault, but there does not seem to
# be anything sensible we can do.
raise
# -------------------------------------------------------------------------
# SortFilterTableModel interface:
# -------------------------------------------------------------------------
def moveRow(self, old_row, new_row):
"""Convenience method to move a single row."""
return self.moveRows([old_row], new_row)
def moveRows(self, current_rows, new_row):
"""Delegate to source model with mapped rows."""
source = self.sourceModel()
current_rows = [
self.mapToSource(self.index(row, 0)).row() for row in current_rows
]
new_row = self.mapToSource(self.index(new_row, 0)).row()
source.moveRows(current_rows, new_row)