diff --git a/examples/data_view/column_example.py b/examples/data_view/column_example.py index 2646d4ede..455079032 100644 --- a/examples/data_view/column_example.py +++ b/examples/data_view/column_example.py @@ -14,12 +14,14 @@ from functools import partial from random import choice, randint -from traits.api import Dict, HasStrictTraits, Instance, Int, Str, List +from traits.api import Bool, Dict, HasStrictTraits, Instance, Int, Str, List from pyface.api import ApplicationWindow, GUI, Image, ImageResource from pyface.data_view.i_data_view_widget import IDataViewWidget from pyface.data_view.data_view_widget import DataViewWidget -from pyface.data_view.value_types.api import IntValue, TextValue, no_value +from pyface.data_view.value_types.api import ( + BoolValue, IntValue, TextValue, no_value +) from column_data_model import ( AbstractRowInfo, ColumnDataModel, HasTraitsRowInfo @@ -46,6 +48,8 @@ class Person(HasStrictTraits): age = Int + contacted = Bool + address = Instance(Address) @@ -72,6 +76,11 @@ def get_image(self, model, row, column): value="age", value_type=IntValue(minimum=0), ), + HasTraitsRowInfo( + title="Contacted", + value="contacted", + value_type=BoolValue(true_text="Yes", false_text="No"), + ), HasTraitsRowInfo( title="Address", value_type=no_value, diff --git a/pyface/data_view/abstract_value_type.py b/pyface/data_view/abstract_value_type.py index ebaee33df..9a2696641 100644 --- a/pyface/data_view/abstract_value_type.py +++ b/pyface/data_view/abstract_value_type.py @@ -19,11 +19,20 @@ and how to actually display it. """ +from enum import IntEnum + from traits.api import ABCHasStrictTraits, Event, observe from .data_view_errors import DataViewSetError +class CheckState(IntEnum): + "Possible checkbox states" + # XXX in the future this may need a "partial" state, see Pyface #695 + UNCHECKED = 0 + CHECKED = 1 + + class AbstractValueType(ABCHasStrictTraits): """ A value type converts raw data into data channels. @@ -226,6 +235,76 @@ def get_image(self, model, row, column): from pyface.image_resource import ImageResource return ImageResource("image_not_found") + def has_check_state(self, model, row, column): + """ Whether or not the value has checked state. + + The default implementation returns False. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + + Returns + ------- + has_check_state : bool + Whether or not the value has a checked state. + """ + return False + + def get_check_state(self, model, row, column): + """ The state of the item check box. + + The default implementation returns "checked" if the value is + truthy, or "unchecked" if the value is falsey. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + + Returns + ------- + check_state : CheckState + The current checked state. + """ + return ( + CheckState.CHECKED + if model.get_value(row, column) + else CheckState.UNCHECKED + ) + + def set_check_state(self, model, row, column, check_state): + """ Set the checked state of the underlying value. + + The default implementation does not allow setting the checked state. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + check_state : CheckState + The check state value to set. + + Raises + ------- + DataViewSetError + If the value cannot be set. + """ + raise DataViewSetError("Cannot set check state.") + def has_tooltip(self, model, row, column): """ Whether or not the value has a tooltip. diff --git a/pyface/data_view/tests/test_abstract_value_type.py b/pyface/data_view/tests/test_abstract_value_type.py index 4119f45ec..df6ec6a9e 100644 --- a/pyface/data_view/tests/test_abstract_value_type.py +++ b/pyface/data_view/tests/test_abstract_value_type.py @@ -15,7 +15,7 @@ from traits.testing.unittest_tools import UnittestTools from pyface.data_view.data_view_errors import DataViewSetError -from pyface.data_view.abstract_value_type import AbstractValueType +from pyface.data_view.abstract_value_type import AbstractValueType, CheckState class ValueType(AbstractValueType): @@ -85,6 +85,21 @@ def test_get_image(self): result = value_type.get_image(self.model, [0], [0]) self.assertEqual(result.name, "image_not_found") + def test_has_check_state(self): + value_type = ValueType() + result = value_type.has_check_state(self.model, [0], [0]) + self.assertFalse(result) + + def test_get_check_state(self): + value_type = ValueType() + result = value_type.get_check_state(self.model, [0], [0]) + self.assertEqual(result, CheckState.CHECKED) + + def test_set_check_state(self): + value_type = ValueType() + with self.assertRaises(DataViewSetError): + value_type.set_check_state(self.model, [0], [0], CheckState.CHECKED) + def test_parameter_update(self): value_type = ValueType() with self.assertTraitChanges(value_type, 'updated', count=1): diff --git a/pyface/data_view/value_types/api.py b/pyface/data_view/value_types/api.py index 1e42d4e49..5921c3da1 100644 --- a/pyface/data_view/value_types/api.py +++ b/pyface/data_view/value_types/api.py @@ -8,6 +8,7 @@ # # Thanks for using Enthought open source! +from .bool_value import BoolValue # noqa: F401 from .constant_value import ConstantValue # noqa: F401 from .editable_value import EditableValue # noqa: F401 from .no_value import NoValue, no_value # noqa: F401 diff --git a/pyface/data_view/value_types/bool_value.py b/pyface/data_view/value_types/bool_value.py new file mode 100644 index 000000000..da414b08a --- /dev/null +++ b/pyface/data_view/value_types/bool_value.py @@ -0,0 +1,108 @@ +# (C) Copyright 2005-2020 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 traits.api import Str + +from pyface.data_view.abstract_value_type import AbstractValueType, CheckState + + +class BoolValue(AbstractValueType): + """ Value that presents a boolean value via checked state. + """ + + #: The text to display next to a True value. + true_text = Str() + + #: The text to display next to a False value. + false_text = Str() + + def has_editor_value(self, model, row, column): + """ BoolValues don't use editors, but have always-on checkbox. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + + Returns + ------- + has_editor_value : bool + Whether or not the value is editable. + """ + return False + + def get_text(self, model, row, column): + """ The textual representation of the underlying value. + + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + + Returns + ------- + text : str + The textual representation of the underlying value. + """ + return ( + self.true_text if model.get_value(row, column) else self.false_text + ) + + def has_check_state(self, model, row, column): + """ Whether or not the value has checked state. + + The default implementation returns True. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being queried. + column : sequence of int + The column in the data model being queried. + + Returns + ------- + has_check_state : bool + Whether or not the value has a checked state. + """ + return True + + def set_check_state(self, model, row, column, check_state): + """ Set the boolean value from the check state. + + Parameters + ---------- + model : AbstractDataModel + The data model holding the data. + row : sequence of int + The row in the data model being set. + column : sequence of int + The column in the data model being set. + check_state : "checked" or "unchecked" + The check state being set. + + Raises + ------- + DataViewSetError + If the value cannot be set. + """ + value = (check_state == CheckState.CHECKED) + model.set_value(row, column, value) diff --git a/pyface/data_view/value_types/tests/test_bool_value.py b/pyface/data_view/value_types/tests/test_bool_value.py new file mode 100644 index 000000000..2c42ce334 --- /dev/null +++ b/pyface/data_view/value_types/tests/test_bool_value.py @@ -0,0 +1,85 @@ +# (C) Copyright 2005-2020 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 unittest import TestCase +from unittest.mock import Mock + +from pyface.data_view.abstract_value_type import CheckState +from pyface.data_view.data_view_errors import DataViewSetError +from pyface.data_view.value_types.bool_value import BoolValue + + +class TestBoolValue(TestCase): + + def setUp(self): + self.model = Mock() + self.model.get_value = Mock(return_value=True) + self.model.can_set_value = Mock(return_value=True) + self.model.set_value = Mock() + + def test_defaults(self): + value = BoolValue() + self.assertEqual(value.true_text, "") + self.assertEqual(value.false_text, "") + + def test_has_text_default(self): + value = BoolValue() + has_text = value.has_text(self.model, [0], [0]) + self.assertFalse(has_text) + + def test_has_text(self): + value = BoolValue(true_text="Yes", false_text="No") + has_text = value.has_text(self.model, [0], [0]) + self.assertTrue(has_text) + + def test_get_text_default(self): + value = BoolValue() + text = value.get_text(self.model, [0], [0]) + self.assertEqual(text, "") + + self.model.get_value = Mock(return_value=False) + text = value.get_text(self.model, [0], [0]) + self.assertEqual(text, "") + + def test_get_text(self): + value = BoolValue(true_text="Yes", false_text="No") + text = value.get_text(self.model, [0], [0]) + self.assertEqual(text, "Yes") + + self.model.get_value = Mock(return_value=False) + text = value.get_text(self.model, [0], [0]) + self.assertEqual(text, "No") + + def test_get_check_state(self): + value = BoolValue() + check_state = value.get_check_state(self.model, [0], [0]) + self.assertEqual(check_state, CheckState.CHECKED) + + def test_get_check_state_false(self): + value = BoolValue() + self.model.get_value = Mock(return_value=False) + check_state = value.get_check_state(self.model, [0], [0]) + self.assertEqual(check_state, CheckState.UNCHECKED) + + def test_set_check_state(self): + value = BoolValue() + value.set_check_state(self.model, [0], [0], CheckState.CHECKED) + self.model.set_value.assert_called_once_with([0], [0], True) + + def test_set_check_state_unchecked(self): + value = BoolValue() + value.set_check_state(self.model, [0], [0], CheckState.UNCHECKED) + self.model.set_value.assert_called_once_with([0], [0], False) + + def test_set_check_state_no_set_value(self): + self.model.can_set_value = Mock(return_value=False) + value = BoolValue() + with self.assertRaises(DataViewSetError): + value.set_text(self.model, [0], [0], CheckState.CHECKED) diff --git a/pyface/ui/qt4/data_view/data_view_item_model.py b/pyface/ui/qt4/data_view/data_view_item_model.py index 5f7dcda50..8a2a304d9 100644 --- a/pyface/ui/qt4/data_view/data_view_item_model.py +++ b/pyface/ui/qt4/data_view/data_view_item_model.py @@ -14,6 +14,7 @@ from pyface.qt import is_qt5 from pyface.qt.QtCore import QAbstractItemModel, QModelIndex, Qt from pyface.data_view.abstract_data_model import AbstractDataModel +from pyface.data_view.abstract_value_type import CheckState from pyface.data_view.data_view_errors import ( DataViewGetError, DataViewSetError ) @@ -24,6 +25,15 @@ # XXX This file is scaffolding and may need to be rewritten +set_check_state_map = { + Qt.Checked: CheckState.CHECKED, + Qt.Unchecked: CheckState.UNCHECKED, +} +get_check_state_map = { + CheckState.CHECKED: Qt.Checked, + CheckState.UNCHECKED: Qt.Unchecked, +} + class DataViewItemModel(QAbstractItemModel): """ A QAbstractItemModel that understands AbstractDataModels. """ @@ -133,8 +143,14 @@ def flags(self, index): flags |= Qt.ItemNeverHasChildren try: - if value_type and value_type.has_editor_value(self.model, row, column): - flags |= Qt.ItemIsEditable + if value_type: + if value_type.has_editor_value(self.model, row, column): + flags |= Qt.ItemIsEditable + if ( + value_type.has_check_state(self.model, row, column) + and self.model.can_set_value(row, column) + ): + flags |= Qt.ItemIsUserCheckable except DataViewGetError: # expected error, ignore pass @@ -168,6 +184,10 @@ def data(self, index, role=Qt.DisplayRole): image = value_type.get_image(self.model, row, column) if isinstance(image, IImageResource): return image.create_image() + elif role == Qt.CheckStateRole: + if value_type.has_check_state(self.model, row, column): + value = value_type.get_check_state(self.model, row, column) + return get_check_state_map[value] elif role == Qt.ToolTipRole: if value_type.has_tooltip(self.model, row, column): return value_type.get_tooltip(self.model, row, column) @@ -199,6 +219,11 @@ def setData(self, index, value, role=Qt.EditRole): elif role == Qt.DisplayRole: if value_type.has_text(self.model, row, column): value_type.set_text(self.model, row, column, value) + elif role == Qt.CheckStateRole: + if value_type.has_check_state(self.model, row, column): + state = set_check_state_map[value] + value_type.set_check_state(self.model, row, column, state) + except DataViewSetError: return False except Exception: