Source code for TermTk.TTkWidgets.TTkModelView.table_edit_proxy

# MIT License
#
# Copyright (c) 2025 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from __future__ import annotations

__all__ = ['TTkTableProxyEdit', 'TTkTableEditLeaving', 'TTkTableProxyEditWidget']


import datetime
from dataclasses import dataclass
from enum import Enum, auto
from typing import Union, Tuple, Type, List, Optional, Any, Callable

from TermTk.TTkCore.constant import TTkK
from TermTk.TTkCore.string import TTkString, TTkStringType
from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot
from TermTk.TTkCore.TTkTerm.inputkey import TTkKeyEvent

from TermTk.TTkWidgets.widget import TTkWidget
from TermTk.TTkWidgets.texedit import TTkTextEdit, TTkTextEditView
from TermTk.TTkWidgets.spinbox import TTkSpinBox
from TermTk.TTkWidgets.datetime_time import TTkTime
from TermTk.TTkWidgets.datetime_date import TTkDate
from TermTk.TTkWidgets.datetime_datetime import TTkDateTime
from TermTk.TTkWidgets.TTkPickers.textpicker import TTkTextPicker

[docs] class TTkTableEditLeaving(Enum): ''' Enum indicating the direction the user is leaving the table cell editor Used by :py:class:`TTkTableProxyEditWidget` to signal navigation intent ''' NONE = auto() TOP = auto() BOTTOM = auto() LEFT = auto() RIGHT = auto()
[docs] class TTkTableProxyEditWidget(TTkWidget): ''' Protocol for table cell editor widgets Any widget implementing these signals can be used as a table cell editor. The protocol ensures consistent behavior across different editor types. Example implementation:: class MyEditor(TTkLineEdit): __slots__ = ('leavingTriggered', 'dataChanged') def __init__(self, **kwargs): self.leavingTriggered = pyTTkSignal(TTkTableEditLeaving) self.dataChanged = pyTTkSignal(object) super().__init__(**kwargs) self.textChanged.connect(self.dataChanged.emit) ''' leavingTriggered: pyTTkSignal ''' This signal is emitted when the user navigates out of the editor :param direction: The direction of navigation :type direction: TTkTableEditLeaving ''' dataChanged: pyTTkSignal ''' This signal is emitted when the editor data changes :param data: The new data value :type data: object '''
[docs] @staticmethod def editWidgetFactory(data: object) -> TTkTableProxyEditWidget: ''' Factory method to create an editor widget from data :param data: The initial data value for the editor :type data: object :return: A new editor widget instance :rtype: :py:class:`TTkTableProxyEditWidget` :raises NotImplementedError: Must be implemented by subclasses ''' raise NotImplementedError()
[docs] def getCellData(self) -> object: ''' Get the current data value from the editor :return: The current cell data :rtype: object :raises NotImplementedError: Must be implemented by subclasses ''' raise NotImplementedError()
[docs] def proxyDispose(self) -> None: ''' Clean up the editor widget and disconnect signals ''' self.leavingTriggered.clear() self.dataChanged.clear() self.close()
class _TextEditViewProxy(TTkTextEditView, TTkTableProxyEditWidget): ''' Text editor view for table cells Extends :py:class:`TTkTextEditView` with table-specific signals for navigation and data change notification. ''' __slots__ = ('leavingTriggered', 'dataChanged') leavingTriggered: pyTTkSignal ''' This signal is emitted when the user navigates out of the editor :param direction: The direction of navigation :type direction: TTkTableEditLeaving ''' dataChanged: pyTTkSignal ''' This signal is emitted when the text content changes :param data: The new text value :type data: Union[str, TTkString] ''' def __init__(self, **kwargs): ''' Initialize the text edit view proxy ''' self.leavingTriggered = pyTTkSignal(TTkTableEditLeaving) self.dataChanged = pyTTkSignal(object) super().__init__(**kwargs) self.textChanged.connect(self._emitDataChanged) def keyEvent(self, evt: TTkKeyEvent) -> bool: ''' Handle keyboard events for navigation and data entry :param evt: The keyboard event :type evt: TTkKeyEvent :return: True if event was handled, False otherwise :rtype: bool ''' if (evt.type == TTkK.SpecialKey): _cur = self.textCursor() _doc = self.document() _line = _cur.anchor().line _pos = _cur.anchor().pos _lineCount = _doc.lineCount() if evt.mod == TTkK.NoModifier: if evt.key == TTkK.Key_Enter: self.leavingTriggered.emit(TTkTableEditLeaving.BOTTOM) return True elif evt.key == TTkK.Key_Up: if _line == 0: self.leavingTriggered.emit(TTkTableEditLeaving.TOP) return True elif evt.key == TTkK.Key_Down: if _lineCount == 1: self.leavingTriggered.emit(TTkTableEditLeaving.BOTTOM) return True elif evt.key == TTkK.Key_Left: if _pos == _line == 0: self.leavingTriggered.emit(TTkTableEditLeaving.LEFT) return True elif evt.key == TTkK.Key_Right: if _lineCount == 1 and _pos == len(_doc.toPlainText()): self.leavingTriggered.emit(TTkTableEditLeaving.RIGHT) return True elif (evt.type == TTkK.SpecialKey and evt.mod == TTkK.ControlModifier | TTkK.AltModifier and evt.key == TTkK.Key_M): evt.mod = TTkK.NoModifier evt.key = TTkK.Key_Enter return super().keyEvent(evt) @pyTTkSlot() def _emitDataChanged(self) -> None: ''' Emit dataChanged signal when text changes ''' txt = self.toRawText() val = str(txt) if txt.isPlainText() else txt self.dataChanged.emit(val) class _TextEditProxy(TTkTextEdit, TTkTableProxyEditWidget): ''' Text editor for table cells Extends :py:class:`TTkTextEdit` with table-specific signals for navigation and data change notification. ''' __slots__ = ('leavingTriggered', 'dataChanged') leavingTriggered: pyTTkSignal ''' This signal is emitted when the user navigates out of the editor :param direction: The direction of navigation :type direction: TTkTableEditLeaving ''' dataChanged: pyTTkSignal ''' This signal is emitted when the text content changes :param data: The new text value :type data: Union[str, TTkString] ''' def __init__(self, **kwargs): ''' Initialize the text edit proxy ''' self.leavingTriggered = pyTTkSignal(TTkTableEditLeaving) self.dataChanged = pyTTkSignal(object) tew = _TextEditViewProxy() super().__init__(**kwargs | {'textEditView': tew}) tew.leavingTriggered.connect(self.leavingTriggered.emit) tew.dataChanged.connect(self.dataChanged.emit) @staticmethod def editWidgetFactory(data: Any) -> TTkTableProxyEditWidget: ''' Factory method to create a text editor from string data :param data: The initial text value :type data: Union[str, TTkString] :return: A new text editor instance :rtype: :py:class:`TTkTableProxyEditWidget` :raises ValueError: If data is not a string or TTkString ''' if not isinstance(data, (TTkString, str)): raise ValueError(f"{data} is not a TTkStringType") te = _TextEditProxy() te.setText(data) return te def getCellData(self) -> TTkStringType: ''' Get the current text value from the editor :return: The current text content :rtype: Union[str, TTkString] ''' txt = self.toRawText() val = str(txt) if txt.isPlainText() else txt return val class _SpinBoxProxy(TTkSpinBox, TTkTableProxyEditWidget): ''' Numeric editor for table cells Extends :py:class:`TTkSpinBox` with table-specific signals for navigation and data change notification. ''' __slots__ = ('leavingTriggered', 'dataChanged') leavingTriggered: pyTTkSignal ''' This signal is emitted when the user navigates out of the editor :param direction: The direction of navigation :type direction: TTkTableEditLeaving ''' dataChanged: pyTTkSignal ''' This signal is emitted when the numeric value changes :param data: The new numeric value :type data: Union[int, float] ''' def __init__(self, **kwargs): ''' Initialize the spin box proxy ''' self.leavingTriggered = pyTTkSignal(TTkTableEditLeaving) self.dataChanged = pyTTkSignal(object) super().__init__(**kwargs) self.valueChanged.connect(self.dataChanged.emit) @staticmethod def editWidgetFactory(data: Any) -> TTkTableProxyEditWidget: ''' Factory method to create a spin box from numeric data :param data: The initial numeric value :type data: Union[int, float] :return: A new spin box instance :rtype: :py:class:`TTkTableProxyEditWidget` :raises ValueError: If data is not an int or float ''' if not isinstance(data, (int, float)): raise ValueError(f"{data} is not a int or float") sb = _SpinBoxProxy( minimum=-1000000, maximum=1000000, value=data) return sb def getCellData(self) -> Union[float, int]: ''' Get the current numeric value from the editor :return: The current spin box value :rtype: Union[int, float] ''' return self.value() def keyEvent(self, evt: TTkKeyEvent) -> bool: ''' Handle keyboard events for navigation :param evt: The keyboard event :type evt: TTkKeyEvent :return: True if event was handled, False otherwise :rtype: bool ''' if (evt.type == TTkK.SpecialKey): if evt.mod == TTkK.NoModifier: if evt.key == TTkK.Key_Enter: self.leavingTriggered.emit(TTkTableEditLeaving.RIGHT) return True return super().keyEvent(evt) class _DateTime_KeyGeneric(): ''' Mixin class for datetime widget keyboard navigation Provides common keyboard event handling for datetime-based editors, including arrow key navigation and Enter key handling. ''' leavingTriggered: pyTTkSignal dataChanged: pyTTkSignal def newKeyEvent(self, evt: TTkKeyEvent, cb:Callable[[TTkKeyEvent],bool]) -> bool: ''' Handle keyboard events with custom callback and navigation :param evt: The keyboard event :type evt: TTkKeyEvent :param cb: Callback function for additional key handling :type cb: Callable[[TTkKeyEvent], bool] :return: True if event was handled, False otherwise :rtype: bool ''' if (evt.type == TTkK.SpecialKey): if evt.mod == TTkK.NoModifier: if evt.key == TTkK.Key_Enter: self.leavingTriggered.emit(TTkTableEditLeaving.RIGHT) return True if cb(evt): return True if (evt.type == TTkK.SpecialKey): if evt.mod == TTkK.NoModifier: if evt.key == TTkK.Key_Up: self.leavingTriggered.emit(TTkTableEditLeaving.TOP) return True elif evt.key == TTkK.Key_Down: self.leavingTriggered.emit(TTkTableEditLeaving.BOTTOM) return True elif evt.key == TTkK.Key_Left: self.leavingTriggered.emit(TTkTableEditLeaving.LEFT) return True elif evt.key == TTkK.Key_Right: self.leavingTriggered.emit(TTkTableEditLeaving.RIGHT) return True return False class _DateTime_TimeProxy(TTkTime, TTkTableProxyEditWidget, _DateTime_KeyGeneric): ''' Time editor for table cells Extends :py:class:`TTkTime` with table-specific signals for navigation and data change notification. ''' __slots__ = ('leavingTriggered', 'dataChanged') leavingTriggered: pyTTkSignal ''' This signal is emitted when the user navigates out of the editor :param direction: The direction of navigation :type direction: TTkTableEditLeaving ''' dataChanged: pyTTkSignal ''' This signal is emitted when the time value changes :param data: The new time value :type data: datetime.time ''' def __init__(self, **kwargs): ''' Initialize the time editor proxy ''' self.leavingTriggered = pyTTkSignal(TTkTableEditLeaving) self.dataChanged = pyTTkSignal(object) super().__init__(**kwargs) self.timeChanged.connect(self.dataChanged.emit) @staticmethod def editWidgetFactory(data: Any) -> TTkTableProxyEditWidget: ''' Factory method to create a time editor from time data :param data: The initial time value :type data: datetime.time :return: A new time editor instance :rtype: :py:class:`TTkTableProxyEditWidget` :raises ValueError: If data is not a datetime.time ''' if not isinstance(data, datetime.time): raise ValueError(f"{data} is not a int or float") tp = _DateTime_TimeProxy(time=data) return tp def getCellData(self) -> datetime.time: ''' Get the current time value from the editor :return: The current time :rtype: datetime.time ''' return self.time() def keyEvent(self, evt: TTkKeyEvent) -> bool: ''' Handle keyboard events for navigation :param evt: The keyboard event :type evt: TTkKeyEvent :return: True if event was handled, False otherwise :rtype: bool ''' return self.newKeyEvent(evt,super().keyEvent) class _DateTime_DateProxy(TTkDate, TTkTableProxyEditWidget, _DateTime_KeyGeneric): ''' Date editor for table cells Extends :py:class:`TTkDate` with table-specific signals for navigation and data change notification. ''' __slots__ = ('leavingTriggered', 'dataChanged') leavingTriggered: pyTTkSignal ''' This signal is emitted when the user navigates out of the editor :param direction: The direction of navigation :type direction: TTkTableEditLeaving ''' dataChanged: pyTTkSignal ''' This signal is emitted when the date value changes :param data: The new date value :type data: datetime.date ''' def __init__(self, **kwargs): ''' Initialize the date editor proxy ''' self.leavingTriggered = pyTTkSignal(TTkTableEditLeaving) self.dataChanged = pyTTkSignal(object) super().__init__(**kwargs) self.dataChanged.connect(self.dataChanged.emit) @staticmethod def editWidgetFactory(data: Any) -> TTkTableProxyEditWidget: ''' Factory method to create a date editor from date data :param data: The initial date value :type data: datetime.date :return: A new date editor instance :rtype: :py:class:`TTkTableProxyEditWidget` :raises ValueError: If data is not a datetime.date ''' if not isinstance(data, datetime.date): raise ValueError(f"{data} is not a int or float") dp = _DateTime_DateProxy(date=data) return dp def getCellData(self) -> datetime.date: ''' Get the current date value from the editor :return: The current date :rtype: datetime.date ''' return self.date() def keyEvent(self, evt: TTkKeyEvent) -> bool: ''' Handle keyboard events for navigation :param evt: The keyboard event :type evt: TTkKeyEvent :return: True if event was handled, False otherwise :rtype: bool ''' return self.newKeyEvent(evt,super().keyEvent) class _DateTime_DateTimeProxy(TTkDateTime, TTkTableProxyEditWidget): ''' DateTime editor for table cells Extends :py:class:`TTkDateTime` with table-specific signals for navigation and data change notification. ''' __slots__ = ('leavingTriggered', 'dataChanged') leavingTriggered: pyTTkSignal ''' This signal is emitted when the user navigates out of the editor :param direction: The direction of navigation :type direction: TTkTableEditLeaving ''' dataChanged: pyTTkSignal ''' This signal is emitted when the datetime value changes :param data: The new datetime value :type data: datetime.datetime ''' def __init__(self, **kwargs): ''' Initialize the datetime editor proxy ''' self.leavingTriggered = pyTTkSignal(TTkTableEditLeaving) self.dataChanged = pyTTkSignal(object) super().__init__(**kwargs) self.datetimeChanged.connect(self.dataChanged.emit) @staticmethod def editWidgetFactory(data: Any) -> TTkTableProxyEditWidget: ''' Factory method to create a datetime editor from datetime data :param data: The initial datetime value :type data: datetime.datetime :return: A new datetime editor instance :rtype: :py:class:`TTkTableProxyEditWidget` :raises ValueError: If data is not a datetime.datetime ''' if not isinstance(data, datetime.datetime): raise ValueError(f"{data} is not a int or float") dtp = _DateTime_DateTimeProxy(datetime=data) return dtp def getCellData(self) -> datetime.datetime: ''' Get the current datetime value from the editor :return: The current datetime :rtype: datetime.datetime ''' return self.datetime() def keyEvent(self, evt: TTkKeyEvent) -> bool: ''' Handle keyboard events for navigation Always triggers right navigation on any key event. :param evt: The keyboard event :type evt: TTkKeyEvent :return: Always returns True :rtype: bool ''' self.leavingTriggered.emit(TTkTableEditLeaving.RIGHT) return True class _TextPickerProxy(TTkTextPicker, TTkTableProxyEditWidget): ''' Rich text editor for table cells Extends :py:class:`TTkTextPicker` with table-specific signals for navigation and data change notification. ''' __slots__ = ('leavingTriggered', 'dataChanged') leavingTriggered: pyTTkSignal ''' This signal is emitted when the user navigates out of the editor :param direction: The direction of navigation :type direction: TTkTableEditLeaving ''' dataChanged: pyTTkSignal ''' This signal is emitted when the rich text content changes :param data: The new TTkString value :type data: TTkString ''' def __init__(self, **kwargs): ''' Initialize the text picker proxy ''' self.leavingTriggered = pyTTkSignal(TTkTableEditLeaving) self.dataChanged = pyTTkSignal(object) super().__init__(**kwargs) self.textChanged.connect(self._textChanged) @pyTTkSlot() def _textChanged(self): ''' Internal slot to emit dataChanged signal ''' self.dataChanged.emit(self.getCellData()) @staticmethod def editWidgetFactory(data: Any) -> TTkTableProxyEditWidget: ''' Factory method to create a text picker from string data :param data: The initial text value :type data: Union[str, TTkString] :return: A new text picker instance :rtype: :py:class:`TTkTableProxyEditWidget` :raises ValueError: If data is not a string or TTkString ''' if not isinstance(data, (TTkString, str)): raise ValueError(f"{data} is not a TTkStringType") te = _TextPickerProxy( text=data, autoSize=False, wrapMode=TTkK.NoWrap) return te def getCellData(self) -> TTkString: ''' Get the current rich text value from the editor :return: The current TTkString content :rtype: TTkString ''' return self.getTTkString() def keyEvent(self, evt: TTkKeyEvent) -> bool: ''' Handle keyboard events for navigation :param evt: The keyboard event :type evt: TTkKeyEvent :return: True if event was handled, False otherwise :rtype: bool ''' if (evt.type == TTkK.SpecialKey): if evt.mod == TTkK.NoModifier: if evt.key == TTkK.Key_Enter: self.leavingTriggered.emit(TTkTableEditLeaving.RIGHT) return True return super().keyEvent(evt) @dataclass class TTkProxyEditDef(): ''' Definition for table cell editor proxy :param types: Tuple of data types this editor handles (e.g., (int, float)) :type types: Tuple[type, ...] :param class_def: Widget class implementing TTkTableProxyEditWidget protocol :type class_def: Type[TTkTableProxyEditWidget] :param rich: Whether this editor supports rich text formatting :type rich: bool ''' types: Tuple[type, ...] class_def: Type[TTkTableProxyEditWidget] rich: bool = False
[docs] class TTkTableProxyEdit(): ''' Proxy class for managing table cell editors Creates and configures appropriate editor widgets based on cell data type. All editors implement the :py:class:`TTkTableProxyEditWidget` protocol. Automatically selects the correct editor type: - :py:class:`_SpinBoxProxy` for int and float values - :py:class:`_TextEditProxy` for plain text strings - :py:class:`_TextPickerProxy` for rich text (TTkString with formatting) Example usage:: proxy = TTkTableProxyEdit() editor = proxy.getProxyWidget(data=42, rich=False) if editor: editor.leavingTriggered.connect(handleNavigation) editor.dataChanged.connect(handleDataChange) ''' __slots__ = ('_proxies',) _proxies: List[TTkProxyEditDef] def __init__(self): ''' Initialize the table proxy edit manager ''' self._proxies = [ TTkProxyEditDef(class_def=_SpinBoxProxy, types=(int, float)), TTkProxyEditDef(class_def=_TextEditProxy, types=(str, TTkString)), TTkProxyEditDef(class_def=_TextEditProxy, types=(str,), rich=True), TTkProxyEditDef(class_def=_TextPickerProxy, types=(TTkString,), rich=True), # Datetime go first because # datetime is instance of date as well TTkProxyEditDef(class_def=_DateTime_DateTimeProxy, types=(datetime.datetime)), TTkProxyEditDef(class_def=_DateTime_TimeProxy, types=(datetime.time)), TTkProxyEditDef(class_def=_DateTime_DateProxy, types=(datetime.date)), ]
[docs] def getProxyWidget(self, data, rich: bool = False) -> Optional[TTkTableProxyEditWidget]: ''' Get an appropriate editor widget for the given data :param data: The data value to edit :type data: object :param rich: Whether rich text editing is required :type rich: bool :return: An editor widget instance, or None if no suitable editor found :rtype: Optional[TTkTableProxyEditWidget] ''' for proxy in self._proxies: if proxy.rich == rich and isinstance(data, proxy.types): return proxy.class_def.editWidgetFactory(data) for proxy in self._proxies: if isinstance(data, proxy.types): return proxy.class_def.editWidgetFactory(data) return None