|
| 1 | +from qtpy import QtWidgets, QtCore |
| 2 | + |
| 3 | +import puzzlepiece as pzp |
| 4 | + |
| 5 | + |
| 6 | +class DataGrid(QtWidgets.QWidget): |
| 7 | + """ |
| 8 | + A table containing multiple :class:`~puzzlepiece.extras.datagrid.Row` objects, each acting kind of like |
| 9 | + a :class:`~puzzlepiece.piece.Piece` object, in that it has params and actions |
| 10 | + (see :class:`~puzzlepiece.param.BaseParam` and :class:`~puzzlepiece.action.Action`). |
| 11 | +
|
| 12 | + This is a QWidget, co it can be added to your Piece's :func:`~puzzlepiece.piece.Piece.custom_layout` |
| 13 | + or used as a standalone Widget if you know what you're doing. |
| 14 | +
|
| 15 | + **This is not very performant, very large numbers of Rows should be avoided!** Consider using |
| 16 | + Qt's QTableWidget instead. |
| 17 | +
|
| 18 | + :param row_class: The :class:`~puzzlepiece.extras.datagrid.Row` class that will be used to construct Rows. |
| 19 | + :param puzzle: (optional) The parent :class:`~puzzlepiece.puzzle.Puzzle`. |
| 20 | + """ |
| 21 | + |
| 22 | + #: A Qt signal emitted when a row is added or removed. |
| 23 | + rows_changed = QtCore.Signal() |
| 24 | + #: A Qt signal emitted when any data in the DataGrid changes (including when rows are added/removed). |
| 25 | + data_changed = QtCore.Signal() |
| 26 | + |
| 27 | + def __init__(self, row_class, puzzle=None): |
| 28 | + super().__init__() |
| 29 | + #: Reference to the parent :class:`~puzzlepiece.puzzle.Puzzle`. |
| 30 | + self.puzzle = puzzle or pzp.puzzle.PretendPuzzle() |
| 31 | + self._row_class = row_class |
| 32 | + row_example = row_class(self.puzzle) |
| 33 | + self.param_names = row_example.params.keys() |
| 34 | + |
| 35 | + self._tree = QtWidgets.QTreeWidget() |
| 36 | + self._tree.setHeaderLabels(("ID", *row_example.params.keys(), "actions")) |
| 37 | + |
| 38 | + layout = QtWidgets.QVBoxLayout() |
| 39 | + layout.addWidget(self._tree) |
| 40 | + self.setLayout(layout) |
| 41 | + |
| 42 | + #: A list of Rows. |
| 43 | + self.rows = [] |
| 44 | + self._items = [] |
| 45 | + self._root = self._tree.invisibleRootItem() |
| 46 | + self._slots = {} |
| 47 | + self.rows_changed.connect(self.data_changed.emit) |
| 48 | + |
| 49 | + @property |
| 50 | + def values(self): |
| 51 | + """ |
| 52 | + The current values for all params in the Rows (this does not invoke their getters). |
| 53 | + """ |
| 54 | + return [{key: x[key].value for key in x.params} for x in self.rows] |
| 55 | + |
| 56 | + def add_row(self, **kwargs): |
| 57 | + """ |
| 58 | + Add a Row with default param values. |
| 59 | +
|
| 60 | + :param kwargs: keyword arguments matching param names can be passed |
| 61 | + to set param values in the new row |
| 62 | + """ |
| 63 | + item = QtWidgets.QTreeWidgetItem(self._tree, (str(len(self.rows)),)) |
| 64 | + row = self._row_class(self, self.puzzle) |
| 65 | + row._populate_item(self._tree, item) |
| 66 | + self.rows.append(row) |
| 67 | + self._items.append(item) |
| 68 | + for key in kwargs: |
| 69 | + row.params[key].set_value(kwargs[key]) |
| 70 | + for param_name in self.param_names: |
| 71 | + if param_name in self._slots: |
| 72 | + for slot in self._slots[param_name]: |
| 73 | + row.params[param_name].changed.connect(slot) |
| 74 | + row.params[param_name].changed.connect(self.data_changed.emit) |
| 75 | + self.rows_changed.emit() |
| 76 | + return row |
| 77 | + |
| 78 | + def remove_row(self, id): |
| 79 | + """ |
| 80 | + Remove the row with the given id. |
| 81 | +
|
| 82 | + :param id: id of the row to remove. |
| 83 | + """ |
| 84 | + self._root.removeChild(self._items[id]) |
| 85 | + del self._items[id] |
| 86 | + del self.rows[id] |
| 87 | + for i in range(len(self.rows)): |
| 88 | + self._items[i].setText(0, str(i)) |
| 89 | + self.rows_changed.emit() |
| 90 | + |
| 91 | + def get_index(self, row): |
| 92 | + """ |
| 93 | + Get the current index of a given :class:`~puzzlepiece.extras.datagrid.Row` object. |
| 94 | +
|
| 95 | + :param row: the row object. |
| 96 | + :rtype: int |
| 97 | + """ |
| 98 | + return self.rows.index(row) |
| 99 | + |
| 100 | + def clear(self): |
| 101 | + """ |
| 102 | + Remove all rows. |
| 103 | + """ |
| 104 | + self._tree.clear() |
| 105 | + self.rows = [] |
| 106 | + self._items = [] |
| 107 | + self.rows_changed.emit() |
| 108 | + |
| 109 | + def add_changed_slot(self, param_name, function): |
| 110 | + """ |
| 111 | + Connect a Slot (usually a method) to the :attr:`~puzzlepiece.param.BaseParam.changed` |
| 112 | + Signal of the given param in all the rows (including Rows added in the future). |
| 113 | +
|
| 114 | + :param param_name: The name of the param whose changed Signal we're connecting to. |
| 115 | + :param function: any method or other Qt Slot to connect. |
| 116 | + """ |
| 117 | + if param_name in self._slots: |
| 118 | + self._slots[param_name].append(function) |
| 119 | + else: |
| 120 | + self._slots[param_name] = [function] |
| 121 | + for row in self.rows: |
| 122 | + row.params[param_name].changed.connect(function) |
| 123 | + |
| 124 | + |
| 125 | +class Row: |
| 126 | + """ |
| 127 | + A Row is a template for a :class:`~puzzlepiece.extras.datagrid.DataGrid` object, holding the data (params) |
| 128 | + and actions that the DataGrid displays. |
| 129 | +
|
| 130 | + It acts kind of like a :class:`~puzzlepiece.piece.Piece` object, in that it has params and actions |
| 131 | + (see :class:`~puzzlepiece.param.BaseParam` and :class:`~puzzlepiece.action.Action`). |
| 132 | +
|
| 133 | + :param parent: (optional) The parent DataGrid. |
| 134 | + :parem puzzle: (optional) The parent Puzzle. |
| 135 | + """ |
| 136 | + |
| 137 | + def __init__(self, parent=None, puzzle=None): |
| 138 | + self.puzzle = puzzle or pzp.puzzle.PretendPuzzle() |
| 139 | + self.parent = parent |
| 140 | + #: dict: A dictionary of this Row's params (see :class:`~puzzlepiece.param.BaseParam`). You can also directly index the Row object with the param name. |
| 141 | + self.params = {} |
| 142 | + #: dict: A dictionary of this Row's actions (see :class:`~puzzlepiece.action.Action`). |
| 143 | + self.actions = {} |
| 144 | + self.define_params() |
| 145 | + self.define_actions() |
| 146 | + for key in self.params: |
| 147 | + self.params[key]._main_layout.removeWidget(self.params[key].label) |
| 148 | + |
| 149 | + def define_params(self): |
| 150 | + """ |
| 151 | + Override to define params using decorators from :mod:`puzzlepiece.param`. |
| 152 | + """ |
| 153 | + pass |
| 154 | + |
| 155 | + def define_actions(self): |
| 156 | + """ |
| 157 | + Override to define actions using decorators from :mod:`puzzlepiece.action`. |
| 158 | + """ |
| 159 | + pass |
| 160 | + |
| 161 | + def elevate(self): |
| 162 | + """ |
| 163 | + For compatibility with the Piece's API. |
| 164 | +
|
| 165 | + :meta private: |
| 166 | + """ |
| 167 | + pass |
| 168 | + |
| 169 | + def _populate_item(self, tree, item): |
| 170 | + for i, key in enumerate(self.params): |
| 171 | + tree.setItemWidget(item, i + 1, self.params[key]) |
| 172 | + tree.setItemWidget(item, i + 2, self._action_buttons()) |
| 173 | + |
| 174 | + def _action_buttons(self): |
| 175 | + widget = QtWidgets.QWidget() |
| 176 | + layout = QtWidgets.QHBoxLayout() |
| 177 | + widget.setLayout(layout) |
| 178 | + |
| 179 | + visible_actions = [key for key in self.actions if self.actions[key].visible] |
| 180 | + for i, key in enumerate(visible_actions): |
| 181 | + button = QtWidgets.QPushButton(key) |
| 182 | + button.clicked.connect(lambda x=False, _key=key: self.actions[_key]()) |
| 183 | + layout.addWidget(button) |
| 184 | + return widget |
| 185 | + |
| 186 | + def __iter__(self): |
| 187 | + for key in self.params: |
| 188 | + yield key |
| 189 | + |
| 190 | + def __getitem__(self, key): |
| 191 | + return self.params[key] |
| 192 | + |
| 193 | + def __contains__(self, item): |
| 194 | + return item in self.params |
0 commit comments