diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 82f6e955..3162d0d6 100644 --- a/.github/copilot-instructions.md +++ b/.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 `__) - 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 `_ + App Template Layout + ┌─────────────────────────────────┐ + │ Header │ + ├─────────┬──────────────┬────────┤ H + │ │ Top │ │ + │ ├──────────────┤ │ T + │ │ │ │ + │ Right │ Main │ Left │ + │ │ Center │ │ + │ │ │ │ + │ ├──────────────┤ │ B + │ │ Bottom │ │ + ├─────────┴──────────────┴────────┤ F + │ Footer │ + └─────────────────────────────────┘ + R L + + Demo: `apptemplate.py `_ + `online `_ ''' # For signals, document parameters: diff --git a/libs/pyTermTk/TermTk/TTkCore/cfg.py b/libs/pyTermTk/TermTk/TTkCore/cfg.py index c3f6a086..6698f51e 100644 --- a/libs/pyTermTk/TermTk/TTkCore/cfg.py +++ b/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 diff --git a/libs/pyTermTk/TermTk/TTkCore/helper.py b/libs/pyTermTk/TermTk/TTkCore/helper.py index 299e2308..7fa34a10 100644 --- a/libs/pyTermTk/TermTk/TTkCore/helper.py +++ b/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 diff --git a/libs/pyTermTk/TermTk/TTkCore/timer_pyodide.py b/libs/pyTermTk/TermTk/TTkCore/timer_pyodide.py index 1b01e659..71830cb2 100644 --- a/libs/pyTermTk/TermTk/TTkCore/timer_pyodide.py +++ b/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, diff --git a/libs/pyTermTk/TermTk/TTkCore/timer_unix.py b/libs/pyTermTk/TermTk/TTkCore/timer_unix.py index 333c3870..a1ff52b4 100644 --- a/libs/pyTermTk/TermTk/TTkCore/timer_unix.py +++ b/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__( diff --git a/libs/pyTermTk/TermTk/TTkGui/textdocument_highlight_pygments.py b/libs/pyTermTk/TermTk/TTkGui/textdocument_highlight_pygments.py index 469f622e..2551a506 100644 --- a/libs/pyTermTk/TermTk/TTkGui/textdocument_highlight_pygments.py +++ b/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 diff --git a/libs/pyTermTk/TermTk/TTkGui/tooltip.py b/libs/pyTermTk/TermTk/TTkGui/tooltip.py index bfa98989..e4f48b5d 100644 --- a/libs/pyTermTk/TermTk/TTkGui/tooltip.py +++ b/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) diff --git a/libs/pyTermTk/TermTk/TTkWidgets/apptemplate.py b/libs/pyTermTk/TermTk/TTkWidgets/apptemplate.py index 6396fa9a..6303e62d 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/apptemplate.py +++ b/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 `_ + `online `_ ''' - 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 - widget.minimumWidth() if position in (TTkAppTemplate.LEFT,TTkAppTemplate.RIGHT) else - widget.minimumHeight() ) + 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 - item.minimumWidth() if position in (TTkAppTemplate.LEFT,TTkAppTemplate.RIGHT) else - item.minimumHeight() ) + 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() @@ -616,13 +699,14 @@ class TTkAppTemplate(TTkContainer): #def setLayout(self, layout): # self._panels[TTkAppTemplate.MAIN].item = layout - def paintEvent(self, canvas: TTkCanvas) -> None: + def paintEvent(self, canvas:TTkCanvas) -> None: w,h = self.size() pns = self._panels 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) \ No newline at end of file diff --git a/libs/pyTermTk/TermTk/TTkWidgets/splitter.py b/libs/pyTermTk/TermTk/TTkWidgets/splitter.py index 456af25a..f737aa06 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/splitter.py +++ b/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 `_ + (`online `__) + + .. 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 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: @@ -391,7 +611,15 @@ class TTkSplitter(TTkContainer): ret = max(ret,item.minimumHeight()+b) 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 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: @@ -417,7 +653,15 @@ class TTkSplitter(TTkContainer): ret = min(ret,item.maximumHeight()+b) 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 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'] diff --git a/tests/timeit/34.dataclasses.py b/tests/timeit/34.dataclasses.py new file mode 100755 index 00000000..67bbc98c --- /dev/null +++ b/tests/timeit/34.dataclasses.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2025 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import annotations + +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)}") diff --git a/tools/check.import.sh b/tools/check.import.sh index 8a0818b0..ee07a561 100755 --- a/tools/check.import.sh +++ b/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" \