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)

Read our documentation on viewing source code .

Loading