File: i_data_view_widget.py

package info (click to toggle)
python-pyface 8.0.0-5
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 13,944 kB
  • sloc: python: 54,107; makefile: 82
file content (371 lines) | stat: -rw-r--r-- 13,031 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# (C) Copyright 2005-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!

from contextlib import contextmanager
import logging

from traits.api import (
    Bool, Enum, HasTraits, Instance, List, Property,
    TraitError, Tuple, cached_property,
)

from pyface.data_view.abstract_data_model import AbstractDataModel
from pyface.data_view.abstract_data_exporter import AbstractDataExporter
from pyface.i_drop_handler import IDropHandler
from pyface.i_layout_widget import ILayoutWidget


logger = logging.getLogger(__name__)


class IDataViewWidget(ILayoutWidget):
    """ Interface for data view widgets. """

    #: The data model for the data view.
    data_model = Instance(AbstractDataModel, allow_none=False)

    #: Whether or not the column headers are visible.
    header_visible = Bool(True)

    #: The global drop handlers for the data view.  These are intended to
    #: handle drop actions which either affect the whole data view, or where
    #: the data handler can work out how to change the underlying data without
    #: additional input.
    drop_handlers = List(Instance(IDropHandler, allow_none=False))

    #: What can be selected.  Implementations may optionally allow "column"
    #: and "item" selection types.
    selection_type = Enum("row",)

    #: How selections are modified.  Implementations may optionally allow
    #: "none" for no selection, or possibly other multiple selection modes
    #: as supported by the toolkit.
    selection_mode = Enum("extended", "single")

    #: The selected indices in the view.
    selection = List(Tuple)

    #: Exporters available for the DataViewWidget.
    exporters = List(Instance(AbstractDataExporter))


class MDataViewWidget(HasTraits):
    """ Mixin class for data view widgets. """

    # IDataViewWidget Interface traits --------------------------------------

    #: The data model for the data view.
    data_model = Instance(AbstractDataModel, allow_none=False)

    #: Whether or not the column headers are visible.
    header_visible = Bool(True)

    #: The global drop handlers for the data view.  These are intended to
    #: handle drop actions which either affect the whole data view, or where
    #: the data handler can work out how to change the underlying data without
    #: additional input.
    drop_handlers = List(Instance(IDropHandler, allow_none=False))

    #: The selected indices in the view.  This should never be mutated, any
    #: changes should be by replacement of the entire list.
    selection = Property(observe='_selection.items')

    #: Exporters available for the DataViewWidget.
    exporters = List(Instance(AbstractDataExporter))

    # Private traits --------------------------------------------------------

    #: Whether the selection is currently being updated.
    _selection_updating_flag = Bool()

    #: The selected indices in the view.  This should never be mutated, any
    #: changes should be by replacement of the entire list.
    _selection = List(Tuple)

    # ------------------------------------------------------------------------
    # MDataViewWidget Interface
    # ------------------------------------------------------------------------

    def _header_visible_updated(self, event):
        """ Observer for header_visible trait. """
        if self.control is not None:
            self._set_control_header_visible(event.new)

    def _get_control_header_visible(self):
        """ Toolkit specific method to get the visibility of the header.

        Returns
        -------
        control_header_visible : bool
            Whether or not the control's header is visible.
        """
        raise NotImplementedError()

    def _set_control_header_visible(self, control_header_visible):
        """ Toolkit specific method to set the visibility of the header.

        Parameters
        ----------
        control_header_visible : bool
            Whether or not the control's header is visible.
        """
        raise NotImplementedError()

    def _selection_type_updated(self, event):
        """ Observer for selection_type trait. """
        if self.control is not None:
            self._set_control_selection_type(event.new)
            self.selection = []

    def _get_control_selection_type(self):
        """ Toolkit specific method to get the selection type.

        Returns
        -------
        selection_type : str
            The type of selection the control is using.
        """
        raise NotImplementedError()

    def _set_control_selection_type(self, selection_type):
        """ Toolkit specific method to change the selection type.

        Parameters
        ----------
        selection_type : str
            The type of selection the control is using.
        """
        raise NotImplementedError()

    def _selection_mode_updated(self, event):
        """ Observer for selection_mode trait. """
        if self.control is not None:
            self._set_control_selection_mode(event.new)
            self.selection = []

    def _get_control_selection_mode(self):
        """ Toolkit specific method to get the selection mode.

        Returns
        -------
        selection_mode : str
            The selection mode the control is using (eg. single vs. extended
            selection).
        """
        raise NotImplementedError()

    def _set_control_selection_mode(self, selection_mode):
        """ Toolkit specific method to change the selection mode.

        Parameters
        ----------
        selection_mode : str
            The selection mode the control is using (eg. single vs. extended
            selection).
        """
        raise NotImplementedError()

    def _selection_updated(self, event):
        """ Observer for selection trait. """
        if self.control is not None and not self._selection_updating_flag:
            with self._selection_updating():
                self._set_control_selection(self.selection)

    def _get_control_selection(self):
        """ Toolkit specific method to get the selection.

        Returns
        -------
        selection : list of pairs of row and column indices
            The selected elements of the control.
        """
        raise NotImplementedError()

    def _set_control_selection(self, selection):
        """ Toolkit specific method to change the selection.

        Parameters
        ----------
        selection : list of pairs of row and column indices
            The selected elements of the control.
        """
        raise NotImplementedError()

    def _observe_control_selection(self, remove=False):
        """ Toolkit specific method to watch for changes in the selection.

        The _update_selection method is available as a toolkit-independent
        callback when the selection changes, but particular toolkits may
        choose to implement their own callbacks with similar functionality
        if appropriate.
        """
        raise NotImplementedError()

    def _update_selection(self, *args, **kwargs):
        """ Handle a toolkit even that  changes the selection.

        This is designed to be usable as a callback for a toolkit event
        or signal handler, so it accepts any arguments.
        """
        if not self._selection_updating_flag:
            with self._selection_updating():
                self._selection = self._get_control_selection()

    # ------------------------------------------------------------------------
    # Widget Interface
    # ------------------------------------------------------------------------

    def create(self, parent=None):
        """ Creates the toolkit specific control.

        This method should create the control and assign it to the
        :py:attr:``control`` trait.
        """
        super().create(parent=parent)

        self.show(self.visible)
        self.enable(self.enabled)

    def _initialize_control(self):
        """ Initializes the toolkit specific control.
        """
        logger.debug('Initializing DataViewWidget')
        super()._initialize_control()
        self._set_control_header_visible(self.header_visible)
        self._set_control_selection_mode(self.selection_mode)
        self._set_control_selection_type(self.selection_type)
        self._set_control_selection(self.selection)

    def _add_event_listeners(self):
        logger.debug('Adding DataViewWidget listeners')
        super()._add_event_listeners()
        self.observe(
            self._header_visible_updated,
            'header_visible',
            dispatch='ui',
        )
        self.observe(
            self._selection_type_updated,
            'selection_type',
            dispatch='ui',
        )
        self.observe(
            self._selection_mode_updated,
            'selection_mode',
            dispatch='ui',
        )
        self.observe(
            self._selection_updated,
            '_selection.items',
            dispatch='ui',
        )
        if self.control is not None:
            self._observe_control_selection()

    def _remove_event_listeners(self):
        logger.debug('Removing DataViewWidget listeners')
        if self.control is not None:
            self._observe_control_selection(remove=True)
        self.observe(
            self._header_visible_updated,
            'header_visible',
            dispatch='ui',
            remove=True,
        )
        self.observe(
            self._selection_type_updated,
            'selection_type',
            dispatch='ui',
            remove=True,
        )
        self.observe(
            self._selection_mode_updated,
            'selection_mode',
            dispatch='ui',
            remove=True,
        )
        self.observe(
            self._selection_updated,
            '_selection.items',
            dispatch='ui',
            remove=True,
        )
        super()._remove_event_listeners()

    # ------------------------------------------------------------------------
    # Private methods
    # ------------------------------------------------------------------------

    @contextmanager
    def _selection_updating(self):
        """ Context manager to prevent loopback when updating selections. """
        if self._selection_updating_flag:
            yield
        else:
            self._selection_updating_flag = True
            try:
                yield
            finally:
                self._selection_updating_flag = False

    # Trait property handlers

    @cached_property
    def _get_selection(self):
        return self._selection

    def _set_selection(self, selection):
        if self.selection_mode == 'none' and len(selection) != 0:
            raise TraitError(
                "Selection must be empty when selection_mode is 'none', "
                "got {!r}".format(selection)
            )
        elif self.selection_mode == 'single' and len(selection) > 1:
            raise TraitError(
                "Selection must have at most one element when selection_mode "
                "is 'single', got {!r}".format(selection)
            )

        if self.selection_type == 'row':
            for row, column in selection:
                if column != ():
                    raise TraitError(
                        "Column values must be () when selection_type is "
                        "'row', got {!r}".format(column)
                    )
                if not self.data_model.is_row_valid(row):
                    raise TraitError(
                        "Invalid row index {!r}".format(row)
                    )
        elif self.selection_type == 'column':
            for row, column in selection:
                if not (self.data_model.is_row_valid(row)
                        and self.data_model.can_have_children(row)
                        and self.data_model.get_row_count(row) > 0):
                    raise TraitError(
                        "Row values must have children when selection_type "
                        "is 'column', got {!r}".format(column)
                    )
                if not self.data_model.is_column_valid(column):
                    raise TraitError(
                        "Invalid column index {!r}".format(column)
                    )
        else:
            for row, column in selection:
                if not self.data_model.is_row_valid(row):
                    raise TraitError(
                        "Invalid row index {!r}".format(row)
                    )
                if not self.data_model.is_column_valid(column):
                    raise TraitError(
                        "Invalid column index {!r}".format(column)
                    )

        self._selection = selection