From 9331c036892e8cb81154e5d06e1d30f02110d7fe Mon Sep 17 00:00:00 2001 From: Pier CeccoPierangioliEugenio Date: Sat, 8 Nov 2025 15:05:46 +0000 Subject: [PATCH] feat(table): add edit proxy widget to allow a common extensible interface for the cell editing (#517) --- .../TTkWidgets/TTkModelView/__init__.py | 1 + .../TTkModelView/table_edit_proxy.py | 484 ++++++++++++++++++ .../TTkWidgets/TTkModelView/tablewidget.py | 203 +++----- tools/check.import.sh | 1 + 4 files changed, 559 insertions(+), 130 deletions(-) create mode 100644 libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/table_edit_proxy.py diff --git a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/__init__.py b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/__init__.py index 23a9e97d..f0eddf49 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/__init__.py +++ b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/__init__.py @@ -11,6 +11,7 @@ from .tablewidget import * from .tablewidgetitem import * from .tablemodellist import * from .tablemodelcsv import * +from .table_edit_proxy import * if find_spec('sqlite3'): from .tablemodelsqlite3 import * diff --git a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/table_edit_proxy.py b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/table_edit_proxy.py new file mode 100644 index 00000000..1e6a6941 --- /dev/null +++ b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/table_edit_proxy.py @@ -0,0 +1,484 @@ +# MIT License +# +# Copyright (c) 2025 Eugenio Parodi +# +# 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'] + +from dataclasses import dataclass +from enum import Enum, auto +from typing import Union, Tuple, Type, List, Optional, Any + +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.TTkPickers.textpicker import TTkTextPicker + +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() + + +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 + ''' + + @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: TTkTableProxyEditWidget + :raises NotImplementedError: Must be implemented by subclasses + ''' + raise NotImplementedError() + + 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() + + 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 + + :param kwargs: Additional keyword arguments passed to parent + :type kwargs: dict + ''' + 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 + + :param kwargs: Additional keyword arguments passed to parent + :type kwargs: dict + ''' + 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: 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 + + :param kwargs: Additional keyword arguments passed to parent + :type kwargs: dict + ''' + 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: 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 _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 + + :param kwargs: Additional keyword arguments passed to parent + :type kwargs: dict + ''' + 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: 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 + +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), + ] + + 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 diff --git a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py index f8925f4c..d3d6f1b4 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py +++ b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py @@ -27,7 +27,7 @@ __all__ = ['TTkTableWidget','TTkHeaderView'] from typing import Optional, List, Tuple, Callable, Iterator, Any, Protocol from dataclasses import dataclass -from TermTk.TTkCore.log import TTkLog +from TermTk.TTkCore.helper import TTkHelper from TermTk.TTkCore.constant import TTkK from TermTk.TTkCore.string import TTkString from TermTk.TTkCore.color import TTkColor @@ -42,6 +42,7 @@ from TermTk.TTkWidgets.texedit import TTkTextEdit from TermTk.TTkWidgets.spinbox import TTkSpinBox from TermTk.TTkWidgets.TTkPickers.textpicker import TTkTextPicker from TermTk.TTkWidgets.TTkModelView.tablemodellist import TTkTableModelList, TTkModelIndex +from TermTk.TTkWidgets.TTkModelView.table_edit_proxy import TTkTableProxyEdit, TTkTableProxyEditWidget, TTkTableEditLeaving from TermTk.TTkAbstract.abstractscrollview import TTkAbstractScrollView from TermTk.TTkAbstract.abstracttablemodel import TTkAbstractTableModel @@ -251,6 +252,13 @@ class _DragPosType(): fr:Tuple[int,int] to:Tuple[int,int] +@dataclass +class _ProxyWidgetLocation: + __slots__ = ('widget', 'row', 'col') + widget: TTkTableProxyEditWidget + row: int + col: int + class TTkTableWidget(TTkAbstractScrollView): ''' A :py:class:`TTkTableWidget` implements a table view that displays items from a model. @@ -403,6 +411,7 @@ class TTkTableWidget(TTkAbstractScrollView): '_sortColumn', '_sortOrder', '_fastCheck', '_guessDataEdit', '_snapshot', '_snapshotId', + '_edit_proxy', '_edit_proxy_widget', # Signals # '_cellActivated', '_cellChanged', @@ -412,6 +421,8 @@ class TTkTableWidget(TTkAbstractScrollView): ) _select_proxy:_SelectionProxy + _edit_proxy:TTkTableProxyEdit + _edit_proxy_widget:Optional[_ProxyWidgetLocation] _snapshot:List[_SnapshotItems] _hoverPos:Optional[Tuple[int,int]] _currentPos:Optional[Tuple[int,int]] @@ -421,6 +432,7 @@ class TTkTableWidget(TTkAbstractScrollView): def __init__(self, *, tableModel:Optional[TTkAbstractTableModel]=None, + tableEditProxy:Optional[TTkTableProxyEdit]=None, vSeparator:bool=True, hSeparator:bool=True, vHeader:bool=True, @@ -480,6 +492,8 @@ class TTkTableWidget(TTkAbstractScrollView): self._verticalHeader = TTkHeaderView(visible=vHeader) self._horizontallHeader = TTkHeaderView(visible=hHeader) self._select_proxy = _SelectionProxy() + self._edit_proxy = tableEditProxy if tableEditProxy else TTkTableProxyEdit() + self._edit_proxy_widget = None self._hoverPos = None self._dragPos = None self._currentPos = None @@ -500,8 +514,6 @@ class TTkTableWidget(TTkAbstractScrollView): self._verticalHeader.visibilityUpdated.connect( self._headerVisibilityChanged) self._horizontallHeader.visibilityUpdated.connect(self._headerVisibilityChanged) - - def _saveSnapshot(self, items:list[_SnapItem], currentPos:TTkModelIndex) -> None: self._snapshot = self._snapshot[:self._snapshotId] + [_SnapshotItems(pos=currentPos, items=items)] self._snapshotId += 1 @@ -734,6 +746,7 @@ class TTkTableWidget(TTkAbstractScrollView): Deselects all selected items. The current index will not be changed. ''' + self._removeProxyWidget() self._select_proxy.clear() self.update() @@ -742,6 +755,7 @@ class TTkTableWidget(TTkAbstractScrollView): Selects all items in the view. This function will use the selection behavior set on the view when selecting. ''' + self._removeProxyWidget() self._select_proxy.selectAll() self.update() @@ -756,6 +770,7 @@ class TTkTableWidget(TTkAbstractScrollView): :param flags: the selection model used (i.e. :py:class:`TTkItemSelectionModel.Select`) :type flags: :py:class:`TTkItemSelectionModel` ''' + self._removeProxyWidget() self._select_proxy.setSelection(pos=pos, size=size, flags=flags) self.update() @@ -766,6 +781,7 @@ class TTkTableWidget(TTkAbstractScrollView): :param row: the row to be selected :type row: int ''' + self._removeProxyWidget() self._select_proxy.selectRow(row=row) self.update() @@ -776,6 +792,7 @@ class TTkTableWidget(TTkAbstractScrollView): :param col: the column to be selected :type col: int ''' + self._removeProxyWidget() self._select_proxy.selectColumn(col=col) self.update() @@ -786,6 +803,7 @@ class TTkTableWidget(TTkAbstractScrollView): :param row: the row to be unselected :type row: int ''' + self._removeProxyWidget() self._select_proxy.unselectRow(row=row) self.update() @@ -796,6 +814,7 @@ class TTkTableWidget(TTkAbstractScrollView): :param col: the column to be unselected :type col: int ''' + self._removeProxyWidget() self._select_proxy.unselectColumn(col=col) self.update() @@ -1105,122 +1124,50 @@ class TTkTableWidget(TTkAbstractScrollView): return row,col - def _editStr(self, x,y,w,h, row, col, data): - _te = TTkTextEdit( - parent=self, pos=(x, y), size=(w,h), - readOnly=False, wrapMode=TTkK.NoWrap) - _tev = _te.textEditView() - _te.setText(data) - _te.textCursor().movePosition(operation=TTkTextCursor.EndOfLine) - _te.setFocus() - - @pyTTkSlot(bool) - def _processClose(change): - if change: - self.focusChanged.disconnect(_processClose) - txt = _te.toRawText() - val = str(txt) if txt.isPlainText() else txt - self._tableModel_setData([(row,col,val)]) - self.update() - _te.close() - self.setFocus() - - # Override the key event - _ke = _tev.keyEvent - _doc = _tev.document() - _cur = _tev.textCursor() - def _keyEvent(evt): - if ( evt.type == TTkK.SpecialKey): - _line = _cur.anchor().line - _pos = _cur.anchor().pos - _lineCount = _doc.lineCount() - # _lineLen - if evt.mod==TTkK.NoModifier: - if evt.key == TTkK.Key_Enter: - # self.enterPressed.emit(True) - self._moveCurrentCell(diff=(0,+1)) - _processClose(True) - return True - elif evt.key == TTkK.Key_Up: - if _line == 0: - self._moveCurrentCell(diff=(0,-1)) - _processClose(True) - return True - elif evt.key == TTkK.Key_Down: - if _lineCount == 1: - self._moveCurrentCell(diff=(0,+1)) - _processClose(True) - return True - elif evt.key == TTkK.Key_Left: - if _pos == _line == 0: - self._moveCurrentCell(diff=(-1, 0)) - _processClose(True) - return True - elif evt.key == TTkK.Key_Right: - if _lineCount == 1 and _pos==len(_doc.toPlainText()): - self._moveCurrentCell(diff=(+1, 0)) - _processClose(True) - 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 _ke(evt) - _tev.keyEvent = _keyEvent - - # _tev.enterPressed.connect(_processClose) - self.focusChanged.connect(_processClose) - - def _editNum(self, x,y,w,h, row, col, data): - _sb = TTkSpinBox( - parent=self, pos=(x, y), size=(w,1), - minimum=-1000000, maximum=1000000, - value=data) - _sb.setFocus() - - @pyTTkSlot(bool) - def _processClose(change): - if change: - self.focusChanged.disconnect(_processClose) - val = _sb.value() - self._tableModel_setData([(row,col,val)]) - self.update() - _sb.close() - self.setFocus() - - # Override the key event - _ke = _sb.keyEvent - def _keyEvent(evt): - if ( evt.type == TTkK.SpecialKey): - if evt.mod==TTkK.NoModifier: - if evt.key == TTkK.Key_Enter: - self._moveCurrentCell( 0,+1) - _processClose(True) - return True - return _ke(evt) - _sb.keyEvent = _keyEvent - - self.focusChanged.connect(_processClose) - - def _editTTkString(self, x,y,w,h, row, col, data): - _tp = TTkTextPicker( - parent=self, pos=(x, y), size=(w,h), - text=data, autoSize=False, wrapMode=TTkK.NoWrap) - - _tp.setFocus() - - @pyTTkSlot(bool) - def _processClose(change): - if change: - self.focusChanged.disconnect(_processClose) - txt = _tp.getTTkString() - self._tableModel_setData([(row,col,txt)]) - self.update() - _tp.close() - self.setFocus() + def _alignWidgets(self) -> None: + if not (epwl:=self._edit_proxy_widget): + return + epw = epwl.widget + row = epwl.row + col = epwl.col + showHS = self._showHSeparators + showVS = self._showVSeparators + rp = self._rowsPos + cp = self._colsPos + xa,xb = 1+cp[col-1] if col>0 else 0, cp[col] + (0 if showVS else 1) + ya,yb = 1+rp[row-1] if row>0 else 0, rp[row] + (0 if showHS else 1) + epw.setGeometry(xa,ya,xb-xa,yb-ya) - self.focusChanged.connect(_processClose) + def _removeProxyWidget(self, direction:TTkTableEditLeaving=TTkTableEditLeaving.NONE) -> None: + if not (epwl:=self._edit_proxy_widget): + return + epw = epwl.widget + row = epwl.row + col = epwl.col + self._edit_proxy_widget = None + self.layout().removeWidget(epw) + data = epw.getCellData() + self._tableModel_setData([(row,col,data)]) + epw.proxyDispose() + if direction == TTkTableEditLeaving.TOP: + self._moveCurrentCell(diff=(0,-1)) + elif direction == TTkTableEditLeaving.BOTTOM: + self._moveCurrentCell(diff=(0,+1)) + if direction == TTkTableEditLeaving.LEFT: + self._moveCurrentCell(diff=(-1,0)) + elif direction == TTkTableEditLeaving.RIGHT: + self._moveCurrentCell(diff=(+1,0)) + TTkHelper.hideCursor() + self.setFocus() + + def _placeProxyWidget(self, proxyWidgetLocation:_ProxyWidgetLocation) -> None: + self._removeProxyWidget() + self._edit_proxy_widget = proxyWidgetLocation + proxyWidget = proxyWidgetLocation.widget + self._alignWidgets() + self.layout().addWidget(proxyWidget) + proxyWidget.leavingTriggered.connect(self._removeProxyWidget) + proxyWidget.setFocus() def _editCell(self, row:int, col:int, richEditSupport:bool=True) -> None: if not (self._tableModel.flags(row=row,col=col) & TTkK.ItemFlag.ItemIsEditable): @@ -1239,16 +1186,10 @@ class TTkTableWidget(TTkAbstractScrollView): self.setSelection(pos=(col,row),size=(1,1),flags=TTkK.TTkItemSelectionModel.Select) data = self._tableModel.data(row, col) - if type(data) is str: - self._editStr(xa,ya,xb-xa,yb-ya,row,col,data) - elif type(data) in [int,float]: - self._editNum(xa,ya,xb-xa,yb-ya,row,col,data) - else: - data = self._tableModel.ttkStringData(row, col) - if richEditSupport: - self._editTTkString(xa,ya,xb-xa,yb-ya,row,col,data) - else: - self._editStr(xa,ya,xb-xa,yb-ya,row,col,data) + if proxyWidget := self._edit_proxy.getProxyWidget(data, rich=richEditSupport): + epwl = _ProxyWidgetLocation(widget=proxyWidget, row=row, col=col) + self._placeProxyWidget(epwl) + def _setCurrentCell(self, currRow:int, currCol:int) -> None: prevRow,prevCol = self._currentPos if self._currentPos else (0,0) @@ -1527,7 +1468,7 @@ class TTkTableWidget(TTkAbstractScrollView): # Align all the other Separators relative to the selection for i in range(ss, len(self._colsPos)): self._colsPos[i] += diff - # self._alignWidgets() + self._alignWidgets() self.viewChanged.emit() self.update() return True @@ -1542,7 +1483,7 @@ class TTkTableWidget(TTkAbstractScrollView): # Align all the other Separators relative to the selection for i in range(ss, len(self._rowsPos)): self._rowsPos[i] += diff - # self._alignWidgets() + self._alignWidgets() self.viewChanged.emit() self.update() return True @@ -1579,6 +1520,8 @@ class TTkTableWidget(TTkAbstractScrollView): self._hoverPos = None self._dragPos = None + if self._edit_proxy_widget: + self._edit_proxy_widget.widget.setFocus() self.update() return True diff --git a/tools/check.import.sh b/tools/check.import.sh index ccac79f9..8a0818b0 100755 --- a/tools/check.import.sh +++ b/tools/check.import.sh @@ -118,6 +118,7 @@ __check(){ -e "TTkWidgets/widget.py:from __future__ import annotations" \ -e "TTkWidgets/tabwidget.py:from enum import Enum" \ -e "TTkModelView/__init__.py:from importlib.util import find_spec" \ + -e "TTkModelView/table_edit_proxy.py:from enum import Enum, auto" \ -e "TTkModelView/tablemodelcsv.py:import csv" \ -e "TTkModelView/tablemodelsqlite3.py:import sqlite3" \ -e "TTkModelView/tablemodelsqlite3.py:import threading" |