1
|
|
# ------------------------------------------------------------------------------
|
2
|
|
# Copyright (c) 2008, Riverbank Computing Limited
|
3
|
|
# All rights reserved.
|
4
|
|
#
|
5
|
|
# This software is provided without warranty under the terms of the BSD license.
|
6
|
|
# However, when used with the GPL version of PyQt the additional terms described
|
7
|
|
# in the PyQt GPL exception also apply.
|
8
|
|
#
|
9
|
|
# Author: Riverbank Computing Limited
|
10
|
|
# ------------------------------------------------------------------------------
|
11
|
|
|
12
|
8
|
""" Defines the table model used by the table editor.
|
13
|
|
"""
|
14
|
|
|
15
|
|
|
16
|
|
|
17
|
8
|
import logging
|
18
|
|
|
19
|
8
|
from pyface.qt import QtCore, QtGui
|
20
|
|
|
21
|
8
|
from traitsui.ui_traits import SequenceTypes
|
22
|
|
|
23
|
8
|
from .clipboard import PyMimeData
|
24
|
|
|
25
|
|
|
26
|
|
|
27
|
|
# set up logging for the module
|
28
|
8
|
logger = logging.getLogger(__name__)
|
29
|
|
|
30
|
|
|
31
|
|
# Mapping for trait alignment values to qt4 horizontal alignment constants
|
32
|
8
|
h_alignment_map = {
|
33
|
|
"left": QtCore.Qt.AlignLeft,
|
34
|
|
"center": QtCore.Qt.AlignHCenter,
|
35
|
|
"right": QtCore.Qt.AlignRight,
|
36
|
|
}
|
37
|
|
|
38
|
|
# Mapping for trait alignment values to qt4 vertical alignment constants
|
39
|
8
|
v_alignment_map = {
|
40
|
|
"top": QtCore.Qt.AlignTop,
|
41
|
|
"center": QtCore.Qt.AlignVCenter,
|
42
|
|
"bottom": QtCore.Qt.AlignBottom,
|
43
|
|
}
|
44
|
|
|
45
|
|
# MIME type for internal table drag/drop operations
|
46
|
8
|
mime_type = "traits-ui-table-editor"
|
47
|
|
|
48
|
|
|
49
|
8
|
def as_qcolor(color):
|
50
|
|
""" Convert a color specification (maybe a tuple) into a QColor.
|
51
|
|
"""
|
52
|
11
|
if isinstance(color, SequenceTypes):
|
53
|
8
|
return QtGui.QColor(*color)
|
54
|
|
else:
|
55
|
8
|
return QtGui.QColor(color)
|
56
|
|
|
57
|
|
|
58
|
8
|
class TableModel(QtCore.QAbstractTableModel):
|
59
|
|
"""The model for table data."""
|
60
|
|
|
61
|
8
|
def __init__(self, editor, parent=None):
|
62
|
|
"""Initialise the object."""
|
63
|
|
|
64
|
8
|
QtCore.QAbstractTableModel.__init__(self, parent)
|
65
|
|
|
66
|
8
|
self._editor = editor
|
67
|
|
|
68
|
|
# -------------------------------------------------------------------------
|
69
|
|
# QAbstractTableModel interface:
|
70
|
|
# -------------------------------------------------------------------------
|
71
|
|
|
72
|
8
|
def rowCount(self, mi):
|
73
|
|
"""Reimplemented to return the number of rows."""
|
74
|
|
|
75
|
8
|
return len(self._editor.items())
|
76
|
|
|
77
|
8
|
def columnCount(self, mi):
|
78
|
|
"""Reimplemented to return the number of columns."""
|
79
|
|
|
80
|
8
|
return len(self._editor.columns)
|
81
|
|
|
82
|
8
|
def data(self, mi, role):
|
83
|
|
"""Reimplemented to return the data."""
|
84
|
|
|
85
|
8
|
obj = self._editor.items()[mi.row()]
|
86
|
8
|
column = self._editor.columns[mi.column()]
|
87
|
|
|
88
|
11
|
if self._editor.factory is None:
|
89
|
|
# XXX This should never happen, but it does,
|
90
|
|
# probably during shutdown, but I haven't investigated
|
91
|
8
|
return None
|
92
|
|
|
93
|
11
|
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
|
94
|
8
|
text = column.get_value(obj)
|
95
|
11
|
if text is not None:
|
96
|
8
|
return text
|
97
|
|
|
98
|
11
|
elif role == QtCore.Qt.DecorationRole:
|
99
|
8
|
image = self._editor._get_image(column.get_image(obj))
|
100
|
11
|
if image is not None:
|
101
|
0
|
return image
|
102
|
|
|
103
|
11
|
elif role == QtCore.Qt.ToolTipRole:
|
104
|
0
|
tooltip = column.get_tooltip(obj)
|
105
|
11
|
if tooltip:
|
106
|
0
|
return tooltip
|
107
|
|
|
108
|
11
|
elif role == QtCore.Qt.FontRole:
|
109
|
8
|
font = column.get_text_font(obj)
|
110
|
11
|
if font is None:
|
111
|
8
|
font = self._editor.factory.cell_font
|
112
|
11
|
if font is not None:
|
113
|
8
|
return QtGui.QFont(font)
|
114
|
|
|
115
|
11
|
elif role == QtCore.Qt.TextAlignmentRole:
|
116
|
8
|
string = column.get_horizontal_alignment(obj)
|
117
|
8
|
h_alignment = h_alignment_map.get(string, QtCore.Qt.AlignLeft)
|
118
|
8
|
string = column.get_vertical_alignment(obj)
|
119
|
8
|
v_alignment = v_alignment_map.get(string, QtCore.Qt.AlignVCenter)
|
120
|
8
|
return int(h_alignment | v_alignment)
|
121
|
|
|
122
|
11
|
elif role == QtCore.Qt.BackgroundRole:
|
123
|
8
|
color = column.get_cell_color(obj)
|
124
|
11
|
if color is None:
|
125
|
11
|
if column.is_editable(obj):
|
126
|
0
|
color = self._editor.factory.cell_bg_color_
|
127
|
|
else:
|
128
|
0
|
color = self._editor.factory.cell_read_only_bg_color_
|
129
|
11
|
if color is None:
|
130
|
|
# FIXME: Yes, this is weird. It should work fine to fall through
|
131
|
|
# to the catch-all None at the end, but it doesn't.
|
132
|
0
|
return None
|
133
|
8
|
q_color = as_qcolor(color)
|
134
|
8
|
return QtGui.QBrush(q_color)
|
135
|
|
|
136
|
11
|
elif role == QtCore.Qt.ForegroundRole:
|
137
|
8
|
color = column.get_text_color(obj)
|
138
|
11
|
if color is None:
|
139
|
0
|
color = self._editor.factory.cell_color_
|
140
|
11
|
if color is not None:
|
141
|
8
|
q_color = as_qcolor(color)
|
142
|
8
|
return QtGui.QBrush(q_color)
|
143
|
|
|
144
|
11
|
elif role == QtCore.Qt.UserRole:
|
145
|
8
|
return obj
|
146
|
|
|
147
|
11
|
elif role == QtCore.Qt.CheckStateRole:
|
148
|
11
|
if column.get_type(obj) == "bool" and column.show_checkbox:
|
149
|
11
|
if column.get_raw_value(obj):
|
150
|
0
|
return QtCore.Qt.Checked
|
151
|
|
else:
|
152
|
0
|
return QtCore.Qt.Unchecked
|
153
|
|
|
154
|
8
|
return None
|
155
|
|
|
156
|
8
|
def flags(self, mi):
|
157
|
|
"""Reimplemented to set editable and movable status."""
|
158
|
|
|
159
|
8
|
editor = self._editor
|
160
|
|
|
161
|
11
|
if not mi.isValid():
|
162
|
11
|
if editor.factory.reorderable:
|
163
|
0
|
return QtCore.Qt.ItemIsDropEnabled
|
164
|
|
else:
|
165
|
0
|
return QtCore.Qt.NoItemFlags
|
166
|
|
|
167
|
8
|
flags = (
|
168
|
|
QtCore.Qt.ItemIsSelectable
|
169
|
|
| QtCore.Qt.ItemIsEnabled
|
170
|
|
| QtCore.Qt.ItemIsDragEnabled
|
171
|
|
)
|
172
|
|
|
173
|
8
|
obj = editor.items()[mi.row()]
|
174
|
8
|
column = editor.columns[mi.column()]
|
175
|
|
|
176
|
11
|
if editor.factory:
|
177
|
11
|
if editor.factory.editable and column.is_editable(obj):
|
178
|
8
|
flags |= QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsDropEnabled
|
179
|
|
|
180
|
11
|
if editor.factory.reorderable:
|
181
|
0
|
flags |= QtCore.Qt.ItemIsDropEnabled
|
182
|
|
|
183
|
11
|
if column.get_type(obj) == "bool" and column.show_checkbox:
|
184
|
0
|
flags |= QtCore.Qt.ItemIsUserCheckable
|
185
|
|
|
186
|
8
|
return flags
|
187
|
|
|
188
|
8
|
def headerData(self, section, orientation, role):
|
189
|
|
"""Reimplemented to return the header data."""
|
190
|
|
|
191
|
8
|
editor = self._editor
|
192
|
|
|
193
|
11
|
if orientation == QtCore.Qt.Horizontal:
|
194
|
8
|
column = editor.columns[section]
|
195
|
|
|
196
|
11
|
if role == QtCore.Qt.DisplayRole:
|
197
|
8
|
return column.get_label()
|
198
|
|
|
199
|
11
|
elif orientation == QtCore.Qt.Vertical:
|
200
|
|
|
201
|
11
|
if role == QtCore.Qt.DisplayRole:
|
202
|
8
|
return str(section + 1)
|
203
|
|
|
204
|
11
|
if editor.factory is None:
|
205
|
|
# XXX This should never happen, but it does,
|
206
|
|
# probably during shutdown, but I haven't investigated
|
207
|
8
|
return None
|
208
|
|
|
209
|
11
|
if role == QtCore.Qt.FontRole:
|
210
|
8
|
font = editor.factory.label_font
|
211
|
11
|
if font is not None:
|
212
|
8
|
return QtGui.QFont(font)
|
213
|
|
|
214
|
11
|
elif role == QtCore.Qt.BackgroundRole:
|
215
|
8
|
color = editor.factory.label_bg_color_
|
216
|
11
|
if color is not None:
|
217
|
0
|
return color
|
218
|
|
|
219
|
11
|
elif role == QtCore.Qt.ForegroundRole:
|
220
|
8
|
color = editor.factory.label_color_
|
221
|
11
|
if color is not None:
|
222
|
0
|
return color
|
223
|
|
|
224
|
8
|
return None
|
225
|
|
|
226
|
8
|
def insertRow(self, row, parent=QtCore.QModelIndex(), obj=None):
|
227
|
|
"""Reimplemented to allow creation of new rows. Added an optional
|
228
|
|
arg to allow the insertion of an existing row object."""
|
229
|
|
|
230
|
0
|
editor = self._editor
|
231
|
11
|
if obj is None:
|
232
|
0
|
obj = editor.create_new_row()
|
233
|
|
|
234
|
0
|
self.beginInsertRows(parent, row, row)
|
235
|
0
|
editor.callx(editor.items().insert, row, obj)
|
236
|
0
|
self.endInsertRows()
|
237
|
0
|
return True
|
238
|
|
|
239
|
8
|
def insertRows(self, row, count, parent=QtCore.QModelIndex()):
|
240
|
|
"""Reimplemented to allow creation of new rows."""
|
241
|
|
|
242
|
0
|
editor = self._editor
|
243
|
0
|
items = editor.items()
|
244
|
0
|
self.beginInsertRows(parent, row, row + count - 1)
|
245
|
11
|
for i in range(count):
|
246
|
0
|
editor.callx(items.insert, row + i, editor.create_new_row())
|
247
|
0
|
self.endInsertRows()
|
248
|
0
|
return True
|
249
|
|
|
250
|
8
|
def removeRows(self, row, count, parent=QtCore.QModelIndex()):
|
251
|
|
"""Reimplemented to allow row deletion, as well as reordering via drag
|
252
|
|
and drop."""
|
253
|
|
|
254
|
0
|
editor = self._editor
|
255
|
0
|
items = editor.items()
|
256
|
0
|
self.beginRemoveRows(parent, row, row + count - 1)
|
257
|
11
|
for i in range(count):
|
258
|
0
|
editor.callx(items.pop, row + i)
|
259
|
0
|
self.endRemoveRows()
|
260
|
0
|
return True
|
261
|
|
|
262
|
8
|
def mimeTypes(self):
|
263
|
|
"""Reimplemented to expose our internal MIME type for drag and drop
|
264
|
|
operations."""
|
265
|
|
|
266
|
0
|
return [mime_type, PyMimeData.MIME_TYPE, PyMimeData.NOPICKLE_MIME_TYPE]
|
267
|
|
|
268
|
8
|
def mimeData(self, indexes):
|
269
|
|
"""Reimplemented to generate MIME data containing the rows of the
|
270
|
|
current selection."""
|
271
|
|
|
272
|
0
|
editor = self._editor
|
273
|
0
|
selection_mode = editor.factory.selection_mode
|
274
|
|
|
275
|
11
|
if selection_mode.startswith("cell"):
|
276
|
11
|
data = [
|
277
|
|
self._get_cell_drag_value(index.row(), index.column())
|
278
|
|
for index in indexes
|
279
|
|
]
|
280
|
11
|
elif selection_mode.startswith("column"):
|
281
|
11
|
columns = sorted(set(index.column() for index in indexes))
|
282
|
0
|
data = self._get_columns_drag_value(columns)
|
283
|
|
else:
|
284
|
11
|
rows = sorted(set(index.row() for index in indexes))
|
285
|
0
|
data = self._get_rows_drag_value(rows)
|
286
|
|
|
287
|
0
|
mime_data = PyMimeData.coerce(data)
|
288
|
|
|
289
|
|
# handle re-ordering via internal drags
|
290
|
11
|
if editor.factory.reorderable:
|
291
|
11
|
rows = sorted({index.row() for index in indexes})
|
292
|
0
|
data = QtCore.QByteArray(str(id(self)).encode("utf8"))
|
293
|
11
|
for row in rows:
|
294
|
0
|
data.append((" %i" % row).encode("utf8"))
|
295
|
0
|
mime_data.setData(mime_type, data)
|
296
|
0
|
return mime_data
|
297
|
|
|
298
|
8
|
def dropMimeData(self, mime_data, action, row, column, parent):
|
299
|
|
"""Reimplemented to allow items to be moved."""
|
300
|
|
|
301
|
11
|
if action == QtCore.Qt.IgnoreAction:
|
302
|
0
|
return False
|
303
|
|
|
304
|
|
# this is a drag from a table model?
|
305
|
0
|
data = mime_data.data(mime_type)
|
306
|
11
|
if not data.isNull() and action == QtCore.Qt.MoveAction:
|
307
|
11
|
id_and_rows = [
|
308
|
|
int(s) for s in data.data().decode("utf8").split(" ")
|
309
|
|
]
|
310
|
0
|
table_id = id_and_rows[0]
|
311
|
|
# is it from ourself?
|
312
|
11
|
if table_id == id(self):
|
313
|
0
|
current_rows = id_and_rows[1:]
|
314
|
11
|
if not parent.isValid():
|
315
|
0
|
row = len(self._editor.items()) - 1
|
316
|
|
else:
|
317
|
0
|
row = parent.row()
|
318
|
|
|
319
|
0
|
self.moveRows(current_rows, row)
|
320
|
0
|
return True
|
321
|
|
|
322
|
0
|
data = PyMimeData.coerce(mime_data).instance()
|
323
|
11
|
if data is not None:
|
324
|
0
|
editor = self._editor
|
325
|
|
|
326
|
11
|
if row == -1 and column == -1 and parent.isValid():
|
327
|
0
|
row = parent.row()
|
328
|
0
|
column = parent.column()
|
329
|
|
|
330
|
11
|
if row != -1 and column != -1:
|
331
|
0
|
object = editor.items()[row]
|
332
|
0
|
column = editor.columns[column]
|
333
|
11
|
if column.is_droppable(object, data):
|
334
|
0
|
column.set_value(object, data)
|
335
|
0
|
return True
|
336
|
|
|
337
|
0
|
return False
|
338
|
|
|
339
|
8
|
def supportedDropActions(self):
|
340
|
|
"""Reimplemented to allow items to be moved."""
|
341
|
|
|
342
|
0
|
return QtCore.Qt.MoveAction
|
343
|
|
|
344
|
|
# -------------------------------------------------------------------------
|
345
|
|
# Utility methods
|
346
|
|
# -------------------------------------------------------------------------
|
347
|
|
|
348
|
8
|
def _get_columns_drag_value(self, columns):
|
349
|
|
""" Returns the value to use when the specified columns are dragged or
|
350
|
|
copied and pasted. The parameter *cols* is a list of column indexes.
|
351
|
|
"""
|
352
|
11
|
return [self._get_column_data(column) for column in columns]
|
353
|
|
|
354
|
8
|
def _get_column_data(self, column):
|
355
|
|
""" Return the model data for the column as a list """
|
356
|
0
|
editor = self._editor
|
357
|
0
|
column_obj = editor.columns[column]
|
358
|
11
|
return [column_obj.get_value(item) for item in editor.items()]
|
359
|
|
|
360
|
8
|
def _get_rows_drag_value(self, rows):
|
361
|
|
""" Returns the value to use when the specified rows are dragged or
|
362
|
|
copied and pasted. The parameter *rows* is a list of row indexes.
|
363
|
|
Return a list of objects.
|
364
|
|
"""
|
365
|
0
|
items = self._editor.items()
|
366
|
11
|
return [items[row] for row in rows]
|
367
|
|
|
368
|
8
|
def _get_cell_drag_value(self, row, column):
|
369
|
|
""" Returns the value to use when the specified cell is dragged or
|
370
|
|
copied and pasted.
|
371
|
|
"""
|
372
|
0
|
editor = self._editor
|
373
|
0
|
item = editor.items()[row]
|
374
|
0
|
drag_value = editor.columns[column].get_drag_value(item)
|
375
|
0
|
return drag_value
|
376
|
|
|
377
|
|
# -------------------------------------------------------------------------
|
378
|
|
# TableModel interface:
|
379
|
|
# -------------------------------------------------------------------------
|
380
|
|
|
381
|
8
|
def moveRow(self, old_row, new_row):
|
382
|
|
"""Convenience method to move a single row."""
|
383
|
|
|
384
|
0
|
return self.moveRows([old_row], new_row)
|
385
|
|
|
386
|
8
|
def moveRows(self, current_rows, new_row):
|
387
|
|
"""Moves a sequence of rows (provided as a list of row indexes) to a new
|
388
|
|
row."""
|
389
|
|
|
390
|
|
# Sort rows in descending order so they can be removed without
|
391
|
|
# invalidating the indices.
|
392
|
0
|
current_rows.sort()
|
393
|
0
|
current_rows.reverse()
|
394
|
|
|
395
|
|
# If the the highest selected row is lower than the destination, do an
|
396
|
|
# insertion before rather than after the destination.
|
397
|
11
|
if current_rows[-1] < new_row:
|
398
|
0
|
new_row += 1
|
399
|
|
|
400
|
|
# Remove selected rows...
|
401
|
0
|
items = self._editor.items()
|
402
|
0
|
objects = []
|
403
|
11
|
for row in current_rows:
|
404
|
11
|
if row <= new_row:
|
405
|
0
|
new_row -= 1
|
406
|
0
|
objects.insert(0, items[row])
|
407
|
0
|
self.removeRow(row)
|
408
|
|
|
409
|
|
# ...and add them at the new location.
|
410
|
11
|
for i, obj in enumerate(objects):
|
411
|
0
|
self.insertRow(new_row + i, obj=obj)
|
412
|
|
|
413
|
|
# Update the selection for the new location.
|
414
|
0
|
self._editor.set_selection(objects)
|
415
|
|
|
416
|
|
|
417
|
8
|
class SortFilterTableModel(QtGui.QSortFilterProxyModel):
|
418
|
|
"""A wrapper for the TableModel which provides sorting and filtering
|
419
|
|
capability."""
|
420
|
|
|
421
|
8
|
def __init__(self, editor, parent=None):
|
422
|
|
"""Initialise the object."""
|
423
|
|
|
424
|
8
|
QtGui.QSortFilterProxyModel.__init__(self, parent)
|
425
|
|
|
426
|
8
|
self._editor = editor
|
427
|
|
|
428
|
|
# -------------------------------------------------------------------------
|
429
|
|
# QSortFilterProxyModel interface:
|
430
|
|
# -------------------------------------------------------------------------
|
431
|
|
|
432
|
8
|
def filterAcceptsRow(self, source_row, source_parent):
|
433
|
|
""""Reimplemented to use a TableFilter for filtering rows."""
|
434
|
|
|
435
|
11
|
if self._editor._filtered_cache is None:
|
436
|
8
|
return True
|
437
|
|
else:
|
438
|
8
|
return self._editor._filtered_cache[source_row]
|
439
|
|
|
440
|
8
|
def filterAcceptsColumn(self, source_column, source_parent):
|
441
|
|
"""Reimplemented to save time, because we always return True."""
|
442
|
|
|
443
|
8
|
return True
|
444
|
|
|
445
|
8
|
def lessThan(self, left_mi, right_mi):
|
446
|
|
"""Reimplemented to sort according to the 'key' method defined for
|
447
|
|
TableColumn."""
|
448
|
8
|
try:
|
449
|
8
|
editor = self._editor
|
450
|
8
|
column = editor.columns[left_mi.column()]
|
451
|
8
|
items = editor.items()
|
452
|
8
|
left, right = items[left_mi.row()], items[right_mi.row()]
|
453
|
|
|
454
|
8
|
return column.key(left) < column.key(right)
|
455
|
0
|
except Exception as exc:
|
456
|
0
|
logger.exception(exc)
|
457
|
|
# This will almost certainly segfault, but there does not seem to
|
458
|
|
# be anything sensible we can do.
|
459
|
0
|
raise
|
460
|
|
|
461
|
|
# -------------------------------------------------------------------------
|
462
|
|
# SortFilterTableModel interface:
|
463
|
|
# -------------------------------------------------------------------------
|
464
|
|
|
465
|
8
|
def moveRow(self, old_row, new_row):
|
466
|
|
"""Convenience method to move a single row."""
|
467
|
|
|
468
|
0
|
return self.moveRows([old_row], new_row)
|
469
|
|
|
470
|
8
|
def moveRows(self, current_rows, new_row):
|
471
|
|
"""Delegate to source model with mapped rows."""
|
472
|
|
|
473
|
0
|
source = self.sourceModel()
|
474
|
11
|
current_rows = [
|
475
|
|
self.mapToSource(self.index(row, 0)).row() for row in current_rows
|
476
|
|
]
|
477
|
0
|
new_row = self.mapToSource(self.index(new_row, 0)).row()
|
478
|
0
|
source.moveRows(current_rows, new_row)
|