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. 348
      libs/pyTermTk/TermTk/TTkWidgets/apptemplate.py
  9. 348
      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
Use **Sphinx-compatible docstring format** with Epytext-style field lists:
```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):
''' Resize and move the widget
@ -119,18 +138,56 @@ def setGeometry(self, x: int, y: int, width: int, height: int):
:type height: int
'''
# For class/module docstrings include ASCII art examples:
class TTkButton(TTkWidget):
''' TTkButton:
# For class/module docstrings include ASCII art examples,
# a link to the demo and the sandbox link if available:
# 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
::
┌────────┐
│ Text │
╘════════╛
2025/11/04 📅
.. 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:

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

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

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

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

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

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

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

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

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

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

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

@ -22,6 +22,8 @@
__all__ = ['TTkToolTip']
from typing import List
# from TermTk.TTkCore.helper import TTkHelper
from TermTk.TTkCore.log import TTkLog
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.timer import TTkTimer
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.TTkWidgets.widget import TTkWidget
from TermTk.TTkCore.signal import pyTTkSlot
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, *,
toolTip:TTkString="",
toolTip:TTkStringType="",
**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)
self._toolTip = TTkString(toolTip).split('\n')
w = 2+max([s.termWidth() for s in self._toolTip])
h = 2+len(self._toolTip)
if isinstance(toolTip,TTkString):
self._tooltip_list = toolTip.split('\n')
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)
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
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()
borderColor = TTkColor.fg("#888888")
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,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)
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')
toolTip:TTkString = TTkString()
'''Internal timer for delayed tooltip display'''
toolTip:TTkStringType = ''
'''Current tooltip text to be displayed'''
@pyTTkSlot()
@staticmethod
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}")
TTkHelper.toolTipShow(_TTkToolTipDisplayWidget(toolTip=TTkToolTip.toolTip))
@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.toolTip = toolTip
TTkToolTip.toolTipTimer.start(TTkCfg.toolTipTime)
TTkToolTip.toolTipTimer.start(TTkToolTip._toolTipTime)
@staticmethod
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.timeout.connect(TTkToolTip._toolTipShow)

348
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
# SOFTWARE.
from __future__ import annotations
__all__ = ['TTkAppTemplate']
from enum import IntEnum
from dataclasses import dataclass
from typing import Optional,List,Dict,Literal,Tuple
from TermTk.TTkCore.constant import TTkK
from TermTk.TTkCore.color import TTkColor
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.TTkLayouts import TTkLayout, TTkGridLayout
from TermTk.TTkWidgets.container import TTkWidget, TTkContainer
from TermTk.TTkWidgets.menubar import TTkMenuBarLayout
class TTkAppTemplate(TTkContainer):
''' TTkAppTemplate Layout:
''' TTkAppTemplate:
A flexible application template layout with multiple resizable panels.
::
@ -54,9 +61,12 @@ class TTkAppTemplate(TTkContainer):
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=showcase/apptemplate.py>`_
'''
class Position(int):
class Position(IntEnum):
''' This Class enumerate the different panels available in the layout of :py:class:`TTkAppTemplate`
.. autosummary::
@ -87,29 +97,29 @@ class TTkAppTemplate(TTkContainer):
'''Footer'''
MAIN = Position.MAIN
''':py:class:`Position.MAIN`'''
''':py:class:`TTkAppTemplate.Position.MAIN`'''
TOP = Position.TOP
''':py:class:`Position.TOP`'''
''':py:class:`TTkAppTemplate.Position.TOP`'''
BOTTOM = Position.BOTTOM
''':py:class:`Position.BOTTOM`'''
''':py:class:`TTkAppTemplate.Position.BOTTOM`'''
LEFT = Position.LEFT
''':py:class:`Position.LEFT`'''
''':py:class:`TTkAppTemplate.Position.LEFT`'''
RIGHT = Position.RIGHT
''':py:class:`Position.RIGHT`'''
''':py:class:`TTkAppTemplate.Position.RIGHT`'''
CENTER = Position.CENTER
''':py:class:`Position.CENTER`'''
''':py:class:`TTkAppTemplate.Position.CENTER`'''
HEADER = Position.HEADER
''':py:class:`Position.HEADER`'''
''':py:class:`TTkAppTemplate.Position.HEADER`'''
FOOTER = Position.FOOTER
''':py:class:`Position.FOOTER`'''
''':py:class:`TTkAppTemplate.Position.FOOTER`'''
@dataclass(frozen=False)
class _Panel:
# It's either item or widget
item: TTkLayout = None
widget: TTkWidget = None
title: TTkString = None
menubar: TTkMenuBarLayout = None
item: Optional[TTkLayout] = None
widget: Optional[TTkWidget] = None
title: Optional[TTkString] = None
menubar: Optional[TTkMenuBarLayout] = None
size: int = 0
border: bool = True
fixed: bool = False
@ -167,14 +177,31 @@ class TTkAppTemplate(TTkContainer):
return wid.maximumHeight()
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'
#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,
border=False,
**kwargs) -> None:
mp = TTkAppTemplate._Panel(item=TTkLayout(), border=border)
self._panels = {
TTkAppTemplate.MAIN : TTkAppTemplate._Panel(item=TTkLayout(), border=border) ,
TTkAppTemplate.MAIN : mp ,
TTkAppTemplate.TOP : None ,
TTkAppTemplate.BOTTOM : None ,
TTkAppTemplate.LEFT : None ,
@ -196,120 +223,130 @@ class TTkAppTemplate(TTkContainer):
TTkAppTemplate.RIGHT : None ,
TTkAppTemplate.HEADER : None ,
TTkAppTemplate.FOOTER : None }
self._selected = None
self._selected = []
super().__init__( **kwargs)
self.layout().addItem(self._panels[TTkAppTemplate.MAIN].item)
self.layout().addItem(mp.item)
self._updateGeometries(force=True)
self.setFocusPolicy(TTkK.ClickFocus)
def setWidget(self,
widget:TTkWidget, position:Position=Position.MAIN,
size:int=None, title:TTkString="",
border:bool=None, fixed:bool=None) -> None:
widget:TTkWidget, position:TTkAppTemplate.Position=Position.MAIN,
size:Optional[int]=None, title:TTkStringType="",
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`
:param position: defaults to :py:class:`Position.MAIN`
:type position: :py:class:`Position`, optional
:param size: defaults to None
:param position: The panel position, defaults to :py:class:`TTkAppTemplate.Position.MAIN`
:type position: :py:class:`TTkAppTemplate.Position`, optional
: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
:param title: defaults to ""
:param title: The panel title displayed in the border, defaults to ""
: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
:param fixed: defaults to False
:param fixed: Whether the panel size is fixed (non-resizable), defaults to False
:type fixed: bool, optional
'''
if not self._panels[position]:
self._panels[position] = TTkAppTemplate._Panel()
if wid:=self._panels[position].widget:
if not (p:=self._panels[position]):
p = self._panels[position] = TTkAppTemplate._Panel()
if wid:=p.widget:
self.layout().removeWidget(wid)
self._panels[position].widget = None
if it:=self._panels[position].item:
p.widget = None
if it:=p.item:
self.layout().removeItem(it)
self._panels[position].item = None
p.item = None
if widget:
self._panels[position].widget = widget
p.widget = widget
self.layout().addWidget(widget)
if border!=None:
self._panels[position].border = border
if border is not None:
p.border = border
if fixed is not None:
self._panels[position].fixed = fixed
self._panels[position].title = TTkString(title)
self._panels[position].size = ( size if size is not None else
p.fixed = fixed
p.title = title if isinstance(title,TTkString) else TTkString(title)
p.size = (
size if size is not None else
widget.minimumWidth() if position in (TTkAppTemplate.LEFT,TTkAppTemplate.RIGHT) else
widget.minimumHeight() )
self._updateGeometries(force=True)
def setItem(self,
item:TTkLayout, position:Position=Position.MAIN,
size:int=None, title:TTkString="",
border:bool=None, fixed:bool=None) -> None:
'''
Place the :py:class:`TTkLayout` in the :py:class:`TTkAppTemplate`'s panel identified by its :py:class:`Position`
item:TTkLayout, position:TTkAppTemplate.Position=Position.MAIN,
size:Optional[int]=None, title:TTkStringType="",
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`
:param item:
:param item: The layout to place in the panel
:type item: :py:class:`TTkLayout`
:param position: defaults to :py:class:`Position.MAIN`
:type position: :py:class:`Position`, optional
:param size: defaults to None
:param position: The panel position, defaults to :py:class:`TTkAppTemplate.Position.MAIN`
:type position: :py:class:`TTkAppTemplate.Position`, optional
: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
:param title: defaults to ""
:param title: The panel title displayed in the border, defaults to ""
: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
:param fixed: defaults to False
:param fixed: Whether the panel size is fixed (non-resizable), defaults to False
:type fixed: bool, optional
'''
if not self._panels[position]:
self._panels[position] = TTkAppTemplate._Panel()
if wid:=self._panels[position].widget:
if not (p:=self._panels[position]):
p = self._panels[position] = TTkAppTemplate._Panel()
if wid:=p.widget:
self.layout().removeWidget(wid)
self._panels[position].widget = None
if it:=self._panels[position].item:
p.widget = None
if it:=p.item:
self.layout().removeItem(it)
self._panels[position].item = None
p.item = None
if item:
self._panels[position].item = item
p.item = item
self.layout().addItem(item)
if border!=None:
self._panels[position].border = border
p.border = border
if fixed is not None:
self._panels[position].fixed = fixed
self._panels[position].title = TTkString(title)
self._panels[position].size = ( size if size is not None else
p.fixed = fixed
p.title = title if isinstance(title,TTkString) else TTkString(title)
p.size = (
size if size is not None else
item.minimumWidth() if position in (TTkAppTemplate.LEFT,TTkAppTemplate.RIGHT) else
item.minimumHeight() )
self._updateGeometries(force=True)
def setTitle(self, position:Position=Position.MAIN, title:str=""):
'''Set the title of the panel identified by the "position"
def setTitle(self, position:TTkAppTemplate.Position=Position.MAIN, title:TTkStringType="") -> None:
''' Set the title of the panel identified by the position
:param position: defaults to :py:class:`Position.MAIN`
:type position: :py:class:`Position`, optional
:param title: defaults to ""
:param position: The panel position, defaults to :py:class:`TTkAppTemplate.Position.MAIN`
:type position: :py:class:`TTkAppTemplate.Position`, optional
:param title: The title text to display, defaults to ""
:type title: :py:class:`TTkString`, optional
'''
if not self._panels[position]: return
self._panels[position].title = TTkString(title) if title else ""
if not (p:=self._panels[position]):
return
p.title = title if isinstance(title,TTkString) else TTkString(title)
self._updateGeometries(force=True)
def menuBar(self, position:Position=MAIN) -> TTkMenuBarLayout:
'''
Retrieve the :py:class:`TTkMenuBarLayout` in the panel identified by the position.
def menuBar(self, position:TTkAppTemplate.Position=MAIN) -> Optional[TTkMenuBarLayout]:
''' Retrieve the :py:class:`TTkMenuBarLayout` in the panel identified by the position
:param position: defaults to :py:class:`Position.MAIN`
:type position: :py:class:`Position`, optional
:param position: The panel position, defaults to :py:class:`TTkAppTemplate.Position.MAIN`
: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:
if not self._panels[position]:
self._panels[position] = TTkAppTemplate._Panel()
p = self._panels[position]
:param menuBar: The menu bar layout to set
:type menuBar: :py:class:`TTkMenuBarLayout`
: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()
if p.menubar:
self.rootLayout().removeItem(p.menubar)
# TODO: Dispose the menubar
@ -319,26 +356,40 @@ class TTkAppTemplate(TTkContainer):
self._updateGeometries(force=True)
def setBorder(self, border=True, position=MAIN) -> None:
if not self._panels[position]:
self._panels[position] = TTkAppTemplate._Panel()
self._panels[position].border = border
''' Set whether to draw a border around the panel
: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)
def setFixed(self, fixed=False, position=MAIN) -> None:
if not self._panels[position]:
self._panels[position] = TTkAppTemplate._Panel()
self._panels[position].fixed = fixed
''' Set whether the panel size is fixed (non-resizable)
: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)
def resizeEvent(self, width: int, height: int) -> None:
self._updateGeometries()
def focusOutEvent(self) -> None:
self._selected = None
self._selected = []
self.update()
def mouseReleaseEvent(self, evt:TTkMouseEvent) -> bool:
self._selected = None
self._selected = []
self.update()
return True
@ -348,10 +399,10 @@ class TTkAppTemplate(TTkContainer):
spl = self._splitters
pns = self._panels
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)
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)
return True
@ -359,7 +410,9 @@ class TTkAppTemplate(TTkContainer):
if not self._selected: return False
pns = self._panels
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:
p.size = evt.x-x
elif loc == TTkAppTemplate.RIGHT:
@ -372,6 +425,11 @@ class TTkAppTemplate(TTkContainer):
return True
def minimumWidth(self) -> int:
''' Get the minimum width required for the template
:return: The minimum width in characters
:rtype: int
'''
pns = self._panels
# 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.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)
def maximumWidth(self) -> int:
''' Get the maximum width allowed for the template
:return: The maximum width in characters
:rtype: int
'''
pns = self._panels
# 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.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)
def minimumHeight(self) -> int:
''' Get the minimum height required for the template
:return: The minimum height in characters
:rtype: int
'''
pns = self._panels
# 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 )
def maximumHeight(self) -> int:
''' Get the maximum height allowed for the template
:return: The maximum height in characters
:rtype: int
'''
pns = self._panels
# 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)
# Main Boundaries
pm=pns[TTkAppTemplate.MAIN]
if not (pm:=pns[TTkAppTemplate.MAIN]):
raise ValueError()
mm=pm.menubar
mmaxw = pm.maximumWidth()
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
# Check if there is a splitter to be used for the menubar
# 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 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 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 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 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 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 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 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
if not (mminw <= (newszw:=(w-sl-sr)) <= mmaxw):
@ -562,7 +639,13 @@ class TTkAppTemplate(TTkContainer):
# Resize any panel to the proper dimension
w+=bl+br
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 _fix: # Fixed
styleToMerge = {'default':{'glyphs':('','','','','','')}}
@ -571,7 +654,7 @@ class TTkAppTemplate(TTkContainer):
if not _adj:
mbl[_loc] = None
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.RIGHT_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
w,h = self.size()
spl[TTkAppTemplate.HEADER] = None if not bh else {'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.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 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 )
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.RIGHT] = None if not br else {'pos':(bm+sl+bl+newszw , ca ) ,'size':cb-ca , 'fixed':fr , 'panel': pr }
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 TTkAppTemplate._Splitter( pos=(bm+sl+bl+newszw , ca ) ,size=cb-ca , fixed=fr , panel=pr )
ca = sl + (bm if pl else 0 )
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.BOTTOM] = None if not bb else {'pos':(ca , bm+sh+bh+st+bt+newszh) ,'size':cb-ca , 'fixed':fb , 'panel': pb }
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 TTkAppTemplate._Splitter( pos=(ca , bm+sh+bh+st+bt+newszh) ,size=cb-ca , fixed=fb , panel=pb )
self.update()
@ -622,7 +705,8 @@ class TTkAppTemplate(TTkContainer):
spl = self._splitters
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))
selectColor = TTkColor.fg('#88FF00')
@ -630,21 +714,21 @@ class TTkAppTemplate(TTkContainer):
# hline = ('╞','═','╡')
# vline = ('╥','║','╨')
def drawVLine(sp, color=TTkColor.RST):
_x,_y = sp['pos']
_w,_h = 1,sp['size']
chs = ('','','','','') if sp['fixed'] else ('','','','','')
def drawVLine(sp:TTkAppTemplate._Splitter, color:TTkColor=TTkColor.RST) -> None:
_x,_y = sp.pos
_w,_h = 1,sp.size
chs = ('','','','','') if sp.fixed else ('','','','','')
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+_h-1), color=color, char=chs[2]if b and _y+_h==h else chs[4])
def drawHLine(sp, color=TTkColor.RST):
_x,_y = sp['pos']
_w,_h = sp['size'],1
chs = ('','','','','') if sp['fixed'] else ('','','','','')
def drawHLine(sp:TTkAppTemplate._Splitter, color:TTkColor=TTkColor.RST) -> None:
_x,_y = sp.pos
_w,_h = sp.size,1
chs = ('','','','','') if sp.fixed else ('','','','','')
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+_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())
_tx = (_w-_l)//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)
# 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.FOOTER]) : drawHLine(sp, color=selectColor if self._selected and 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.RIGHT]) : drawVLine(sp, color=selectColor if self._selected and 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.BOTTOM]) : drawHLine(sp, color=selectColor if self._selected and TTkAppTemplate.BOTTOM 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 TTkAppTemplate.FOOTER 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 TTkAppTemplate.RIGHT 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 TTkAppTemplate.BOTTOM in self._selected else TTkColor.RST)
# 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:
x = spv['pos'][0]
y = sph['pos'][1]
ch = chs[( 0 if sph['fixed'] else 0x01 ) | ( 0 if spv['fixed'] else 0x02 )]
x = spv.pos[0]
y = sph.pos[1]
ch = chs[( 0 if sph.fixed else 0x01 ) | ( 0 if spv.fixed else 0x02 )]
canvas.drawChar(pos=(x,y), char=ch)
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
for l in mbl:
if mb:=mbl[l]:
canvas.drawText(pos=mb['pos'],text=mb['text'])
canvas.drawText(pos=mb.pos,text=mb.text)
return super().paintEvent(canvas)

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

@ -22,10 +22,13 @@
__all__ = ['TTkSplitter']
from typing import Union,List,Optional
from TermTk.TTkCore.constant import TTkK
from TermTk.TTkCore.cfg import TTkCfg
from TermTk.TTkCore.canvas import TTkCanvas
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.TTkLayouts.layout import TTkLayout
@ -33,7 +36,46 @@ from TermTk.TTkWidgets.widget import TTkWidget
from TermTk.TTkWidgets.container import 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 = {
'default': {'glyphs' : {
@ -51,10 +93,23 @@ class TTkSplitter(TTkContainer):
'_orientation', '_separators', '_refSizes',
'_items', '_titles', '_separatorSelected',
'_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, *,
border:bool=False,
orientation:TTkK.Direction=TTkK.HORIZONTAL,
**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._titles = []
self._separators = []
@ -77,61 +132,113 @@ class TTkSplitter(TTkContainer):
self.addItem(item)
self.setLayout(_SplitterLayout())
def setBorder(self, border):
'''setBorder'''
def setBorder(self, border:bool) -> None:
''' Set whether to draw a border around the splitter
:param border: True to show border, False to hide
:type border: bool
'''
self._border = border
if border: self.setPadding(1,1,1,1)
else: self.setPadding(0,0,0,0)
self.update()
def border(self):
'''border'''
def border(self) -> bool:
''' Get the current border state
:return: True if border is visible, False otherwise
:rtype: bool
'''
return self._border
def orientation(self):
'''orientation'''
def orientation(self) -> TTkK.Direction:
''' Get the current splitter orientation
:return: The orientation (HORIZONTAL or VERTICAL)
:rtype: :py:class:`TTkK.Direction`
'''
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 not in (TTkK.HORIZONTAL, TTkK.VERTICAL): return
self._orientation = orientation
self._updateGeometries()
def clean(self):
for i in reversed(self._items):
if issubclass(type(i),TTkWidget):
self.removeWidget(i)
def clean(self) -> None:
''' Remove all widgets and items from the splitter '''
for _i in reversed(self._items):
if isinstance(_i,TTkWidget):
self.removeWidget(_i)
else:
self.removeItem(i)
self.removeItem(_i)
def count(self) -> int:
''' Get the number of items in the splitter
def count(self):
'''count'''
:return: The count of widgets/items
:rtype: int
'''
return len(self._items)
def indexOf(self, widget):
'''indexOf'''
def indexOf(self, widget:Union[TTkWidget,TTkLayout]) -> int:
''' 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)
def widget(self, index):
'''widget'''
def widget(self, index:int) -> Union[TTkWidget,TTkLayout]:
''' 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]
def replaceItem(self, index, item, title=None):
'''replaceItem'''
def replaceItem(self, index:int, item:TTkLayout, title:Optional[TTkStringType]=None) -> None:
''' 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):
return self.addItem(item, title=title)
TTkLayout.removeItem(self.layout(), self._items[index])
TTkLayout.insertItem(self.layout(), 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()
b = 2 if self._border else 0
self._processRefSizes(w-b,h-b)
self._updateGeometries()
def replaceWidget(self, index, widget, title=None):
'''replaceWidget'''
def replaceWidget(self, index:int, widget:TTkWidget, title:Optional[str]=None) -> None:
''' 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):
return self.addWidget(widget, title=title)
TTkLayout.removeWidget(self.layout(), self._items[index])
@ -143,8 +250,12 @@ class TTkSplitter(TTkContainer):
self._processRefSizes(w-b,h-b)
self._updateGeometries()
def removeItem(self, item):
'''removeItem'''
def removeItem(self, item:TTkLayout) -> None:
''' Remove a layout from the splitter
:param item: The layout to remove
:type item: :py:class:`TTkLayout`
'''
index = self.indexOf(item)
self._items.pop(index)
self._refSizes.pop(index)
@ -155,8 +266,12 @@ class TTkSplitter(TTkContainer):
self._processRefSizes(w-b,h-b)
self._updateGeometries()
def removeWidget(self, widget):
'''removeWidget'''
def removeWidget(self, widget:TTkWidget) -> None:
''' Remove a widget from the splitter
:param widget: The widget to remove
:type widget: :py:class:`TTkWidget`
'''
index = self.indexOf(widget)
self._items.pop(index)
self._refSizes.pop(index)
@ -167,27 +282,74 @@ class TTkSplitter(TTkContainer):
self._processRefSizes(w-b,h-b)
self._updateGeometries()
def addItem(self, item, size=None, title=None):
'''addItem'''
def addItem(self, item:TTkLayout, size:Optional[int]=None, title:Optional[TTkStringType]=None) -> None:
''' 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)
def insertItem(self, index, item, size=None, title=None):
'''insertItem'''
def insertItem(self, index:int, item:TTkLayout, size:Optional[int]=None, title:Optional[TTkStringType]=None) -> None:
''' 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)
self._insertWidgetItem(index, item, size=size, title=title)
def addWidget(self, widget, size=None, title=None):
'''addWidget'''
def addWidget(self, widget:TTkWidget, size:Optional[int]=None, title:Optional[TTkStringType]=None) -> None:
''' 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)
def insertWidget(self, index, widget, size=None, title=None):
'''insertWidget'''
def insertWidget(self, index:int, widget:TTkWidget, size:Optional[int]=None, title:Optional[TTkStringType]=None) -> None:
''' 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)
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._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
self._refSizes.insert(index, size)
@ -198,8 +360,12 @@ class TTkSplitter(TTkContainer):
if self.parentWidget():
self.parentWidget().update(repaint=True, updateLayout=True)
def setSizes(self, sizes):
'''setSizes'''
def setSizes(self, sizes:List[Optional[int]]) -> None:
''' 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)
sizes=sizes[:ls]+[None]*max(0,ls-len(sizes))
self._refSizes = sizes.copy()
@ -208,8 +374,15 @@ class TTkSplitter(TTkContainer):
self._processRefSizes(w-b,h-b)
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:
return 0, 0x1000
# this is because there is a hidden splitter at position -1
@ -221,7 +394,15 @@ class TTkSplitter(TTkContainer):
maxsize += item.maxDimension(self._orientation)+1
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:
return 0, 0x1000
minsize = 0x0
@ -232,7 +413,12 @@ class TTkSplitter(TTkContainer):
maxsize += item.maxDimension(self._orientation)+1
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
w,h = self.size()
if w==h==0: return
@ -301,11 +487,24 @@ class TTkSplitter(TTkContainer):
_processGeometry(i, True)
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.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
if self._orientation == TTkK.HORIZONTAL:
sizeRef = w
@ -321,15 +520,17 @@ class TTkSplitter(TTkContainer):
numVarSizes = len([x for x in self._refSizes if x is None])
avalSize = sizeRef-fixSize
varSize = avalSize//numVarSizes
sizes = []
sizes:List[int] = []
for s in self._refSizes:
if not s:
if s is None:
avalSize -= varSize
s = varSize + avalSize if avalSize<varSize else 0
newSize = varSize + avalSize if avalSize<varSize else 0
sizes.append(newSize)
else:
sizes.append(s)
sizes = [varSize if s is None else s for s in self._refSizes]
else:
sizes = self._refSizes
sizes = [s for s in self._refSizes if s is not None]
sizeRef = sum(sizes)
self._separators = [sum(sizes[:i+1]) for i in range(len(sizes))]
@ -341,7 +542,18 @@ class TTkSplitter(TTkContainer):
diff = h/sizeRef
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
self._processRefSizes(w-b,h-b)
self._updateGeometries(resized=True)
@ -380,6 +592,14 @@ class TTkSplitter(TTkContainer):
self._separatorSelected = None
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
if not self._items: return ret
if self._orientation == TTkK.VERTICAL:
@ -392,6 +612,14 @@ class TTkSplitter(TTkContainer):
return ret
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
if not self._items: return ret
if self._orientation == TTkK.HORIZONTAL:
@ -404,6 +632,14 @@ class TTkSplitter(TTkContainer):
return ret
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
if not self._items: return 0x10000
if self._orientation == TTkK.VERTICAL:
@ -418,6 +654,14 @@ class TTkSplitter(TTkContainer):
return ret
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
if not self._items: return 0x10000
if self._orientation == TTkK.HORIZONTAL:
@ -431,7 +675,7 @@ class TTkSplitter(TTkContainer):
ret = min(ret,item.maximumWidth()+b)
return ret
def paintEvent(self, canvas):
def paintEvent(self, canvas:TTkCanvas) -> None:
style = self.currentStyle()
color = style['color']
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 \
-e "TTkWidgets/widget.py:from __future__ import annotations" \
-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/table_edit_proxy.py:from enum import Enum, auto" \
-e "TTkModelView/tablemodelcsv.py:import csv" \

Loading…
Cancel
Save