Browse Source

refactor: Improve typing (#533)

pull/540/head
Pier CeccoPierangioliEugenio 4 months ago committed by GitHub
parent
commit
9c6a9fbfaa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 73
      .github/copilot-instructions.md
  2. 1
      libs/pyTermTk/TermTk/TTkCore/cfg.py
  3. 2
      libs/pyTermTk/TermTk/TTkCore/helper.py
  4. 2
      libs/pyTermTk/TermTk/TTkCore/timer_pyodide.py
  5. 1
      libs/pyTermTk/TermTk/TTkCore/timer_unix.py
  6. 2
      libs/pyTermTk/TermTk/TTkGui/textdocument_highlight_pygments.py
  7. 101
      libs/pyTermTk/TermTk/TTkGui/tooltip.py
  8. 358
      libs/pyTermTk/TermTk/TTkWidgets/apptemplate.py
  9. 354
      libs/pyTermTk/TermTk/TTkWidgets/splitter.py
  10. 133
      tests/timeit/34.dataclasses.py
  11. 1
      tools/check.import.sh

73
.github/copilot-instructions.md

@ -106,6 +106,25 @@ style = self.currentStyle() # Get theme-aware colors
### Documentation & Docstrings ### Documentation & Docstrings
Use **Sphinx-compatible docstring format** with Epytext-style field lists: Use **Sphinx-compatible docstring format** with Epytext-style field lists:
```python ```python
# In any sphinx reference
# i.e. ':py:class:' or ':py:meth:'
# the full path is not required but just che class name,
# the link will be resolved in one of the sphynx custom plugins.
def ttkStringData(self, row:int, col:int) -> TTkString:
'''
Returns the :py:class:`TTkString` reprsents the data stored in the row/column.
:param row: the row position of the data
:type row: int
:param col: the column position of the data
:type col: int
:return: the formatted string
:rtype: :py:class:`TTkString`
'''
data = self.data(row,col)
return TTkAbstractTableModel._dataToTTkString(data)
def setGeometry(self, x: int, y: int, width: int, height: int): def setGeometry(self, x: int, y: int, width: int, height: int):
''' Resize and move the widget ''' Resize and move the widget
@ -119,18 +138,56 @@ def setGeometry(self, x: int, y: int, width: int, height: int):
:type height: int :type height: int
''' '''
# For class/module docstrings include ASCII art examples: # For class/module docstrings include ASCII art examples,
class TTkButton(TTkWidget): # a link to the demo and the sandbox link if available:
''' TTkButton: # A small code example if not too complex
class TTkDate(TTkWidget):
''' TTkDate:
A widget for displaying and editing dates. (`demo <https://ceccopierangiolieugenio.github.io/pyTermTk-Docs/sandbox/sandbox.html?filePath=demo/showcase/date_time.py>`__)
Border = True
:: ::
┌────────┐ 2025/11/04 📅
│ Text │
╘════════╛ .. code:: python
import TermTk as ttk
root = ttk.TTk(mouseTrack=True)
ttk.TTkDate(parent=root) # Defaults to the current date
root.mainloop()
'''
class TTkAppTemplate(TTkContainer):
''' TTkAppTemplate:
A flexible application template layout with multiple resizable panels.
::
Demo: `formwidgets.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/demo/showcase/formwidgets.py>`_ App Template Layout
┌─────────────────────────────────┐
│ Header │
├─────────┬──────────────┬────────┤ H
│ │ Top │ │
│ ├──────────────┤ │ T
│ │ │ │
│ Right │ Main │ Left │
│ │ Center │ │
│ │ │ │
│ ├──────────────┤ │ B
│ │ Bottom │ │
├─────────┴──────────────┴────────┤ F
│ Footer │
└─────────────────────────────────┘
R L
Demo: `apptemplate.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/demo/showcase/apptemplate.py>`_
`online <https://ceccopierangiolieugenio.github.io/pyTermTk-Docs/sandbox/sandbox.html?filePath=demo/showcase/apptemplate.py>`_
''' '''
# For signals, document parameters: # For signals, document parameters:

1
libs/pyTermTk/TermTk/TTkCore/cfg.py

@ -36,7 +36,6 @@ class _TTkCfg:
color_depth: int = TTkK.DEP_24 color_depth: int = TTkK.DEP_24
toolTipTime:int = 1
maxFps:int = 65 maxFps:int = 65
doubleBuffer:bool = True doubleBuffer:bool = True
doubleBufferNew:bool = False doubleBufferNew:bool = False

2
libs/pyTermTk/TermTk/TTkCore/helper.py

@ -573,7 +573,7 @@ class TTkHelper:
# ToolTip Helper Methods # ToolTip Helper Methods
toolTipWidget: Optional[TTkWidget] = None toolTipWidget: Optional[TTkWidget] = None
toolTipTrigger: Callable[[TTkString], bool] = lambda _: True toolTipTrigger: Callable[[TTkString], None] = lambda _: None
toolTipReset: Callable[[], None] = lambda : None toolTipReset: Callable[[], None] = lambda : None
@staticmethod @staticmethod

2
libs/pyTermTk/TermTk/TTkCore/timer_pyodide.py

@ -40,7 +40,7 @@ class TTkTimer():
'timeout', '_timerEvent', 'timeout', '_timerEvent',
'_delay', '_delayLock', '_quit', '_delay', '_delayLock', '_quit',
'_stopTime') '_stopTime')
timeout:pyTTkSignal
def __init__( def __init__(
self, self,
name:Optional[str]=None, name:Optional[str]=None,

1
libs/pyTermTk/TermTk/TTkCore/timer_unix.py

@ -35,6 +35,7 @@ class TTkTimer(threading.Thread):
'_timer', '_quit', '_start', '_timer', '_quit', '_start',
'_excepthook' '_excepthook'
) )
timeout:pyTTkSignal
_delay:float _delay:float
_excepthook:Optional[Callable[[Exception],None]] _excepthook:Optional[Callable[[Exception],None]]
def __init__( def __init__(

2
libs/pyTermTk/TermTk/TTkGui/textdocument_highlight_pygments.py

@ -97,7 +97,7 @@ class _TTkFormatter(Formatter):
ttype = ttype.parent ttype = ttype.parent
# TTkLog.debug (f"{ttype=}") # TTkLog.debug (f"{ttype=}")
# TTkLog.debug (f"{value=}") # TTkLog.debug (f"{value=}")
color:TTkColor = self._highlightStyles[ttype] color = self._highlightStyles[ttype]
if not color.hasForeground(): if not color.hasForeground():
color += self._defaultColor color += self._defaultColor

101
libs/pyTermTk/TermTk/TTkGui/tooltip.py

@ -22,6 +22,8 @@
__all__ = ['TTkToolTip'] __all__ = ['TTkToolTip']
from typing import List
# from TermTk.TTkCore.helper import TTkHelper # from TermTk.TTkCore.helper import TTkHelper
from TermTk.TTkCore.log import TTkLog from TermTk.TTkCore.log import TTkLog
from TermTk.TTkCore.canvas import TTkCanvas from TermTk.TTkCore.canvas import TTkCanvas
@ -29,26 +31,62 @@ from TermTk.TTkCore.cfg import TTkCfg
from TermTk.TTkCore.color import TTkColor from TermTk.TTkCore.color import TTkColor
from TermTk.TTkCore.timer import TTkTimer from TermTk.TTkCore.timer import TTkTimer
from TermTk.TTkCore.helper import TTkHelper from TermTk.TTkCore.helper import TTkHelper
from TermTk.TTkCore.string import TTkString from TermTk.TTkCore.string import TTkString, TTkStringType
from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent
from TermTk.TTkWidgets.widget import TTkWidget from TermTk.TTkWidgets.widget import TTkWidget
from TermTk.TTkCore.signal import pyTTkSlot from TermTk.TTkCore.signal import pyTTkSlot
class _TTkToolTipDisplayWidget(TTkWidget): class _TTkToolTipDisplayWidget(TTkWidget):
__slots__ = ('_toolTip', '_x', '_y') ''' _TTkToolTipDisplayWidget:
Internal widget that renders tooltip content in a bordered box.
::
Tooltip text
Multiple lines
This widget is automatically sized based on tooltip content
and uses a rounded border style for visual distinction.
'''
__slots__ = ('_tooltip_list', '_x', '_y')
_tooltip_list:List[TTkString]
def __init__(self, *, def __init__(self, *,
toolTip:TTkString="", toolTip:TTkStringType="",
**kwargs) -> None: **kwargs) -> None:
''' Initialize the tooltip display widget
:param toolTip: The tooltip text to display (supports multiline with \\n)
:type toolTip: :py:class:`TTkString`, optional
'''
super().__init__(**kwargs) super().__init__(**kwargs)
self._toolTip = TTkString(toolTip).split('\n') if isinstance(toolTip,TTkString):
w = 2+max([s.termWidth() for s in self._toolTip]) self._tooltip_list = toolTip.split('\n')
h = 2+len(self._toolTip) else:
self._tooltip_list = TTkString(toolTip).split('\n')
w = 2+max([s.termWidth() for s in self._tooltip_list])
h = 2+len(self._tooltip_list)
self.resize(w,h) self.resize(w,h)
def mouseEvent(self, evt: TTkMouseEvent) -> bool: def mouseEvent(self, evt: TTkMouseEvent) -> bool:
''' Handle mouse events (always returns False to allow click-through)
:param evt: The mouse event
:type evt: :py:class:`TTkMouseEvent`
:return: False to propagate event
:rtype: bool
'''
return False return False
def paintEvent(self, canvas: TTkCanvas) -> None: def paintEvent(self, canvas: TTkCanvas) -> None:
''' Paint the tooltip with rounded border and text content
:param canvas: The canvas to draw on
:type canvas: :py:class:`TTkCanvas`
'''
w,h = self.size() w,h = self.size()
borderColor = TTkColor.fg("#888888") borderColor = TTkColor.fg("#888888")
canvas.drawBox(pos=(0,0),size=(w,h), color=borderColor) canvas.drawBox(pos=(0,0),size=(w,h), color=borderColor)
@ -56,27 +94,70 @@ class _TTkToolTipDisplayWidget(TTkWidget):
canvas.drawChar(pos=(w-1,0), char='', color=borderColor) canvas.drawChar(pos=(w-1,0), char='', color=borderColor)
canvas.drawChar(pos=(w-1,h-1),char='', color=borderColor) canvas.drawChar(pos=(w-1,h-1),char='', color=borderColor)
canvas.drawChar(pos=(0, h-1),char='', color=borderColor) canvas.drawChar(pos=(0, h-1),char='', color=borderColor)
for i,s in enumerate(self._toolTip,1): for i,s in enumerate(self._tooltip_list,1):
canvas.drawTTkString(pos=(1,i), text=s) canvas.drawTTkString(pos=(1,i), text=s)
class TTkToolTip(): class TTkToolTip():
''' TTkToolTip:
Global tooltip manager for delayed display of help text.
This class manages tooltip behavior across the application, including:
- Delayed tooltip display after hover timeout (configurable via :py:class:`TTkToolTip._toolTipTime`)
- Automatic positioning and sizing
- Support for multiline tooltips
.. note::
This is a singleton-like class using class methods. Do not instantiate it directly.
Usage:
.. code-block:: python
# Widgets set tooltips via their toolTip property
button = TTkButton(text="Click me", toolTip="This button does something")
# The tooltip system automatically handles display timing and positioning
'''
_toolTipTime:int = 1
'''Timeout in seconds'''
toolTipTimer:TTkTimer = TTkTimer(name='ToolTip') toolTipTimer:TTkTimer = TTkTimer(name='ToolTip')
toolTip:TTkString = TTkString() '''Internal timer for delayed tooltip display'''
toolTip:TTkStringType = ''
'''Current tooltip text to be displayed'''
@pyTTkSlot() @pyTTkSlot()
@staticmethod @staticmethod
def _toolTipShow() -> None: def _toolTipShow() -> None:
''' Internal slot that creates and displays the tooltip widget
This method is called by the timer after the configured delay period.
'''
# TTkLog.debug(f"TT:{TTkToolTip.toolTip}") # TTkLog.debug(f"TT:{TTkToolTip.toolTip}")
TTkHelper.toolTipShow(_TTkToolTipDisplayWidget(toolTip=TTkToolTip.toolTip)) TTkHelper.toolTipShow(_TTkToolTipDisplayWidget(toolTip=TTkToolTip.toolTip))
@staticmethod @staticmethod
def trigger(toolTip) -> None: def trigger(toolTip:TTkStringType) -> None:
''' Trigger a tooltip to be displayed after the configured delay
:param toolTip: The tooltip text to display (supports \\n for multiline)
:type toolTip: :py:class:`TTkString`
'''
# TTkToolTip.toolTipTimer.stop() # TTkToolTip.toolTipTimer.stop()
TTkToolTip.toolTip = toolTip TTkToolTip.toolTip = toolTip
TTkToolTip.toolTipTimer.start(TTkCfg.toolTipTime) TTkToolTip.toolTipTimer.start(TTkToolTip._toolTipTime)
@staticmethod @staticmethod
def reset() -> None: def reset() -> None:
''' Cancel any pending tooltip display
This is typically called when the mouse leaves a widget
or when the tooltip should be hidden.
'''
TTkToolTip.toolTipTimer.stop() TTkToolTip.toolTipTimer.stop()
TTkToolTip.toolTipTimer.timeout.connect(TTkToolTip._toolTipShow) TTkToolTip.toolTipTimer.timeout.connect(TTkToolTip._toolTipShow)

358
libs/pyTermTk/TermTk/TTkWidgets/apptemplate.py

@ -20,21 +20,28 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
from __future__ import annotations
__all__ = ['TTkAppTemplate'] __all__ = ['TTkAppTemplate']
from enum import IntEnum
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional,List,Dict,Literal,Tuple
from TermTk.TTkCore.constant import TTkK from TermTk.TTkCore.constant import TTkK
from TermTk.TTkCore.color import TTkColor from TermTk.TTkCore.color import TTkColor
from TermTk.TTkCore.canvas import TTkCanvas from TermTk.TTkCore.canvas import TTkCanvas
from TermTk.TTkCore.string import TTkString from TermTk.TTkCore.string import TTkString, TTkStringType
from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent
from TermTk.TTkLayouts import TTkLayout, TTkGridLayout from TermTk.TTkLayouts import TTkLayout, TTkGridLayout
from TermTk.TTkWidgets.container import TTkWidget, TTkContainer from TermTk.TTkWidgets.container import TTkWidget, TTkContainer
from TermTk.TTkWidgets.menubar import TTkMenuBarLayout from TermTk.TTkWidgets.menubar import TTkMenuBarLayout
class TTkAppTemplate(TTkContainer): class TTkAppTemplate(TTkContainer):
''' TTkAppTemplate Layout: ''' TTkAppTemplate:
A flexible application template layout with multiple resizable panels.
:: ::
@ -54,9 +61,12 @@ class TTkAppTemplate(TTkContainer):
Footer Footer
R L R L
Demo: `apptemplate.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/demo/showcase/apptemplate.py>`_
`online <https://ceccopierangiolieugenio.github.io/pyTermTk-Docs/sandbox/sandbox.html?filePath=showcase/apptemplate.py>`_
''' '''
class Position(int): class Position(IntEnum):
''' This Class enumerate the different panels available in the layout of :py:class:`TTkAppTemplate` ''' This Class enumerate the different panels available in the layout of :py:class:`TTkAppTemplate`
.. autosummary:: .. autosummary::
@ -87,29 +97,29 @@ class TTkAppTemplate(TTkContainer):
'''Footer''' '''Footer'''
MAIN = Position.MAIN MAIN = Position.MAIN
''':py:class:`Position.MAIN`''' ''':py:class:`TTkAppTemplate.Position.MAIN`'''
TOP = Position.TOP TOP = Position.TOP
''':py:class:`Position.TOP`''' ''':py:class:`TTkAppTemplate.Position.TOP`'''
BOTTOM = Position.BOTTOM BOTTOM = Position.BOTTOM
''':py:class:`Position.BOTTOM`''' ''':py:class:`TTkAppTemplate.Position.BOTTOM`'''
LEFT = Position.LEFT LEFT = Position.LEFT
''':py:class:`Position.LEFT`''' ''':py:class:`TTkAppTemplate.Position.LEFT`'''
RIGHT = Position.RIGHT RIGHT = Position.RIGHT
''':py:class:`Position.RIGHT`''' ''':py:class:`TTkAppTemplate.Position.RIGHT`'''
CENTER = Position.CENTER CENTER = Position.CENTER
''':py:class:`Position.CENTER`''' ''':py:class:`TTkAppTemplate.Position.CENTER`'''
HEADER = Position.HEADER HEADER = Position.HEADER
''':py:class:`Position.HEADER`''' ''':py:class:`TTkAppTemplate.Position.HEADER`'''
FOOTER = Position.FOOTER FOOTER = Position.FOOTER
''':py:class:`Position.FOOTER`''' ''':py:class:`TTkAppTemplate.Position.FOOTER`'''
@dataclass(frozen=False) @dataclass(frozen=False)
class _Panel: class _Panel:
# It's either item or widget # It's either item or widget
item: TTkLayout = None item: Optional[TTkLayout] = None
widget: TTkWidget = None widget: Optional[TTkWidget] = None
title: TTkString = None title: Optional[TTkString] = None
menubar: TTkMenuBarLayout = None menubar: Optional[TTkMenuBarLayout] = None
size: int = 0 size: int = 0
border: bool = True border: bool = True
fixed: bool = False fixed: bool = False
@ -167,14 +177,31 @@ class TTkAppTemplate(TTkContainer):
return wid.maximumHeight() return wid.maximumHeight()
return 0x10000 return 0x10000
@dataclass
class _Splitter():
pos:Tuple[int,int]
size:int
fixed:bool
panel:TTkAppTemplate._Panel
@dataclass
class _MenuBarLine():
pos:Tuple[int,int]
text:str
__slots__ = ('_panels', '_splitters', '_menubarLines', '_selected' __slots__ = ('_panels', '_splitters', '_menubarLines', '_selected'
#Signals #Signals
) )
_selected:List[TTkAppTemplate.Position]
_panels:Dict[TTkAppTemplate.Position,Optional[TTkAppTemplate._Panel]]
_splitters:Dict[TTkAppTemplate.Position,Optional[TTkAppTemplate._Splitter]]
_menubarLines:Dict[TTkAppTemplate.Position,Optional[TTkAppTemplate._MenuBarLine]]
def __init__(self, def __init__(self,
border=False, border=False,
**kwargs) -> None: **kwargs) -> None:
mp = TTkAppTemplate._Panel(item=TTkLayout(), border=border)
self._panels = { self._panels = {
TTkAppTemplate.MAIN : TTkAppTemplate._Panel(item=TTkLayout(), border=border) , TTkAppTemplate.MAIN : mp ,
TTkAppTemplate.TOP : None , TTkAppTemplate.TOP : None ,
TTkAppTemplate.BOTTOM : None , TTkAppTemplate.BOTTOM : None ,
TTkAppTemplate.LEFT : None , TTkAppTemplate.LEFT : None ,
@ -196,120 +223,130 @@ class TTkAppTemplate(TTkContainer):
TTkAppTemplate.RIGHT : None , TTkAppTemplate.RIGHT : None ,
TTkAppTemplate.HEADER : None , TTkAppTemplate.HEADER : None ,
TTkAppTemplate.FOOTER : None } TTkAppTemplate.FOOTER : None }
self._selected = None self._selected = []
super().__init__( **kwargs) super().__init__( **kwargs)
self.layout().addItem(self._panels[TTkAppTemplate.MAIN].item) self.layout().addItem(mp.item)
self._updateGeometries(force=True) self._updateGeometries(force=True)
self.setFocusPolicy(TTkK.ClickFocus) self.setFocusPolicy(TTkK.ClickFocus)
def setWidget(self, def setWidget(self,
widget:TTkWidget, position:Position=Position.MAIN, widget:TTkWidget, position:TTkAppTemplate.Position=Position.MAIN,
size:int=None, title:TTkString="", size:Optional[int]=None, title:TTkStringType="",
border:bool=None, fixed:bool=None) -> None: border:Optional[bool]=None, fixed:Optional[bool]=None) -> None:
''' '''
Place the :py:class:`TTkWidget` in the :py:class:`TTkAppTemplate`'s panel identified by its :py:class:`Position` Place the :py:class:`TTkWidget` in the :py:class:`TTkAppTemplate`'s panel identified by its :py:class:`TTkAppTemplate.Position`
:param widget: :param widget: The widget to place in the panel
:type widget: :py:class:`TTkWidget` :type widget: :py:class:`TTkWidget`
:param position: defaults to :py:class:`Position.MAIN` :param position: The panel position, defaults to :py:class:`TTkAppTemplate.Position.MAIN`
:type position: :py:class:`Position`, optional :type position: :py:class:`TTkAppTemplate.Position`, optional
:param size: defaults to None :param size: The panel size in characters (width for LEFT/RIGHT, height for TOP/BOTTOM/HEADER/FOOTER), defaults to widget's minimum size
:type size: int, optional :type size: int, optional
:param title: defaults to "" :param title: The panel title displayed in the border, defaults to ""
:type title: :py:class:`TTkString`, optional :type title: :py:class:`TTkString`, optional
:param border: defaults to True :param border: Whether to draw a border around the panel, defaults to True
:type border: bool, optional :type border: bool, optional
:param fixed: defaults to False :param fixed: Whether the panel size is fixed (non-resizable), defaults to False
:type fixed: bool, optional :type fixed: bool, optional
''' '''
if not self._panels[position]: if not (p:=self._panels[position]):
self._panels[position] = TTkAppTemplate._Panel() p = self._panels[position] = TTkAppTemplate._Panel()
if wid:=self._panels[position].widget: if wid:=p.widget:
self.layout().removeWidget(wid) self.layout().removeWidget(wid)
self._panels[position].widget = None p.widget = None
if it:=self._panels[position].item: if it:=p.item:
self.layout().removeItem(it) self.layout().removeItem(it)
self._panels[position].item = None p.item = None
if widget: if widget:
self._panels[position].widget = widget p.widget = widget
self.layout().addWidget(widget) self.layout().addWidget(widget)
if border!=None: if border is not None:
self._panels[position].border = border p.border = border
if fixed is not None: if fixed is not None:
self._panels[position].fixed = fixed p.fixed = fixed
self._panels[position].title = TTkString(title) p.title = title if isinstance(title,TTkString) else TTkString(title)
self._panels[position].size = ( size if size is not None else p.size = (
widget.minimumWidth() if position in (TTkAppTemplate.LEFT,TTkAppTemplate.RIGHT) else size if size is not None else
widget.minimumHeight() ) widget.minimumWidth() if position in (TTkAppTemplate.LEFT,TTkAppTemplate.RIGHT) else
widget.minimumHeight() )
self._updateGeometries(force=True) self._updateGeometries(force=True)
def setItem(self, def setItem(self,
item:TTkLayout, position:Position=Position.MAIN, item:TTkLayout, position:TTkAppTemplate.Position=Position.MAIN,
size:int=None, title:TTkString="", size:Optional[int]=None, title:TTkStringType="",
border:bool=None, fixed:bool=None) -> None: border:Optional[bool]=None, fixed:Optional[bool]=None) -> None:
''' ''' Place the :py:class:`TTkLayout` in the :py:class:`TTkAppTemplate`'s panel identified by its :py:class:`TTkAppTemplate.Position`
Place the :py:class:`TTkLayout` in the :py:class:`TTkAppTemplate`'s panel identified by its :py:class:`Position`
:param item: :param item: The layout to place in the panel
:type item: :py:class:`TTkLayout` :type item: :py:class:`TTkLayout`
:param position: defaults to :py:class:`Position.MAIN` :param position: The panel position, defaults to :py:class:`TTkAppTemplate.Position.MAIN`
:type position: :py:class:`Position`, optional :type position: :py:class:`TTkAppTemplate.Position`, optional
:param size: defaults to None :param size: The panel size in characters (width for LEFT/RIGHT, height for TOP/BOTTOM/HEADER/FOOTER), defaults to layout's minimum size
:type size: int, optional :type size: int, optional
:param title: defaults to "" :param title: The panel title displayed in the border, defaults to ""
:type title: :py:class:`TTkString`, optional :type title: :py:class:`TTkString`, optional
:param border: defaults to True :param border: Whether to draw a border around the panel, defaults to True
:type border: bool, optional :type border: bool, optional
:param fixed: defaults to False :param fixed: Whether the panel size is fixed (non-resizable), defaults to False
:type fixed: bool, optional :type fixed: bool, optional
''' '''
if not self._panels[position]: if not (p:=self._panels[position]):
self._panels[position] = TTkAppTemplate._Panel() p = self._panels[position] = TTkAppTemplate._Panel()
if wid:=self._panels[position].widget: if wid:=p.widget:
self.layout().removeWidget(wid) self.layout().removeWidget(wid)
self._panels[position].widget = None p.widget = None
if it:=self._panels[position].item: if it:=p.item:
self.layout().removeItem(it) self.layout().removeItem(it)
self._panels[position].item = None p.item = None
if item: if item:
self._panels[position].item = item p.item = item
self.layout().addItem(item) self.layout().addItem(item)
if border!=None: if border!=None:
self._panels[position].border = border p.border = border
if fixed is not None: if fixed is not None:
self._panels[position].fixed = fixed p.fixed = fixed
self._panels[position].title = TTkString(title) p.title = title if isinstance(title,TTkString) else TTkString(title)
self._panels[position].size = ( size if size is not None else p.size = (
item.minimumWidth() if position in (TTkAppTemplate.LEFT,TTkAppTemplate.RIGHT) else size if size is not None else
item.minimumHeight() ) item.minimumWidth() if position in (TTkAppTemplate.LEFT,TTkAppTemplate.RIGHT) else
item.minimumHeight() )
self._updateGeometries(force=True) self._updateGeometries(force=True)
def setTitle(self, position:Position=Position.MAIN, title:str=""): def setTitle(self, position:TTkAppTemplate.Position=Position.MAIN, title:TTkStringType="") -> None:
'''Set the title of the panel identified by the "position" ''' Set the title of the panel identified by the position
:param position: defaults to :py:class:`Position.MAIN` :param position: The panel position, defaults to :py:class:`TTkAppTemplate.Position.MAIN`
:type position: :py:class:`Position`, optional :type position: :py:class:`TTkAppTemplate.Position`, optional
:param title: defaults to "" :param title: The title text to display, defaults to ""
:type title: :py:class:`TTkString`, optional :type title: :py:class:`TTkString`, optional
''' '''
if not self._panels[position]: return if not (p:=self._panels[position]):
self._panels[position].title = TTkString(title) if title else "" return
p.title = title if isinstance(title,TTkString) else TTkString(title)
self._updateGeometries(force=True) self._updateGeometries(force=True)
def menuBar(self, position:Position=MAIN) -> TTkMenuBarLayout: def menuBar(self, position:TTkAppTemplate.Position=MAIN) -> Optional[TTkMenuBarLayout]:
''' ''' Retrieve the :py:class:`TTkMenuBarLayout` in the panel identified by the position
Retrieve the :py:class:`TTkMenuBarLayout` in the panel identified by the position.
:param position: defaults to :py:class:`Position.MAIN` :param position: The panel position, defaults to :py:class:`TTkAppTemplate.Position.MAIN`
:type position: :py:class:`Position`, optional :type position: :py:class:`TTkAppTemplate.Position`, optional
:return: The menu bar layout or None if not set
:rtype: :py:class:`TTkMenuBarLayout` or None
''' '''
return None if not self._panels[position] else self._panels[position].menubar return None if not (p:=self._panels[position]) else p.menubar
def setMenuBar(self, menuBar:TTkMenuBarLayout, position:TTkAppTemplate.Position=MAIN) -> None:
''' Set the :py:class:`TTkMenuBarLayout` for the panel identified by the position
def setMenuBar(self, menuBar:TTkMenuBarLayout, position:Position=MAIN) -> None: :param menuBar: The menu bar layout to set
if not self._panels[position]: :type menuBar: :py:class:`TTkMenuBarLayout`
self._panels[position] = TTkAppTemplate._Panel() :param position: The panel position, defaults to :py:class:`TTkAppTemplate.Position.MAIN`
p = self._panels[position] :type position: :py:class:`TTkAppTemplate.Position`, optional
'''
if not (p:=self._panels[position]):
p = self._panels[position] = TTkAppTemplate._Panel()
if p.menubar: if p.menubar:
self.rootLayout().removeItem(p.menubar) self.rootLayout().removeItem(p.menubar)
# TODO: Dispose the menubar # TODO: Dispose the menubar
@ -319,26 +356,40 @@ class TTkAppTemplate(TTkContainer):
self._updateGeometries(force=True) self._updateGeometries(force=True)
def setBorder(self, border=True, position=MAIN) -> None: def setBorder(self, border=True, position=MAIN) -> None:
if not self._panels[position]: ''' Set whether to draw a border around the panel
self._panels[position] = TTkAppTemplate._Panel()
self._panels[position].border = border :param border: True to show border, False to hide, defaults to True
:type border: bool, optional
:param position: The panel position, defaults to :py:class:`TTkAppTemplate.Position.MAIN`
:type position: :py:class:`TTkAppTemplate.Position`, optional
'''
if not (p:=self._panels[position]):
p = self._panels[position] = TTkAppTemplate._Panel()
p.border = border
self._updateGeometries(force=True) self._updateGeometries(force=True)
def setFixed(self, fixed=False, position=MAIN) -> None: def setFixed(self, fixed=False, position=MAIN) -> None:
if not self._panels[position]: ''' Set whether the panel size is fixed (non-resizable)
self._panels[position] = TTkAppTemplate._Panel()
self._panels[position].fixed = fixed :param fixed: True for fixed size, False for resizable, defaults to False
:type fixed: bool, optional
:param position: The panel position, defaults to :py:class:`TTkAppTemplate.Position.MAIN`
:type position: :py:class:`TTkAppTemplate.Position`, optional
'''
if not (p:=self._panels[position]):
p = self._panels[position] = TTkAppTemplate._Panel()
p.fixed = fixed
self._updateGeometries(force=True) self._updateGeometries(force=True)
def resizeEvent(self, width: int, height: int) -> None: def resizeEvent(self, width: int, height: int) -> None:
self._updateGeometries() self._updateGeometries()
def focusOutEvent(self) -> None: def focusOutEvent(self) -> None:
self._selected = None self._selected = []
self.update() self.update()
def mouseReleaseEvent(self, evt:TTkMouseEvent) -> bool: def mouseReleaseEvent(self, evt:TTkMouseEvent) -> bool:
self._selected = None self._selected = []
self.update() self.update()
return True return True
@ -348,10 +399,10 @@ class TTkAppTemplate(TTkContainer):
spl = self._splitters spl = self._splitters
pns = self._panels pns = self._panels
for loc in (TTkAppTemplate.TOP, TTkAppTemplate.BOTTOM, TTkAppTemplate.HEADER, TTkAppTemplate.FOOTER): for loc in (TTkAppTemplate.TOP, TTkAppTemplate.BOTTOM, TTkAppTemplate.HEADER, TTkAppTemplate.FOOTER):
if (s:=spl[loc]) and not pns[loc].fixed and (p:=s['pos'])[1]==evt.y and p[0] <= evt.x <=p[0]+s['size']: if (s:=spl[loc]) and (pn:=pns[loc]) and not pn.fixed and (p:=s.pos)[1]==evt.y and p[0] <= evt.x <=p[0]+s.size:
self._selected.append(loc) self._selected.append(loc)
for loc in (TTkAppTemplate.LEFT, TTkAppTemplate.RIGHT): for loc in (TTkAppTemplate.LEFT, TTkAppTemplate.RIGHT):
if (s:=spl[loc]) and not pns[loc].fixed and (p:=s['pos'])[0]==evt.x and p[1] <= evt.y <=p[1]+s['size']: if (s:=spl[loc]) and (pn:=pns[loc]) and not pn.fixed and (p:=s.pos)[0]==evt.x and p[1] <= evt.y <=p[1]+s.size:
self._selected.append(loc) self._selected.append(loc)
return True return True
@ -359,7 +410,9 @@ class TTkAppTemplate(TTkContainer):
if not self._selected: return False if not self._selected: return False
pns = self._panels pns = self._panels
for loc in self._selected: for loc in self._selected:
x,y,w,h = (p:=pns[loc]).geometry() if not (p:=pns[loc]):
raise ValueError()
x,y,w,h = p.geometry()
if loc == TTkAppTemplate.LEFT: if loc == TTkAppTemplate.LEFT:
p.size = evt.x-x p.size = evt.x-x
elif loc == TTkAppTemplate.RIGHT: elif loc == TTkAppTemplate.RIGHT:
@ -372,6 +425,11 @@ class TTkAppTemplate(TTkContainer):
return True return True
def minimumWidth(self) -> int: def minimumWidth(self) -> int:
''' Get the minimum width required for the template
:return: The minimum width in characters
:rtype: int
'''
pns = self._panels pns = self._panels
# Header and Footer sizes # Header and Footer sizes
@ -389,11 +447,18 @@ class TTkAppTemplate(TTkContainer):
if (p:=pns[TTkAppTemplate.TOP ]) and p.isVisible(): mct = p.minimumWidth() if (p:=pns[TTkAppTemplate.TOP ]) and p.isVisible(): mct = p.minimumWidth()
if (p:=pns[TTkAppTemplate.BOTTOM]) and p.isVisible(): mcb = p.minimumWidth() if (p:=pns[TTkAppTemplate.BOTTOM]) and p.isVisible(): mcb = p.minimumWidth()
mcm = (p:=pns[TTkAppTemplate.MAIN]).minimumWidth() if not (p:=pns[TTkAppTemplate.MAIN]):
raise ValueError()
mcm = p.minimumWidth()
return max(mh, mf, mcr+mcl+max(mct, mcb, mcm)) + (2 if p.border else 0) return max(mh, mf, mcr+mcl+max(mct, mcb, mcm)) + (2 if p.border else 0)
def maximumWidth(self) -> int: def maximumWidth(self) -> int:
''' Get the maximum width allowed for the template
:return: The maximum width in characters
:rtype: int
'''
pns = self._panels pns = self._panels
# Header and Footer sizes # Header and Footer sizes
@ -411,11 +476,17 @@ class TTkAppTemplate(TTkContainer):
if (p:=pns[TTkAppTemplate.TOP ]) and p.isVisible(): mct = p.maximumWidth() if (p:=pns[TTkAppTemplate.TOP ]) and p.isVisible(): mct = p.maximumWidth()
if (p:=pns[TTkAppTemplate.BOTTOM]) and p.isVisible(): mcb = p.maximumWidth() if (p:=pns[TTkAppTemplate.BOTTOM]) and p.isVisible(): mcb = p.maximumWidth()
mcm = (p:=pns[TTkAppTemplate.MAIN]).maximumWidth() if not (p:=pns[TTkAppTemplate.MAIN]):
raise ValueError()
mcm = p.maximumWidth()
return min(mh, mf, mcr+mcl+min(mct, mcb, mcm)) + (2 if p.border else 0) return min(mh, mf, mcr+mcl+min(mct, mcb, mcm)) + (2 if p.border else 0)
def minimumHeight(self) -> int: def minimumHeight(self) -> int:
''' Get the minimum height required for the template
:return: The minimum height in characters
:rtype: int
'''
pns = self._panels pns = self._panels
# Retrieve all the panels parameters and hide the menubar if required # Retrieve all the panels parameters and hide the menubar if required
@ -445,6 +516,11 @@ class TTkAppTemplate(TTkContainer):
return mh+mf+max(mr ,ml, mm+mt+mb ) + ( 2 if bm else 0 ) return mh+mf+max(mr ,ml, mm+mt+mb ) + ( 2 if bm else 0 )
def maximumHeight(self) -> int: def maximumHeight(self) -> int:
''' Get the maximum height allowed for the template
:return: The maximum height in characters
:rtype: int
'''
pns = self._panels pns = self._panels
# Retrieve all the panels parameters and hide the menubar if required # Retrieve all the panels parameters and hide the menubar if required
@ -507,7 +583,8 @@ class TTkAppTemplate(TTkContainer):
pr,prmin,prmax,sr,fr,br,mr = _processPanel(TTkAppTemplate.RIGHT) pr,prmin,prmax,sr,fr,br,mr = _processPanel(TTkAppTemplate.RIGHT)
# Main Boundaries # Main Boundaries
pm=pns[TTkAppTemplate.MAIN] if not (pm:=pns[TTkAppTemplate.MAIN]):
raise ValueError()
mm=pm.menubar mm=pm.menubar
mmaxw = pm.maximumWidth() mmaxw = pm.maximumWidth()
mminw = pm.minimumWidth() mminw = pm.minimumWidth()
@ -522,13 +599,13 @@ class TTkAppTemplate(TTkContainer):
# Retune the max/min sizes and adjustment based on the menubar,border and visible widgets # Retune the max/min sizes and adjustment based on the menubar,border and visible widgets
# Check if there is a splitter to be used for the menubar # Check if there is a splitter to be used for the menubar
# Fix bar status if the menu is on the closest splitter # Fix bar status if the menu is on the closest splitter
if pt and mt: adjt,adjtf = ( 0, fh if _phbh else True ) if (_phbh:=(ph and bh)) or (not ph and bm) else (1,True) ; st+=adjt ; ptmin+=adjt ; ptmax+=adjt if pt and mt: (adjt,adjtf) = ( 0, fh if _phbh else True ) if (_phbh:=(ph and bh)) or (not ph and bm) else (1,True) ; st+=adjt ; ptmin+=adjt ; ptmax+=adjt
if pb and mb: adjb,adjbf = ( 0, fb ) if bb else (1,True) ; sb+=adjb ; pbmin+=adjb ; pbmax+=adjb if pb and mb: (adjb,adjbf) = ( 0, fb ) if bb else (1,True) ; sb+=adjb ; pbmin+=adjb ; pbmax+=adjb
if ph and mh: adjh,adjhf = ( 0, 0 ) if bm else (1,True) ; sh+=adjh ; phmin+=adjh ; phmax+=adjh if ph and mh: (adjh,adjhf) = ( 0, 0 ) if bm else (1,True) ; sh+=adjh ; phmin+=adjh ; phmax+=adjh
if pf and mf: adjf,adjff = ( 0, ff ) if bf else (1,True) ; sf+=adjf ; pfmin+=adjf ; pfmax+=adjf if pf and mf: (adjf,adjff) = ( 0, ff ) if bf else (1,True) ; sf+=adjf ; pfmin+=adjf ; pfmax+=adjf
if pl and ml: adjl,adjlf = ( 0, fh if _phbh else True ) if (_phbh:=(ph and bh)) or (not ph and bm) else (1,True) ; plmin+=adjl ; plmax+=adjl if pl and ml: (adjl,adjlf) = ( 0, fh if _phbh else True ) if (_phbh:=(ph and bh)) or (not ph and bm) else (1,True) ; plmin+=adjl ; plmax+=adjl
if pr and mr: adjr,adjrf = ( 0, fh if _phbh else True ) if (_phbh:=(ph and bh)) or (not ph and bm) else (1,True) ; prmin+=adjr ; prmax+=adjr if pr and mr: (adjr,adjrf) = ( 0, fh if _phbh else True ) if (_phbh:=(ph and bh)) or (not ph and bm) else (1,True) ; prmin+=adjr ; prmax+=adjr
if mm: adjm,adjmf = ( 0, ft if (pt and bt) else fh if _phbh else True) if (_phbh:=(ph and bh)) or (not pt and ph and bh) or (not pt and not ph and bm) else (1,True) ; mminh+=adjm ; mmaxh+=adjm if mm: (adjm,adjmf) = ( 0, ft if (pt and bt) else fh if _phbh else True) if (_phbh:=(ph and bh)) or (not pt and ph and bh) or (not pt and not ph and bm) else (1,True) ; mminh+=adjm ; mmaxh+=adjm
# check horizontal sizes # check horizontal sizes
if not (mminw <= (newszw:=(w-sl-sr)) <= mmaxw): if not (mminw <= (newszw:=(w-sl-sr)) <= mmaxw):
@ -562,7 +639,13 @@ class TTkAppTemplate(TTkContainer):
# Resize any panel to the proper dimension # Resize any panel to the proper dimension
w+=bl+br w+=bl+br
h+=bt+bb+bh+bf h+=bt+bb+bh+bf
def _setGeometries(_loc, _p, _x,_y,_w,_h,_mb,_adj,_fix): def _setGeometries(
_loc:TTkAppTemplate.Position,
_p:TTkAppTemplate._Panel,
_x:int,_y:int,_w:int,_h:int,
_mb:Optional[TTkMenuBarLayout],
_adj:int,
_fix:int) -> None:
if _mb: if _mb:
if _fix: # Fixed if _fix: # Fixed
styleToMerge = {'default':{'glyphs':('','','','','','')}} styleToMerge = {'default':{'glyphs':('','','','','','')}}
@ -571,7 +654,7 @@ class TTkAppTemplate(TTkContainer):
if not _adj: if not _adj:
mbl[_loc] = None mbl[_loc] = None
else: else:
mbl[_loc] = {'pos':(_x,_y),'text':f"{''*(_w-2)}"} mbl[_loc] = TTkAppTemplate._MenuBarLine(pos=(_x,_y), text=f"{''*(_w-2)}")
for m in _mb._menus(TTkK.LEFT_ALIGN): m.mergeStyle(styleToMerge) for m in _mb._menus(TTkK.LEFT_ALIGN): m.mergeStyle(styleToMerge)
for m in _mb._menus(TTkK.RIGHT_ALIGN): m.mergeStyle(styleToMerge) for m in _mb._menus(TTkK.RIGHT_ALIGN): m.mergeStyle(styleToMerge)
for m in _mb._menus(TTkK.CENTER_ALIGN): m.mergeStyle(styleToMerge) for m in _mb._menus(TTkK.CENTER_ALIGN): m.mergeStyle(styleToMerge)
@ -590,18 +673,18 @@ class TTkAppTemplate(TTkContainer):
# Define Splitter geometries # Define Splitter geometries
w,h = self.size() w,h = self.size()
spl[TTkAppTemplate.HEADER] = None if not bh else {'pos':(0 , bm+sh ) ,'size':w , 'fixed':fh , 'panel': ph } spl[TTkAppTemplate.HEADER] = None if not bh else TTkAppTemplate._Splitter( pos=(0 , bm+sh ) ,size=w , fixed=fh , panel=ph )
spl[TTkAppTemplate.FOOTER] = None if not bf else {'pos':(0 , bm+sh+bh+st+bt+newszh+bb+sb) ,'size':w , 'fixed':ff , 'panel': pf } spl[TTkAppTemplate.FOOTER] = None if not bf else TTkAppTemplate._Splitter( pos=(0 , bm+sh+bh+st+bt+newszh+bb+sb) ,size=w , fixed=ff , panel=pf )
ca = sh + (bm if ph else 0 ) ca = sh + (bm if ph else 0 )
cb = bm+sh+bh+st+bt+newszh+bb+sb + (bf if pf else bm) cb = bm+sh+bh+st+bt+newszh+bb+sb + (bf if pf else bm)
spl[TTkAppTemplate.LEFT] = None if not bl else {'pos':(bm+sl , ca ) ,'size':cb-ca , 'fixed':fl , 'panel': pl } spl[TTkAppTemplate.LEFT] = None if not bl else TTkAppTemplate._Splitter( pos=(bm+sl , ca ) ,size=cb-ca , fixed=fl , panel=pl )
spl[TTkAppTemplate.RIGHT] = None if not br else {'pos':(bm+sl+bl+newszw , ca ) ,'size':cb-ca , 'fixed':fr , 'panel': pr } spl[TTkAppTemplate.RIGHT] = None if not br else TTkAppTemplate._Splitter( pos=(bm+sl+bl+newszw , ca ) ,size=cb-ca , fixed=fr , panel=pr )
ca = sl + (bm if pl else 0 ) ca = sl + (bm if pl else 0 )
cb = bm+sl+bl+newszw + (br if pr else bm) cb = bm+sl+bl+newszw + (br if pr else bm)
spl[TTkAppTemplate.TOP] = None if not bt else {'pos':(ca , bm+sh+bh+st ) ,'size':cb-ca , 'fixed':ft , 'panel': pt } spl[TTkAppTemplate.TOP] = None if not bt else TTkAppTemplate._Splitter( pos=(ca , bm+sh+bh+st ) ,size=cb-ca , fixed=ft , panel=pt )
spl[TTkAppTemplate.BOTTOM] = None if not bb else {'pos':(ca , bm+sh+bh+st+bt+newszh) ,'size':cb-ca , 'fixed':fb , 'panel': pb } spl[TTkAppTemplate.BOTTOM] = None if not bb else TTkAppTemplate._Splitter( pos=(ca , bm+sh+bh+st+bt+newszh) ,size=cb-ca , fixed=fb , panel=pb )
self.update() self.update()
@ -616,13 +699,14 @@ class TTkAppTemplate(TTkContainer):
#def setLayout(self, layout): #def setLayout(self, layout):
# self._panels[TTkAppTemplate.MAIN].item = layout # self._panels[TTkAppTemplate.MAIN].item = layout
def paintEvent(self, canvas: TTkCanvas) -> None: def paintEvent(self, canvas:TTkCanvas) -> None:
w,h = self.size() w,h = self.size()
pns = self._panels pns = self._panels
spl = self._splitters spl = self._splitters
mbl = self._menubarLines mbl = self._menubarLines
if b:=pns[TTkAppTemplate.MAIN].border: b = False
if (_am:=pns[TTkAppTemplate.MAIN]) is not None and (b:=_am.border):
canvas.drawBox(pos=(0,0), size=(w,h)) canvas.drawBox(pos=(0,0), size=(w,h))
selectColor = TTkColor.fg('#88FF00') selectColor = TTkColor.fg('#88FF00')
@ -630,21 +714,21 @@ class TTkAppTemplate(TTkContainer):
# hline = ('╞','═','╡') # hline = ('╞','═','╡')
# vline = ('╥','║','╨') # vline = ('╥','║','╨')
def drawVLine(sp, color=TTkColor.RST): def drawVLine(sp:TTkAppTemplate._Splitter, color:TTkColor=TTkColor.RST) -> None:
_x,_y = sp['pos'] _x,_y = sp.pos
_w,_h = 1,sp['size'] _w,_h = 1,sp.size
chs = ('','','','','') if sp['fixed'] else ('','','','','') chs = ('','','','','') if sp.fixed else ('','','','','')
canvas.fill(pos=(_x,_y), size=(_w,_h), color=color, char=chs[0] ) canvas.fill(pos=(_x,_y), size=(_w,_h), color=color, char=chs[0] )
canvas.drawChar(pos=(_x,_y), color=color, char=chs[1]if b and _y==0 else chs[3]) canvas.drawChar(pos=(_x,_y), color=color, char=chs[1]if b and _y==0 else chs[3])
canvas.drawChar(pos=(_x,_y+_h-1), color=color, char=chs[2]if b and _y+_h==h else chs[4]) canvas.drawChar(pos=(_x,_y+_h-1), color=color, char=chs[2]if b and _y+_h==h else chs[4])
def drawHLine(sp, color=TTkColor.RST): def drawHLine(sp:TTkAppTemplate._Splitter, color:TTkColor=TTkColor.RST) -> None:
_x,_y = sp['pos'] _x,_y = sp.pos
_w,_h = sp['size'],1 _w,_h = sp.size,1
chs = ('','','','','') if sp['fixed'] else ('','','','','') chs = ('','','','','') if sp.fixed else ('','','','','')
canvas.fill(pos=(_x,_y), size=(_w,_h), color=color, char=chs[0] ) canvas.fill(pos=(_x,_y), size=(_w,_h), color=color, char=chs[0] )
canvas.drawChar(pos=(_x,_y), color=color, char=chs[1]if b and _x==0 else chs[3]) canvas.drawChar(pos=(_x,_y), color=color, char=chs[1]if b and _x==0 else chs[3])
canvas.drawChar(pos=(_x+_w-1,_y), color=color, char=chs[2]if b and _x+_w==w else chs[4]) canvas.drawChar(pos=(_x+_w-1,_y), color=color, char=chs[2]if b and _x+_w==w else chs[4])
if _title:=sp['panel'].title: if _title:=sp.panel.title:
_l = min(w-2,_title.termWidth()) _l = min(w-2,_title.termWidth())
_tx = (_w-_l)//2 _tx = (_w-_l)//2
canvas.drawChar(pos=(_x+_tx,_y), color=color, char=chs[2]) canvas.drawChar(pos=(_x+_tx,_y), color=color, char=chs[2])
@ -652,19 +736,19 @@ class TTkAppTemplate(TTkContainer):
canvas.drawTTkString(pos=(_x+_tx+1,_y),text=_title,width=_l) canvas.drawTTkString(pos=(_x+_tx+1,_y),text=_title,width=_l)
# Draw the 4 splittters # Draw the 4 splittters
if (sp:=spl[TTkAppTemplate.HEADER]) : drawHLine(sp, color=selectColor if self._selected and TTkAppTemplate.HEADER in self._selected else TTkColor.RST) if (sp:=spl[TTkAppTemplate.HEADER]) : drawHLine(sp, color=selectColor if TTkAppTemplate.HEADER in self._selected else TTkColor.RST)
if (sp:=spl[TTkAppTemplate.FOOTER]) : drawHLine(sp, color=selectColor if self._selected and TTkAppTemplate.FOOTER in self._selected else TTkColor.RST) if (sp:=spl[TTkAppTemplate.FOOTER]) : drawHLine(sp, color=selectColor if TTkAppTemplate.FOOTER in self._selected else TTkColor.RST)
if (sp:=spl[TTkAppTemplate.LEFT]) : drawVLine(sp, color=selectColor if self._selected and TTkAppTemplate.LEFT in self._selected else TTkColor.RST) if (sp:=spl[TTkAppTemplate.LEFT]) : drawVLine(sp, color=selectColor if TTkAppTemplate.LEFT in self._selected else TTkColor.RST)
if (sp:=spl[TTkAppTemplate.RIGHT]) : drawVLine(sp, color=selectColor if self._selected and TTkAppTemplate.RIGHT in self._selected else TTkColor.RST) if (sp:=spl[TTkAppTemplate.RIGHT]) : drawVLine(sp, color=selectColor if TTkAppTemplate.RIGHT in self._selected else TTkColor.RST)
if (sp:=spl[TTkAppTemplate.TOP]) : drawHLine(sp, color=selectColor if self._selected and TTkAppTemplate.TOP in self._selected else TTkColor.RST) if (sp:=spl[TTkAppTemplate.TOP]) : drawHLine(sp, color=selectColor if TTkAppTemplate.TOP in self._selected else TTkColor.RST)
if (sp:=spl[TTkAppTemplate.BOTTOM]) : drawHLine(sp, color=selectColor if self._selected and TTkAppTemplate.BOTTOM in self._selected else TTkColor.RST) if (sp:=spl[TTkAppTemplate.BOTTOM]) : drawHLine(sp, color=selectColor if TTkAppTemplate.BOTTOM in self._selected else TTkColor.RST)
# Draw the 12 intersect # Draw the 12 intersect
def drawIntersect(sph,spv,chs): def drawIntersect(sph:Optional[TTkAppTemplate._Splitter],spv:Optional[TTkAppTemplate._Splitter],chs:Tuple[str,str,str,str]) -> None:
if sph and spv: if sph and spv:
x = spv['pos'][0] x = spv.pos[0]
y = sph['pos'][1] y = sph.pos[1]
ch = chs[( 0 if sph['fixed'] else 0x01 ) | ( 0 if spv['fixed'] else 0x02 )] ch = chs[( 0 if sph.fixed else 0x01 ) | ( 0 if spv.fixed else 0x02 )]
canvas.drawChar(pos=(x,y), char=ch) canvas.drawChar(pos=(x,y), char=ch)
drawIntersect(spl[TTkAppTemplate.HEADER], spl[TTkAppTemplate.LEFT] , ('','','','')) drawIntersect(spl[TTkAppTemplate.HEADER], spl[TTkAppTemplate.LEFT] , ('','','',''))
@ -679,6 +763,6 @@ class TTkAppTemplate(TTkContainer):
# Draw extra MenuBar Lines if there is no border to place them # Draw extra MenuBar Lines if there is no border to place them
for l in mbl: for l in mbl:
if mb:=mbl[l]: if mb:=mbl[l]:
canvas.drawText(pos=mb['pos'],text=mb['text']) canvas.drawText(pos=mb.pos,text=mb.text)
return super().paintEvent(canvas) return super().paintEvent(canvas)

354
libs/pyTermTk/TermTk/TTkWidgets/splitter.py

@ -22,10 +22,13 @@
__all__ = ['TTkSplitter'] __all__ = ['TTkSplitter']
from typing import Union,List,Optional
from TermTk.TTkCore.constant import TTkK from TermTk.TTkCore.constant import TTkK
from TermTk.TTkCore.cfg import TTkCfg from TermTk.TTkCore.cfg import TTkCfg
from TermTk.TTkCore.canvas import TTkCanvas
from TermTk.TTkCore.color import TTkColor from TermTk.TTkCore.color import TTkColor
from TermTk.TTkCore.string import TTkString from TermTk.TTkCore.string import TTkString, TTkStringType
from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent
from TermTk.TTkLayouts.layout import TTkLayout from TermTk.TTkLayouts.layout import TTkLayout
@ -33,7 +36,46 @@ from TermTk.TTkWidgets.widget import TTkWidget
from TermTk.TTkWidgets.container import TTkContainer from TermTk.TTkWidgets.container import TTkContainer
class TTkSplitter(TTkContainer): class TTkSplitter(TTkContainer):
'''TTkSplitter''' '''TTkSplitter:
A container widget that arranges child widgets with adjustable splitter bars.
::
Horizontal Splitter:
Widget1 Widget2 Widget3
Vertical Splitter:
Widget 1
Widget 2
Widget 3
The splitter allows users to redistribute space between child widgets by dragging
the splitter bars. Widgets can have fixed or proportional sizes.
Demo: `splitter.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/demo/showcase/splitter.py>`_
(`online <https://ceccopierangiolieugenio.github.io/pyTermTk-Docs/sandbox/sandbox.html?filePath=demo/showcase/splitter.py>`__)
.. code-block:: python
import TermTk as ttk
root = ttk.TTk(layout=ttk.TTkGridLayout())
splitter = ttk.TTkSplitter(parent=root, orientation=ttk.TTkK.HORIZONTAL)
splitter.addWidget(ttk.TTkTestWidgetSizes(border=True), size=20)
splitter.addWidget(ttk.TTkTestWidgetSizes(border=True))
splitter.addWidget(ttk.TTkTestWidgetSizes(border=True), size=30)
root.mainloop()
'''
classStyle = { classStyle = {
'default': {'glyphs' : { 'default': {'glyphs' : {
@ -51,10 +93,23 @@ class TTkSplitter(TTkContainer):
'_orientation', '_separators', '_refSizes', '_orientation', '_separators', '_refSizes',
'_items', '_titles', '_separatorSelected', '_items', '_titles', '_separatorSelected',
'_border') '_border')
_items:List[Union[TTkWidget,TTkLayout]]
_titles:List[Optional[TTkString]]
_separators:List[int]
_refSizes:List[Optional[int]]
'''Reference sizes for each widget in the splitter'''
_separatorSelected:Optional[int]
def __init__(self, *, def __init__(self, *,
border:bool=False, border:bool=False,
orientation:TTkK.Direction=TTkK.HORIZONTAL, orientation:TTkK.Direction=TTkK.HORIZONTAL,
**kwargs) -> None: **kwargs) -> None:
''' Initialize the splitter
:param border: Draw a border around the splitter, defaults to False
:type border: bool, optional
:param orientation: Splitter orientation (:py:class:`TTkK.Direction.HORIZONTAL` or :py:class:`TTkK.Direction.VERTICAL`), defaults to :py:class:`TTkK.Direction.HORIZONTAL`
:type orientation: :py:class:`TTkK.Direction`, optional
'''
self._items = [] self._items = []
self._titles = [] self._titles = []
self._separators = [] self._separators = []
@ -77,61 +132,113 @@ class TTkSplitter(TTkContainer):
self.addItem(item) self.addItem(item)
self.setLayout(_SplitterLayout()) self.setLayout(_SplitterLayout())
def setBorder(self, border): def setBorder(self, border:bool) -> None:
'''setBorder''' ''' Set whether to draw a border around the splitter
:param border: True to show border, False to hide
:type border: bool
'''
self._border = border self._border = border
if border: self.setPadding(1,1,1,1) if border: self.setPadding(1,1,1,1)
else: self.setPadding(0,0,0,0) else: self.setPadding(0,0,0,0)
self.update() self.update()
def border(self): def border(self) -> bool:
'''border''' ''' Get the current border state
:return: True if border is visible, False otherwise
:rtype: bool
'''
return self._border return self._border
def orientation(self): def orientation(self) -> TTkK.Direction:
'''orientation''' ''' Get the current splitter orientation
:return: The orientation (HORIZONTAL or VERTICAL)
:rtype: :py:class:`TTkK.Direction`
'''
return self._orientation return self._orientation
def setOrientation(self, orientation): def setOrientation(self, orientation:TTkK.Direction) -> None:
''' Set the splitter orientation
:param orientation: The new orientation (HORIZONTAL or VERTICAL)
:type orientation: :py:class:`TTkK.Direction`
'''
if orientation == self._orientation: return if orientation == self._orientation: return
if orientation not in (TTkK.HORIZONTAL, TTkK.VERTICAL): return if orientation not in (TTkK.HORIZONTAL, TTkK.VERTICAL): return
self._orientation = orientation self._orientation = orientation
self._updateGeometries() self._updateGeometries()
def clean(self): def clean(self) -> None:
for i in reversed(self._items): ''' Remove all widgets and items from the splitter '''
if issubclass(type(i),TTkWidget): for _i in reversed(self._items):
self.removeWidget(i) if isinstance(_i,TTkWidget):
self.removeWidget(_i)
else: else:
self.removeItem(i) self.removeItem(_i)
def count(self) -> int:
''' Get the number of items in the splitter
def count(self): :return: The count of widgets/items
'''count''' :rtype: int
'''
return len(self._items) return len(self._items)
def indexOf(self, widget): def indexOf(self, widget:Union[TTkWidget,TTkLayout]) -> int:
'''indexOf''' ''' Get the index of a widget or layout in the splitter
:param widget: The widget or layout to find
:type widget: :py:class:`TTkWidget` or :py:class:`TTkLayout`
:return: The index of the item
:rtype: int
'''
return self._items.index(widget) return self._items.index(widget)
def widget(self, index): def widget(self, index:int) -> Union[TTkWidget,TTkLayout]:
'''widget''' ''' Get the widget or layout at the specified index
:param index: The index of the item
:type index: int
:return: The widget or layout at the index
:rtype: :py:class:`TTkWidget` or :py:class:`TTkLayout`
'''
return self._items[index] return self._items[index]
def replaceItem(self, index, item, title=None): def replaceItem(self, index:int, item:TTkLayout, title:Optional[TTkStringType]=None) -> None:
'''replaceItem''' ''' Replace the layout at the specified index
:param index: The index to replace at
:type index: int
:param item: The new layout
:type item: :py:class:`TTkLayout`
:param title: Optional title for the item, defaults to None
:type title: str, :py:class:`TTkString`, optional
'''
if index >= len(self._items): if index >= len(self._items):
return self.addItem(item, title=title) return self.addItem(item, title=title)
TTkLayout.removeItem(self.layout(), self._items[index]) TTkLayout.removeItem(self.layout(), self._items[index])
TTkLayout.insertItem(self.layout(), index, item) TTkLayout.insertItem(self.layout(), index, item)
self._items[index] = item self._items[index] = item
self._titles[index] = TTkString(title) if title else None self._titles[index] = title if isinstance(title,TTkString) else TTkString(title) if isinstance(title,str) else TTkString()
w,h = self.size() w,h = self.size()
b = 2 if self._border else 0 b = 2 if self._border else 0
self._processRefSizes(w-b,h-b) self._processRefSizes(w-b,h-b)
self._updateGeometries() self._updateGeometries()
def replaceWidget(self, index, widget, title=None): def replaceWidget(self, index:int, widget:TTkWidget, title:Optional[str]=None) -> None:
'''replaceWidget''' ''' Replace the widget at the specified index
:param index: The index to replace at
:type index: int
:param widget: The new widget
:type widget: :py:class:`TTkWidget`
:param title: Optional title for the widget, defaults to None
:type title: str, :py:class:`TTkString`, optional
'''
if index >= len(self._items): if index >= len(self._items):
return self.addWidget(widget, title=title) return self.addWidget(widget, title=title)
TTkLayout.removeWidget(self.layout(), self._items[index]) TTkLayout.removeWidget(self.layout(), self._items[index])
@ -143,8 +250,12 @@ class TTkSplitter(TTkContainer):
self._processRefSizes(w-b,h-b) self._processRefSizes(w-b,h-b)
self._updateGeometries() self._updateGeometries()
def removeItem(self, item): def removeItem(self, item:TTkLayout) -> None:
'''removeItem''' ''' Remove a layout from the splitter
:param item: The layout to remove
:type item: :py:class:`TTkLayout`
'''
index = self.indexOf(item) index = self.indexOf(item)
self._items.pop(index) self._items.pop(index)
self._refSizes.pop(index) self._refSizes.pop(index)
@ -155,8 +266,12 @@ class TTkSplitter(TTkContainer):
self._processRefSizes(w-b,h-b) self._processRefSizes(w-b,h-b)
self._updateGeometries() self._updateGeometries()
def removeWidget(self, widget): def removeWidget(self, widget:TTkWidget) -> None:
'''removeWidget''' ''' Remove a widget from the splitter
:param widget: The widget to remove
:type widget: :py:class:`TTkWidget`
'''
index = self.indexOf(widget) index = self.indexOf(widget)
self._items.pop(index) self._items.pop(index)
self._refSizes.pop(index) self._refSizes.pop(index)
@ -167,27 +282,74 @@ class TTkSplitter(TTkContainer):
self._processRefSizes(w-b,h-b) self._processRefSizes(w-b,h-b)
self._updateGeometries() self._updateGeometries()
def addItem(self, item, size=None, title=None): def addItem(self, item:TTkLayout, size:Optional[int]=None, title:Optional[TTkStringType]=None) -> None:
'''addItem''' ''' Add a layout to the end of the splitter
:param item: The layout to add
:type item: :py:class:`TTkLayout`
:param size: Fixed size for the item in characters, defaults to None (proportional)
:type size: int, optional
:param title: Optional title for the item, defaults to None
:type title: str, :py:class:`TTkString`, optional
'''
self.insertItem(len(self._items), item, size=size, title=title) self.insertItem(len(self._items), item, size=size, title=title)
def insertItem(self, index, item, size=None, title=None): def insertItem(self, index:int, item:TTkLayout, size:Optional[int]=None, title:Optional[TTkStringType]=None) -> None:
'''insertItem''' ''' Insert a layout at the specified index
:param index: The index to insert at
:type index: int
:param item: The layout to insert
:type item: :py:class:`TTkLayout`
:param size: Fixed size for the item in characters, defaults to None (proportional)
:type size: int, optional
:param title: Optional title for the item, defaults to None
:type title: str, :py:class:`TTkString`, optional
'''
TTkLayout.insertItem(self.layout(), index, item) TTkLayout.insertItem(self.layout(), index, item)
self._insertWidgetItem(index, item, size=size, title=title) self._insertWidgetItem(index, item, size=size, title=title)
def addWidget(self, widget, size=None, title=None): def addWidget(self, widget:TTkWidget, size:Optional[int]=None, title:Optional[TTkStringType]=None) -> None:
'''addWidget''' ''' Add a widget to the end of the splitter
:param widget: The widget to add
:type widget: :py:class:`TTkWidget`
:param size: Fixed size for the widget in characters, defaults to None (proportional)
:type size: int, optional
:param title: Optional title for the widget, defaults to None
:type title: str, :py:class:`TTkString`, optional
'''
self.insertWidget(len(self._items), widget, size=size, title=title) self.insertWidget(len(self._items), widget, size=size, title=title)
def insertWidget(self, index, widget, size=None, title=None): def insertWidget(self, index:int, widget:TTkWidget, size:Optional[int]=None, title:Optional[TTkStringType]=None) -> None:
'''insertWidget''' ''' Insert a widget at the specified index
:param index: The index to insert at
:type index: int
:param widget: The widget to insert
:type widget: :py:class:`TTkWidget`
:param size: Fixed size for the widget in characters, defaults to None (proportional)
:type size: int, optional
:param title: Optional title for the widget, defaults to None
:type title: str, :py:class:`TTkString`, optional
'''
TTkLayout.insertWidget(self.layout(), index, widget) TTkLayout.insertWidget(self.layout(), index, widget)
self._insertWidgetItem(index, widget, size=size, title=title) self._insertWidgetItem(index, widget, size=size, title=title)
def _insertWidgetItem(self, index, widgetItem, size=None, title=None): def _insertWidgetItem(self, index:int, widgetItem:Union[TTkWidget,TTkLayout], size:Optional[int]=None, title:Optional[TTkStringType]=None) -> None:
''' Internal method to insert a widget or layout item
:param index: The index to insert at
:type index: int
:param widgetItem: The widget or layout to insert
:type widgetItem: :py:class:`TTkWidget` or :py:class:`TTkLayout`
:param size: Fixed size for the item, defaults to None
:type size: int, optional
:param title: Optional title, defaults to None
:type title: str, :py:class:`TTkString`, optional
'''
self._items.insert(index, widgetItem) self._items.insert(index, widgetItem)
self._titles.insert(index, TTkString(title) if title else None) self._titles.insert(index, title if isinstance(title,TTkString) else TTkString(title) if isinstance(title,str) else None)
# assign the same slice to all the widgets # assign the same slice to all the widgets
self._refSizes.insert(index, size) self._refSizes.insert(index, size)
@ -198,8 +360,12 @@ class TTkSplitter(TTkContainer):
if self.parentWidget(): if self.parentWidget():
self.parentWidget().update(repaint=True, updateLayout=True) self.parentWidget().update(repaint=True, updateLayout=True)
def setSizes(self, sizes): def setSizes(self, sizes:List[Optional[int]]) -> None:
'''setSizes''' ''' Set the sizes for all widgets in the splitter
:param sizes: List of sizes in characters (None for proportional sizing)
:type sizes: list of int or None
'''
ls = len(self._separators) ls = len(self._separators)
sizes=sizes[:ls]+[None]*max(0,ls-len(sizes)) sizes=sizes[:ls]+[None]*max(0,ls-len(sizes))
self._refSizes = sizes.copy() self._refSizes = sizes.copy()
@ -208,8 +374,15 @@ class TTkSplitter(TTkContainer):
self._processRefSizes(w-b,h-b) self._processRefSizes(w-b,h-b)
self._updateGeometries() self._updateGeometries()
def _minMaxSizeBefore(self, index:int) -> tuple[int,int]:
''' Calculate minimum and maximum sizes before the selected separator
def _minMaxSizeBefore(self, index): :param index: The separator index
:type index: int
:return: Tuple of (minimum_size, maximum_size)
:rtype: tuple[int,int]
'''
if self._separatorSelected is None: if self._separatorSelected is None:
return 0, 0x1000 return 0, 0x1000
# this is because there is a hidden splitter at position -1 # this is because there is a hidden splitter at position -1
@ -221,7 +394,15 @@ class TTkSplitter(TTkContainer):
maxsize += item.maxDimension(self._orientation)+1 maxsize += item.maxDimension(self._orientation)+1
return minsize, maxsize return minsize, maxsize
def _minMaxSizeAfter(self, index): def _minMaxSizeAfter(self, index:int) -> tuple[int,int]:
''' Calculate minimum and maximum sizes after the selected separator
:param index: The separator index
:type index: int
:return: Tuple of (minimum_size, maximum_size)
:rtype: tuple[int,int]
'''
if self._separatorSelected is None: if self._separatorSelected is None:
return 0, 0x1000 return 0, 0x1000
minsize = 0x0 minsize = 0x0
@ -232,7 +413,12 @@ class TTkSplitter(TTkContainer):
maxsize += item.maxDimension(self._orientation)+1 maxsize += item.maxDimension(self._orientation)+1
return minsize, maxsize return minsize, maxsize
def _updateGeometries(self, resized=False): def _updateGeometries(self, resized:bool=False) -> None:
''' Internal method to update widget geometries based on splitter positions
:param resized: True if called from resize event, defaults to False
:type resized: bool, optional
'''
if not self.isVisible() or not self._items: return if not self.isVisible() or not self._items: return
w,h = self.size() w,h = self.size()
if w==h==0: return if w==h==0: return
@ -301,11 +487,24 @@ class TTkSplitter(TTkContainer):
_processGeometry(i, True) _processGeometry(i, True)
if self._separatorSelected is not None: if self._separatorSelected is not None:
s = [ b-a for a,b in zip([0]+self._separators,self._separators)] s:List[Optional[int]] = [ b-a for a,b in zip([0]+self._separators,self._separators)]
self._refSizes = s self._refSizes = s
self.update() self.update()
def _processRefSizes(self, w, h): def _processRefSizes(self, w:int, h:int) -> None:
''' Process reference sizes and calculate separator positions
This method handles both fixed and proportional widget sizing:
- When :py:attr:`_refSizes` contains None values, remaining space is distributed proportionally
- When all sizes are fixed, they are scaled to fit the available space
- The last widget always receives any remaining space to prevent rounding errors
:param w: Available width
:type w: int
:param h: Available height
:type h: int
'''
self._separatorSelected = None self._separatorSelected = None
if self._orientation == TTkK.HORIZONTAL: if self._orientation == TTkK.HORIZONTAL:
sizeRef = w sizeRef = w
@ -321,15 +520,17 @@ class TTkSplitter(TTkContainer):
numVarSizes = len([x for x in self._refSizes if x is None]) numVarSizes = len([x for x in self._refSizes if x is None])
avalSize = sizeRef-fixSize avalSize = sizeRef-fixSize
varSize = avalSize//numVarSizes varSize = avalSize//numVarSizes
sizes = [] sizes:List[int] = []
for s in self._refSizes: for s in self._refSizes:
if not s: if s is None:
avalSize -= varSize avalSize -= varSize
s = varSize + avalSize if avalSize<varSize else 0 newSize = varSize + avalSize if avalSize<varSize else 0
sizes.append(s) sizes.append(newSize)
else:
sizes.append(s)
sizes = [varSize if s is None else s for s in self._refSizes] sizes = [varSize if s is None else s for s in self._refSizes]
else: else:
sizes = self._refSizes sizes = [s for s in self._refSizes if s is not None]
sizeRef = sum(sizes) sizeRef = sum(sizes)
self._separators = [sum(sizes[:i+1]) for i in range(len(sizes))] self._separators = [sum(sizes[:i+1]) for i in range(len(sizes))]
@ -341,7 +542,18 @@ class TTkSplitter(TTkContainer):
diff = h/sizeRef diff = h/sizeRef
self._separators = [int(i*diff) for i in self._separators] self._separators = [int(i*diff) for i in self._separators]
def resizeEvent(self, w, h): def resizeEvent(self, w:int, h:int) -> None:
''' Handle resize events and update widget geometries
This method recalculates all separator positions and widget sizes
when the splitter is resized, maintaining the proportional or fixed
size relationships defined by :py:meth:`setSizes` or drag operations.
:param w: New width
:type w: int
:param h: New height
:type h: int
'''
b = 2 if self._border else 0 b = 2 if self._border else 0
self._processRefSizes(w-b,h-b) self._processRefSizes(w-b,h-b)
self._updateGeometries(resized=True) self._updateGeometries(resized=True)
@ -380,6 +592,14 @@ class TTkSplitter(TTkContainer):
self._separatorSelected = None self._separatorSelected = None
def minimumHeight(self) -> int: def minimumHeight(self) -> int:
''' Get the minimum height required for the splitter
For vertical splitters, returns the sum of all child minimum heights plus separators.
For horizontal splitters, returns the maximum child minimum height.
:return: The minimum height in characters
:rtype: int
'''
ret = b = 2 if self._border else 0 ret = b = 2 if self._border else 0
if not self._items: return ret if not self._items: return ret
if self._orientation == TTkK.VERTICAL: if self._orientation == TTkK.VERTICAL:
@ -391,7 +611,15 @@ class TTkSplitter(TTkContainer):
ret = max(ret,item.minimumHeight()+b) ret = max(ret,item.minimumHeight()+b)
return ret return ret
def minimumWidth(self) -> int: def minimumWidth(self) -> int:
''' Get the minimum width required for the splitter
For horizontal splitters, returns the sum of all child minimum widths plus separators.
For vertical splitters, returns the maximum child minimum width.
:return: The minimum width in characters
:rtype: int
'''
ret = b = 2 if self._border else 0 ret = b = 2 if self._border else 0
if not self._items: return ret if not self._items: return ret
if self._orientation == TTkK.HORIZONTAL: if self._orientation == TTkK.HORIZONTAL:
@ -404,6 +632,14 @@ class TTkSplitter(TTkContainer):
return ret return ret
def maximumHeight(self) -> int: def maximumHeight(self) -> int:
''' Get the maximum height allowed for the splitter
For vertical splitters, returns the sum of all child maximum heights plus separators.
For horizontal splitters, returns the minimum child maximum height.
:return: The maximum height in characters
:rtype: int
'''
b = 2 if self._border else 0 b = 2 if self._border else 0
if not self._items: return 0x10000 if not self._items: return 0x10000
if self._orientation == TTkK.VERTICAL: if self._orientation == TTkK.VERTICAL:
@ -417,7 +653,15 @@ class TTkSplitter(TTkContainer):
ret = min(ret,item.maximumHeight()+b) ret = min(ret,item.maximumHeight()+b)
return ret return ret
def maximumWidth(self) -> int: def maximumWidth(self) -> int:
''' Get the maximum width allowed for the splitter
For horizontal splitters, returns the sum of all child maximum widths plus separators.
For vertical splitters, returns the minimum child maximum width.
:return: The maximum width in characters
:rtype: int
'''
b = 2 if self._border else 0 b = 2 if self._border else 0
if not self._items: return 0x10000 if not self._items: return 0x10000
if self._orientation == TTkK.HORIZONTAL: if self._orientation == TTkK.HORIZONTAL:
@ -431,7 +675,7 @@ class TTkSplitter(TTkContainer):
ret = min(ret,item.maximumWidth()+b) ret = min(ret,item.maximumWidth()+b)
return ret return ret
def paintEvent(self, canvas): def paintEvent(self, canvas:TTkCanvas) -> None:
style = self.currentStyle() style = self.currentStyle()
color = style['color'] color = style['color']
borderColor = style['borderColor'] borderColor = style['borderColor']

133
tests/timeit/34.dataclasses.py

@ -0,0 +1,133 @@
#!/usr/bin/env python3
# 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
import sys, os
from dataclasses import dataclass
from enum import Enum,Flag,auto
import timeit
from typing import List, Tuple, Iterator
class _C():
a:int
b:int
c:int
def __init__(self, a:int,b:int,c:int):
self.a = a
self.b = b
self.c = c
class _CS():
__slots__ = ('a','b','c')
a:int
b:int
c:int
def __init__(self, a:int,b:int,c:int):
self.a = a
self.b = b
self.c = c
@dataclass
class _DC1():
a:int
b:int
c:int
@dataclass
class _DC1S():
__slots__ = ('a','b','c')
a:int
b:int
c:int
@dataclass()
class _DC2():
a:int
b:int
c:int
@dataclass()
class _DC2S():
__slots__ = ('a','b','c')
a:int
b:int
c:int
@dataclass(frozen=True)
class _DC3():
a:int
b:int
c:int
@dataclass(frozen=True, slots=True)
class _DC3S():
a:int
b:int
c:int
t1 = [(i,i,i) for i in range(1000)]
d1 = [{'a':i,'b':i,'c':i} for i in range(1000)]
c = [_C(i,i,i) for i in range(1000)]
cs = [_CS(i,i,i) for i in range(1000)]
dc1 = [_DC1(i,i,i) for i in range(1000)]
dc1s = [_DC1S(i,i,i) for i in range(1000)]
dc2 = [_DC2(i,i,i) for i in range(1000)]
dc2s = [_DC2S(i,i,i) for i in range(1000)]
dc3 = [_DC3(i,i,i) for i in range(1000)]
dc3s = [_DC3S(i,i,i) for i in range(1000)]
def test_ti_1_Init_1(): return len([{'a':i,'b':i,'c':i} for i in range(100)])
def test_ti_1_Init_3(): return len([(i,i,i) for i in range(100)])
def test_ti_1_Init_4(): return len([_C(i,i,i) for i in range(100)])
def test_ti_1_Init_5(): return len([_CS(i,i,i) for i in range(100)])
def test_ti_1_Init_6_1(): return len([_DC1(i,i,i) for i in range(100)])
def test_ti_1_Init_6_2(): return len([_DC1S(i,i,i) for i in range(100)])
def test_ti_1_Init_7_1(): return len([_DC2(i,i,i) for i in range(100)])
def test_ti_1_Init_7_2(): return len([_DC2S(i,i,i) for i in range(100)])
def test_ti_1_Init_8_1(): return len([_DC3(i,i,i) for i in range(100)])
def test_ti_1_Init_8_2(): return len([_DC3S(i,i,i) for i in range(100)])
def test_ti_2_Access_1(): return sum(i['a']+i['b']+i['c'] for i in d1)
def test_ti_2_Access_2(): return sum(sum(i) for i in t1)
def test_ti_2_Access_3(): return sum(i[0]+i[1]+i[2] for i in t1)
def test_ti_2_Access_4(): return sum(i.a+i.b+i.c for i in c)
def test_ti_2_Access_5(): return sum(i.a+i.b+i.c for i in cs)
def test_ti_2_Access_6_1(): return sum(i.a+i.b+i.c for i in dc1)
def test_ti_2_Access_6_2(): return sum(i.a+i.b+i.c for i in dc1s)
def test_ti_2_Access_7_1(): return sum(i.a+i.b+i.c for i in dc2)
def test_ti_2_Access_7_2(): return sum(i.a+i.b+i.c for i in dc2s)
def test_ti_2_Access_8_1(): return sum(i.a+i.b+i.c for i in dc3)
def test_ti_2_Access_8_2(): return sum(i.a+i.b+i.c for i in dc3s)
loop = 10000
a:dict = {}
for testName in sorted([tn for tn in globals() if tn.startswith('test_ti_')]):
result = timeit.timeit(f'{testName}(*a)', globals=globals(), number=loop)
# print(f"test{iii}) fps {loop / result :.3f} - s {result / loop:.10f} - {result / loop} {globals()[testName](*a)}")
print(f"{testName} | {result / loop:.10f} sec. | {loop / result : 15.3f} Fps ╞╡-> {globals()[testName](*a)}")

1
tools/check.import.sh

@ -117,6 +117,7 @@ __check(){
grep -v \ grep -v \
-e "TTkWidgets/widget.py:from __future__ import annotations" \ -e "TTkWidgets/widget.py:from __future__ import annotations" \
-e "TTkWidgets/tabwidget.py:from enum import Enum" \ -e "TTkWidgets/tabwidget.py:from enum import Enum" \
-e "TTkWidgets/apptemplate.py:from enum import IntEnum" \
-e "TTkModelView/__init__.py:from importlib.util import find_spec" \ -e "TTkModelView/__init__.py:from importlib.util import find_spec" \
-e "TTkModelView/table_edit_proxy.py:from enum import Enum, auto" \ -e "TTkModelView/table_edit_proxy.py:from enum import Enum, auto" \
-e "TTkModelView/tablemodelcsv.py:import csv" \ -e "TTkModelView/tablemodelcsv.py:import csv" \

Loading…
Cancel
Save