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
|