diff --git a/echo/qt/autoconnect.py b/echo/qt/autoconnect.py index e9005c00..d1fb7891 100644 --- a/echo/qt/autoconnect.py +++ b/echo/qt/autoconnect.py @@ -6,7 +6,8 @@ connect_text, connect_button, connect_combo_selection, - connect_list_selection) + connect_list_selection, + connect_datetime) __all__ = ['autoconnect_callbacks_to_qt'] @@ -20,6 +21,7 @@ HANDLERS['button'] = connect_button HANDLERS['combosel'] = connect_combo_selection HANDLERS['listsel'] = connect_list_selection +HANDLERS['datetime'] = connect_datetime def autoconnect_callbacks_to_qt(instance, widget, connect_kwargs={}): @@ -64,6 +66,10 @@ def autoconnect_callbacks_to_qt(instance, widget, connect_kwargs={}): * ``combotext``: the callback property is linked to a QComboBox based on the label of the entries in the combo box. + * ``datetime``: the callback property is linked to a QDateTimeEdit. + Note that this connection will also work for the more specific QDateEdit + and QTimeEdit widgets, as they are subclasses of QDateTimeEdit. + Applications can also define additional mappings between type and auto-linking. To do this, simply add a new entry to the ``HANDLERS`` object:: diff --git a/echo/qt/connect.py b/echo/qt/connect.py index 51c97ad5..aa804c2a 100644 --- a/echo/qt/connect.py +++ b/echo/qt/connect.py @@ -1,10 +1,11 @@ # The functions in this module are used to connect callback properties to Qt # widgets. +from datetime import datetime import math from qtpy import QtGui, QtWidgets -from qtpy.QtCore import Qt +from qtpy.QtCore import Qt, QDateTime import numpy as np @@ -13,7 +14,7 @@ __all__ = ['connect_checkable_button', 'connect_text', 'connect_combo_data', 'connect_combo_text', 'connect_float_text', 'connect_value', - 'connect_combo_selection', 'connect_list_selection', + 'connect_combo_selection', 'connect_list_selection', 'connect_datetime', 'BaseConnection'] @@ -580,3 +581,51 @@ def connect(self): def disconnect(self): remove_callback(self._instance, self._prop, self.update_widget) self._widget.itemSelectionChanged.disconnect(self.update_prop) + + +class connect_datetime(BaseConnection): + """ + Connect a CallbackProperty to a QDateTimeEdit. + Since QDateEdit and QTimeEdit are subclasses of QDateTimeEdit, this connection + will work for those more specific widgets as well. + """ + + def __init__(self, instance, prop, widget): + super(connect_datetime, self).__init__(instance, prop, widget) + self.connect() + + def update_prop(self): + qdatetime = self._widget.dateTime().toUTC() + value = np.datetime64(qdatetime.toPython()) + setattr(self._instance, self._prop, value) + + def update_widget(self, value): + if value is None: + value = np.datetime64('now') + dt = value.item() + + # datetime64::item can return a date + # If this happens, we use midnight as our time + # (datetime is a subclass of date so we need to check this way) + if not isinstance(dt, datetime): + date = dt + time = datetime.min.time() + else: + date = dt.date() + time = dt.time() + + qdatetime = QDateTime(date, time, Qt.TimeSpec.UTC).toTimeSpec(self._widget.timeSpec()) + self._widget.setDateTime(qdatetime) + + def connect(self): + add_callback(self._instance, self._prop, self.update_widget) + self._widget.dateTimeChanged.connect(self.update_prop) + self._widget.dateChanged.connect(self.update_prop) + self._widget.timeChanged.connect(self.update_prop) + self.update_widget(getattr(self._instance, self._prop)) + + def disconnect(self): + remove_callback(self._instance, self._prop, self.update_widget) + self._widget.dateTimeChanged.disconnect(self.update_prop) + self._widget.dateChanged.disconnect(self.update_prop) + self._widget.timeChanged.disconnect(self.update_prop) diff --git a/echo/qt/tests/test_autoconnect.py b/echo/qt/tests/test_autoconnect.py index ed2beaf0..69177766 100644 --- a/echo/qt/tests/test_autoconnect.py +++ b/echo/qt/tests/test_autoconnect.py @@ -1,4 +1,8 @@ +from datetime import datetime + +from numpy import datetime64 from qtpy import QtWidgets, QtGui +from qtpy.QtCore import QDateTime, Qt from echo.qt.autoconnect import autoconnect_callbacks_to_qt from echo import CallbackProperty @@ -47,6 +51,10 @@ def __init__(self, parent=None): self.bool_log.setCheckable(True) self.layout.addWidget(self.bool_log) + self.datetime_dob = QtWidgets.QDateTimeEdit(objectName="datetime_dob") + self.datetime_dob.setTimeSpec(Qt.TimeSpec.UTC) + self.layout.addWidget(self.datetime_dob) + class Person(object): planet = CallbackProperty() dataset = CallbackProperty() @@ -54,6 +62,7 @@ class Person(object): age = CallbackProperty() height = CallbackProperty() log = CallbackProperty() + dob = CallbackProperty() widget = CustomWidget() @@ -86,6 +95,11 @@ class Person(object): widget.bool_log.setChecked(True) assert person.log + dob = datetime(2000, 1, 1, 11, 52, 6) + qdob = QDateTime(dob.date(), dob.time(), Qt.TimeSpec.UTC) + widget.datetime_dob.setDateTime(qdob) + assert person.dob.item() == dob + # Check that modifying the callback properties updates the Qt widget person.planet = 'mars' @@ -106,6 +120,11 @@ class Person(object): person.log = False assert not widget.bool_log.isChecked() + dob = datetime(2010, 2, 3, 16, 22, 11) + person.dob = datetime64(dob) + qdatetime = widget.datetime_dob.dateTime().toUTC() + assert qdatetime.toPython() == person.dob.item() + def test_autoconnect_with_empty_qt_item(): diff --git a/echo/qt/tests/test_connect.py b/echo/qt/tests/test_connect.py index df3e6398..d1b0f919 100644 --- a/echo/qt/tests/test_connect.py +++ b/echo/qt/tests/test_connect.py @@ -1,10 +1,14 @@ +from datetime import datetime + import pytest +from numpy import datetime64 from unittest.mock import MagicMock from qtpy import QtWidgets +from qtpy.QtCore import QDateTime, Qt from echo import CallbackProperty -from echo.qt.connect import (connect_checkable_button, connect_text, +from echo.qt.connect import (connect_checkable_button, connect_datetime, connect_text, connect_combo_data, connect_combo_text, connect_float_text, connect_value, connect_button, UserDataWrapper) @@ -211,3 +215,26 @@ class Example(object): assert e.a.call_count == 0 button.clicked.emit() assert e.a.call_count == 1 + + +def test_connect_datetime(): + + class Example(object): + t = CallbackProperty() + + e = Example() + + widget = QtWidgets.QDateTimeEdit() + widget.setTimeSpec(Qt.TimeSpec.UTC) + + conn = connect_datetime(e, 't', widget) # noqa + + dt = datetime(2010, 5, 7, 13, 15, 3) + qdt = QDateTime(dt.date(), dt.time(), Qt.TimeSpec.UTC) + widget.setDateTime(qdt) + widget.dateTimeChanged.emit(qdt) + assert dt == e.t.item() + + dt = datetime(2020, 3, 4, 7, 48, 16) + e.t = datetime64(dt) + assert widget.dateTime().toUTC().toPython() == dt