diff --git a/README.md b/README.md index 6038dc7e..cefea4bd 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,16 @@ and inspired by a mix of [Qt5](https://www.riverbankcomputing.com/static/Docs/Py [pyTermTk.Showcase.002.webm](https://user-images.githubusercontent.com/8876552/206490679-2bbdc909-c9bc-41c1-9a50-339b06dabecd.webm) ## Features +- Self Contained (no external lib required) +- Cross compatible: [Linux](https://en.wikipedia.org/wiki/Linux)🐧, [MacOS](https://en.wikipedia.org/wiki/MacOS)🍎, [MS Windows](https://en.wikipedia.org/wiki/Microsoft_Windows)🪟, [HTML5](https://en.wikipedia.org/wiki/HTML5)🌍([Try](https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html)) - Basic widgets for [TUI](https://en.wikipedia.org/wiki/Text-based_user_interface) development (Button, Label, checkbox, ...) - Specialized widgets to improve the usability (Windows, Frames, Tables, ...) - QT Like Layout system to help arrange the widgets in the terminal - True color support - Ful/Half/Zero sized Unicode characters 😎 +- I am pretty sure there is something else... ## Limitations -- The native **Windows** porting is not ready yet but it works with [Cygwin](https://www.cygwin.com) or **WSL**. - Only the key combinations forwarded by the terminal emulator used are detected (ALT,CTRL may not be handled) --- @@ -70,13 +72,14 @@ python3 tests/test.input.py #### Demos ```bash -# Press CTRL-C to exit -# the logs are written to "session.log" -# add "-f" option to run it in "fullscreen" :-D +# Press CTRL-C to exit (CTRL-Break on Windows) # Showcase Demo python3 demo/demo.py -f +# run the ttkDesigner +python3 -m ttkDesigner + # Paint demo python3 demo/paint.py diff --git a/TermTk/TTkCore/TTkTerm/__init__.py b/TermTk/TTkCore/TTkTerm/__init__.py index 4b523bec..5f18a72b 100644 --- a/TermTk/TTkCore/TTkTerm/__init__.py +++ b/TermTk/TTkCore/TTkTerm/__init__.py @@ -1,5 +1,5 @@ -from .inputkey import * -from .inputmouse import * -from .colors import * +# from .inputkey import * +# from .inputmouse import * +# from .colors import * +# from .input import * from .term import * -from .input import * diff --git a/TermTk/TTkCore/TTkTerm/input.py b/TermTk/TTkCore/TTkTerm/input.py index e8afcf34..27ab9141 100644 --- a/TermTk/TTkCore/TTkTerm/input.py +++ b/TermTk/TTkCore/TTkTerm/input.py @@ -27,72 +27,72 @@ from time import time import platform -if platform.system() == 'Linux': - from .readinputlinux import ReadInput - # from .readinputlinux_thread import ReadInput -elif platform.system() == 'Darwin': - from .readinputlinux import ReadInput -elif platform.system() == 'Windows': - raise NotImplementedError('Windows OS not yet supported') +from ..drivers import TTkInputDriver from TermTk.TTkCore.log import TTkLog from TermTk.TTkCore.constant import TTkK from TermTk.TTkCore.signal import pyTTkSignal +from TermTk.TTkCore.TTkTerm.term import TTkTerm from TermTk.TTkCore.TTkTerm.inputkey import TTkKeyEvent from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent + class TTkInput: - __slots__ = ( - '_readInput', - '_leftLastTime', '_midLastTime', '_rightLastTime', - '_leftTap', '_midTap', '_rightTap', - '_pasteBuffer', '_bracketedPaste', - # Signals - 'inputEvent', 'pasteEvent' - ) - - def __init__(self): - self.inputEvent = pyTTkSignal(TTkKeyEvent, TTkMouseEvent) - self.pasteEvent = pyTTkSignal(str) - self._pasteBuffer = "" - self._bracketedPaste = False - self._readInput = None - self._leftLastTime = 0 - self._midLastTime = 0 - self._rightLastTime = 0 - self._leftTap = 0 - self._midTap = 0 - self._rightTap = 0 - - def close(self): - if self._readInput: - self._readInput.close() - - def stop(self): + inputEvent = pyTTkSignal(TTkKeyEvent, TTkMouseEvent) + pasteEvent = pyTTkSignal(str) + _pasteBuffer = "" + _bracketedPaste = False + _readInput = None + _leftLastTime = 0 + _midLastTime = 0 + _rightLastTime = 0 + _leftTap = 0 + _midTap = 0 + _rightTap = 0 + _mouse_re = re.compile(r"\033\[<(\d+);(\d+);(\d+)([mM])") + + class Mouse(int): + ON = 0x01 + DIRECT = 0x02 + + @staticmethod + def init(mouse:bool=False, directMouse:bool=False) -> None: + TTkInput._readInput = TTkInputDriver() + TTkTerm.setMouse(mouse, directMouse) + + @staticmethod + def close() -> None: + TTkTerm.setMouse(False, False) + if TTkInput._readInput: + TTkInput._readInput.close() + + @staticmethod + def stop() -> None: pass - def cont(self): - if self._readInput: - self._readInput.cont() + @staticmethod + def cont() -> None: + if TTkInput._readInput: + TTkInput._readInput.cont() - def start(self): - self._readInput = ReadInput() - for stdinRead in self._readInput.read(): - self.key_process(stdinRead) + @staticmethod + def start() -> None: + for stdinRead in TTkInput._readInput.read(): + TTkInput.key_process(stdinRead) TTkLog.debug("Close TTkInput") - mouse_re = re.compile(r"\033\[<(\d+);(\d+);(\d+)([mM])") - def key_process(self, stdinRead): - if self._bracketedPaste: + @staticmethod + def key_process(stdinRead:str) -> None: + if TTkInput._bracketedPaste: if stdinRead.endswith("\033[201~"): - self._pasteBuffer += stdinRead[:-6] - self._bracketedPaste = False + TTkInput._pasteBuffer += stdinRead[:-6] + TTkInput._bracketedPaste = False # due to the CRNL methos (don't ask me why) the terminal # is substituting all the \n with \r - self.pasteEvent.emit(self._pasteBuffer.replace('\r','\n')) - self._pasteBuffer = "" + TTkInput.pasteEvent.emit(TTkInput._pasteBuffer.replace('\r','\n')) + TTkInput._pasteBuffer = "" else: - self._pasteBuffer += stdinRead + TTkInput._pasteBuffer += stdinRead return mevt,kevt = None, None @@ -102,7 +102,7 @@ class TTkInput: kevt = TTkKeyEvent.parse(stdinRead) else: # Mouse Event - m = self.mouse_re.match(stdinRead) + m = TTkInput._mouse_re.match(stdinRead) if not m: # TODO: Return Error hex = [f"0x{ord(x):02x}" for x in stdinRead] @@ -134,18 +134,18 @@ class TTkInput: mod |= TTkK.AltModifier if code == 0x00: - self._leftLastTime, self._leftTap = _checkTap(self._leftLastTime, self._leftTap) - tap = self._leftTap + TTkInput._leftLastTime, TTkInput._leftTap = _checkTap(TTkInput._leftLastTime, TTkInput._leftTap) + tap = TTkInput._leftTap key = TTkMouseEvent.LeftButton evt = TTkMouseEvent.Press if state=="M" else TTkMouseEvent.Release elif code == 0x01: - self._midLastTime, self._midTap = _checkTap(self._midLastTime, self._midTap) - tap = self._midTap + TTkInput._midLastTime, TTkInput._midTap = _checkTap(TTkInput._midLastTime, TTkInput._midTap) + tap = TTkInput._midTap key = TTkMouseEvent.MidButton evt = TTkMouseEvent.Press if state=="M" else TTkMouseEvent.Release elif code == 0x02: - self._rightLastTime, self._rightTap = _checkTap(self._rightLastTime, self._rightTap) - tap = self._rightTap + TTkInput._rightLastTime, TTkInput._rightTap = _checkTap(TTkInput._rightLastTime, TTkInput._rightTap) + tap = TTkInput._rightTap key = TTkMouseEvent.RightButton evt = TTkMouseEvent.Press if state=="M" else TTkMouseEvent.Release elif code == 0x20: @@ -171,37 +171,13 @@ class TTkInput: mevt = TTkMouseEvent(x, y, key, evt, mod, tap, m.group(0).replace("\033", "")) if kevt or mevt: - self.inputEvent.emit(kevt, mevt) + TTkInput.inputEvent.emit(kevt, mevt) return if stdinRead.startswith("\033[200~"): - self._pasteBuffer = stdinRead[6:] - self._bracketedPaste = True + TTkInput._pasteBuffer = stdinRead[6:] + TTkInput._bracketedPaste = True return hex = [f"0x{ord(x):02x}" for x in stdinRead] TTkLog.error("UNHANDLED: "+stdinRead.replace("\033","") + " - "+",".join(hex)) - - -def main(): - print("Retrieve Keyboard, Mouse press/drag/wheel Events") - print("Press q or to exit") - from term import TTkTerm - - TTkTerm.push(TTkTerm.Mouse.ON) - TTkTerm.setEcho(False) - - def callback(kevt=None, mevt=None): - if kevt is not None: - print(f"Key Event: {kevt}") - if mevt is not None: - print(f"Mouse Event: {mevt}") - - testInput = TTkInput() - testInput.get_key(callback) - - TTkTerm.push(TTkTerm.Mouse.OFF + TTkTerm.Mouse.DIRECT_OFF) - TTkTerm.setEcho(True) - -if __name__ == "__main__": - main() diff --git a/TermTk/TTkCore/TTkTerm/term.py b/TermTk/TTkCore/TTkTerm/term.py index bed6bb1e..8f141038 100644 --- a/TermTk/TTkCore/TTkTerm/term.py +++ b/TermTk/TTkCore/TTkTerm/term.py @@ -22,9 +22,4 @@ __all__ = ['TTkTerm'] -import importlib.util - -if importlib.util.find_spec('pyodideProxy'): - from .term_pyodide import TTkTerm -else: - from .term_unix import TTkTerm +from ..drivers import * \ No newline at end of file diff --git a/TermTk/TTkCore/TTkTerm/term_base.py b/TermTk/TTkCore/TTkTerm/term_base.py index dd80a39a..2585c434 100644 --- a/TermTk/TTkCore/TTkTerm/term_base.py +++ b/TermTk/TTkCore/TTkTerm/term_base.py @@ -30,13 +30,13 @@ class TTkTermBase(): SET_BRACKETED_PM = "\033[?2004h" # Ps = 2 0 0 4 ⇒ Set bracketed paste mode, xterm. RESET_BRACKETED_PM = "\033[?2004l" # Ps = 2 0 0 4 ⇒ Reset bracketed paste mode, xterm. - class Mouse(): + class Mouse(str): ON = "\033[?1002h\033[?1006h" # Enable reporting of mouse position on click and release OFF = "\033[?1002l\033[?1006l" # Disable mouse reporting DIRECT_ON = "\033[?1003h" # Enable reporting of mouse position at any movement DIRECT_OFF = "\033[?1003l" # Disable direct mouse reporting - class Cursor(): + class Cursor(str): # from: # https://superuser.com/questions/607478/how-do-you-change-the-xterm-cursor-to-an-i-beam-or-vertical-bar # echo -e -n "\x1b[\x30 q" # changes to blinking block @@ -91,44 +91,48 @@ class TTkTermBase(): _sigWinChCb = None @staticmethod - def init(mouse: bool = True, directMouse: bool = False, title: str = "TermTk", sigmask=0): + def init(title: str = "TermTk", sigmask=0) -> None: TTkTermBase.title = title - TTkTermBase.mouse = mouse | directMouse - TTkTermBase.directMouse = directMouse TTkTermBase.Cursor.hide() TTkTermBase.push(TTkTermBase.escTitle(TTkTermBase.title)) TTkTermBase.push(TTkTermBase.ALT_SCREEN) TTkTermBase.push(TTkTermBase.SET_BRACKETED_PM) TTkTermBase.push(TTkTermBase.CLEAR + TTkTermBase.Cursor.HIDE) - if TTkTermBase.mouse: - TTkTermBase.push(TTkTermBase.Mouse.ON) - if TTkTermBase.directMouse: - TTkTermBase.push(TTkTermBase.Mouse.DIRECT_ON) TTkTermBase.setEcho(False) TTkTermBase.CRNL(False) TTkTermBase.setSigmask(sigmask, False) @staticmethod - def exit(): + def setMouse(mouse:bool=False, directMouse:bool=False) -> None: + TTkTermBase.mouse = mouse | directMouse + TTkTermBase.directMouse = directMouse + if TTkTermBase.mouse: + TTkTermBase.push(TTkTermBase.Mouse.DIRECT_OFF) + TTkTermBase.push(TTkTermBase.Mouse.ON) + if TTkTermBase.directMouse: + TTkTermBase.push(TTkTermBase.Mouse.DIRECT_ON) + else: + TTkTermBase.push(TTkTermBase.Mouse.OFF) + TTkTermBase.push(TTkTermBase.Mouse.DIRECT_OFF) + + @staticmethod + def exit() -> None: TTkTermBase.push(TTkTermBase.Mouse.OFF + TTkTermBase.Mouse.DIRECT_OFF) TTkTermBase.push(TTkTermBase.CLEAR + TTkTermBase.NORMAL_SCREEN + TTkTermBase.RESET_BRACKETED_PM + TTkTermBase.Cursor.SHOW + TTkTermBase.escTitle()) TTkTermBase.setEcho(True) TTkTermBase.CRNL(True) @staticmethod - def stop(): + def stop() -> None: TTkTermBase.push(TTkTermBase.Mouse.OFF + TTkTermBase.Mouse.DIRECT_OFF) TTkTermBase.push(TTkTermBase.CLEAR + TTkTermBase.NORMAL_SCREEN + TTkTermBase.RESET_BRACKETED_PM + TTkTermBase.Cursor.SHOW + TTkTermBase.escTitle()) TTkTermBase.setEcho(True) TTkTermBase.CRNL(True) @staticmethod - def cont(): + def cont() -> None: TTkTermBase.push(TTkTermBase.ALT_SCREEN + TTkTermBase.SET_BRACKETED_PM + TTkTermBase.CLEAR + TTkTermBase.Cursor.HIDE + TTkTermBase.escTitle(TTkTermBase.title)) - if TTkTermBase.mouse: - TTkTermBase.push(TTkTermBase.Mouse.ON) - if TTkTermBase.directMouse: - TTkTermBase.push(TTkTermBase.Mouse.DIRECT_ON) + TTkTermBase.setMouse(TTkTermBase.mouse, TTkTermBase.directMouse) TTkTermBase.setEcho(False) TTkTermBase.CRNL(False) @@ -148,5 +152,5 @@ class TTkTermBase(): flush = lambda *args: None setEcho = lambda *args: None CRNL = lambda *args: None - getTerminalSize = lambda *args: None + getTerminalSize = lambda *args: (80,24) registerResizeCb = lambda *args: None \ No newline at end of file diff --git a/TermTk/TTkCore/canvas.py b/TermTk/TTkCore/canvas.py index eab9dbfb..7ffc0543 100644 --- a/TermTk/TTkCore/canvas.py +++ b/TermTk/TTkCore/canvas.py @@ -36,21 +36,18 @@ class TTkCanvas: :param height: the height of the Canvas ''' __slots__ = ( - '_widget', '_width', '_height', '_newWidth', '_newHeight', - '_theme', '_data', '_colors', '_bufferedData', '_bufferedColors', '_visible', '_transparent', '_doubleBuffer') def __init__(self, *args, **kwargs): - self._widget = kwargs.get('widget', None) self._visible = True self._transparent = False self._doubleBuffer = False self._width = 0 self._height = 0 - self._data = [[0]] - self._colors = [[TTkColor.RST]] + self._data = [[]] + self._colors = [[]] self._newWidth = kwargs.get('width', 0 ) self._newHeight = kwargs.get('height', 0 ) self.updateSize() @@ -63,8 +60,6 @@ class TTkCanvas: def setTransparent(self, tr): self._transparent = tr - def getWidget(self): return self._widget - def enableDoubleBuffer(self): self._doubleBuffer = True self._bufferedData, self._bufferedColors = self.copyBuffers() @@ -638,7 +633,7 @@ class TTkCanvas: if bx+bw<0 or by+bh<0 or bx>=cw or by>=ch: return if x+w<=bx or y+h<=by or bx+bw<=x or by+bh<=y: return - if (0,0,cw,ch)==geom==bound and (cw,ch)==canvas.size(): + if (0,0,cw,ch)==geom==bound and (cw,ch)==canvas.size() and not canvas._transparent: # fast Copy # the canvas match exactly on top of the current one for y in range(h): diff --git a/TermTk/TTkCore/constant.py b/TermTk/TTkCore/constant.py index efd19d96..4d0fde66 100644 --- a/TermTk/TTkCore/constant.py +++ b/TermTk/TTkCore/constant.py @@ -128,6 +128,21 @@ class TTkConstant: # InsertAlphabetically = 0x06 # '''The string is inserted in the alphabetic order in the combobox.''' + class DragDropMode(int): + '''Specifies the Drag and Drop mode allowed by this widget''' + NoDragDrop = 0x00 + '''No Drag and Drop is allowed''' + AllowDrag = 0x01 + '''Drag allowed''' + AllowDrop = 0x02 + '''Drop allowed''' + AllowDragDrop = 0x03 + '''Drag and Drop allowed''' + NoDragDrop = DragDropMode.NoDragDrop + AllowDrag = DragDropMode.AllowDrag + AllowDrop = DragDropMode.AllowDrop + AllowDragDrop = DragDropMode.AllowDragDrop + class ChildIndicatorPolicy(int): ShowIndicator = 0x00 #The controls for expanding and collapsing will be shown for this item even if there are no children. DontShowIndicator = 0x01 #The controls for expanding and collapsing will never be shown even if there are children. If the node is forced open the user will not be able to expand or collapse the item. diff --git a/TermTk/TTkCore/drivers/__init__.py b/TermTk/TTkCore/drivers/__init__.py new file mode 100644 index 00000000..c2a77f6f --- /dev/null +++ b/TermTk/TTkCore/drivers/__init__.py @@ -0,0 +1,15 @@ +import importlib.util +import platform + +if importlib.util.find_spec('pyodideProxy'): + from .pyodide import * + from .term_pyodide import * +elif platform.system() == 'Linux': + from .unix import * + from .term_unix import * +elif platform.system() == 'Darwin': + from .unix import * + from .term_unix import * +elif platform.system() == 'Windows': + from .windows import * + from .term_windows import * diff --git a/TermTk/TTkCore/drivers/pyodide.py b/TermTk/TTkCore/drivers/pyodide.py new file mode 100644 index 00000000..de482624 --- /dev/null +++ b/TermTk/TTkCore/drivers/pyodide.py @@ -0,0 +1,44 @@ +# MIT License +# +# Copyright (c) 2023 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. + +__all__ = ['TTkSignalDriver','TTkInputDriver'] + +from pyodide import __version__ as pyodideVersion + +from TermTk.TTkCore.signal import pyTTkSignal +from TermTk.TTkCore.log import TTkLog + +class TTkInputDriver(): + def close(self): pass + def cont(self): pass + def read(self): pass + + +class TTkSignalDriver(): + sigStop = pyTTkSignal() + sigCont = pyTTkSignal() + sigInt = pyTTkSignal() + + @staticmethod + def init(): + TTkLog.info(f"Pyodide Version:\033[38;5;11m{pyodideVersion}") + def exit(): pass \ No newline at end of file diff --git a/TermTk/TTkCore/TTkTerm/term_pyodide.py b/TermTk/TTkCore/drivers/term_pyodide.py similarity index 96% rename from TermTk/TTkCore/TTkTerm/term_pyodide.py rename to TermTk/TTkCore/drivers/term_pyodide.py index f3799976..17075987 100644 --- a/TermTk/TTkCore/TTkTerm/term_pyodide.py +++ b/TermTk/TTkCore/drivers/term_pyodide.py @@ -20,9 +20,11 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +__all__ = ['TTkTerm'] + import pyodideProxy -from .term_base import TTkTermBase +from ..TTkTerm.term_base import TTkTermBase class TTkTerm(TTkTermBase): @staticmethod diff --git a/TermTk/TTkCore/TTkTerm/term_unix.py b/TermTk/TTkCore/drivers/term_unix.py similarity index 98% rename from TermTk/TTkCore/TTkTerm/term_unix.py rename to TermTk/TTkCore/drivers/term_unix.py index e8b4cf1c..725cc71d 100644 --- a/TermTk/TTkCore/TTkTerm/term_unix.py +++ b/TermTk/TTkCore/drivers/term_unix.py @@ -20,6 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +__all__ = ['TTkTerm'] + import sys, os, signal from threading import Thread, Lock @@ -28,7 +30,7 @@ except Exception as e: print(f'ERROR: {e}') exit(1) -from .term_base import TTkTermBase +from ..TTkTerm.term_base import TTkTermBase from TermTk.TTkCore.log import TTkLog class TTkTerm(TTkTermBase): diff --git a/TermTk/TTkCore/drivers/term_windows.py b/TermTk/TTkCore/drivers/term_windows.py new file mode 100644 index 00000000..f0506724 --- /dev/null +++ b/TermTk/TTkCore/drivers/term_windows.py @@ -0,0 +1,74 @@ +# MIT License +# +# Copyright (c) 2022 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. + +__all__ = ['TTkTerm'] + +import sys, os +from threading import Thread, Lock + +from ..TTkTerm.term_base import TTkTermBase +from TermTk.TTkCore.log import TTkLog +from .windows import * + +class TTkTerm(TTkTermBase): + @staticmethod + def _push(*args): + try: + sys.stdout.write(str(*args)) + sys.stdout.flush() + except BlockingIOError as e: + TTkLog.fatal(f"{e=} {e.characters_written=}") + except Exception as e: + TTkLog.fatal(e) + TTkTermBase.push = _push + + @staticmethod + def _flush(): + sys.stdout.flush() + TTkTermBase.flush = _flush + + @staticmethod + def _getTerminalSize(): + try: + return os.get_terminal_size() + except OSError as e: + print(f'ERROR: {e}') + TTkTermBase.getTerminalSize = _getTerminalSize + + _sigWinChMutex = Lock() + + @staticmethod + def _sigWinCh(w,h): + def _sigWinChThreaded(): + if not TTkTerm._sigWinChMutex.acquire(blocking=False): return + while (TTkTerm.width, TTkTerm.height) != (wh:=TTkTerm.getTerminalSize()): + TTkTerm.width, TTkTerm.height = wh + if TTkTerm._sigWinChCb is not None: + TTkTerm._sigWinChCb(TTkTerm.width, TTkTerm.height) + TTkTerm._sigWinChMutex.release() + Thread(target=_sigWinChThreaded).start() + + @staticmethod + def _registerResizeCb(callback): + TTkTerm._sigWinChCb = callback + TTkInputDriver.windowResized.connect(TTkTerm._sigWinCh) + TTkTermBase.registerResizeCb = _registerResizeCb \ No newline at end of file diff --git a/TermTk/TTkCore/TTkTerm/readinputlinux.py b/TermTk/TTkCore/drivers/unix.py similarity index 75% rename from TermTk/TTkCore/TTkTerm/readinputlinux.py rename to TermTk/TTkCore/drivers/unix.py index 0ba4b635..66b3ab64 100644 --- a/TermTk/TTkCore/TTkTerm/readinputlinux.py +++ b/TermTk/TTkCore/drivers/unix.py @@ -20,7 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +__all__ = ['TTkSignalDriver','TTkInputDriver'] + import sys, os, re +import signal from select import select try: import fcntl, termios, tty @@ -28,8 +31,10 @@ except Exception as e: print(f'ERROR: {e}') exit(1) +from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot + -class ReadInput(): +class TTkInputDriver(): __slots__ = ('_readPipe','_attr') def __init__(self): @@ -64,3 +69,24 @@ class ReadInput(): else: for ch in sr: yield ch + + + +class TTkSignalDriver(): + sigStop = pyTTkSignal() + sigCont = pyTTkSignal() + sigInt = pyTTkSignal() + + @staticmethod + def init(): + # Register events + signal.signal(signal.SIGTSTP, TTkSignalDriver._SIGSTOP) # Ctrl-Z + signal.signal(signal.SIGCONT, TTkSignalDriver._SIGCONT) # Resume + signal.signal(signal.SIGINT, TTkSignalDriver._SIGINT) # Ctrl-C + + def exit(): + signal.signal(signal.SIGINT, signal.SIG_DFL) + + def _SIGSTOP(signum, frame): TTkSignalDriver.sigStop.emit() + def _SIGCONT(signum, frame): TTkSignalDriver.sigCont.emit() + def _SIGINT( signum, frame): TTkSignalDriver.sigInt.emit() diff --git a/TermTk/TTkCore/TTkTerm/readinputlinux_thread.py b/TermTk/TTkCore/drivers/unix_thread.py similarity index 100% rename from TermTk/TTkCore/TTkTerm/readinputlinux_thread.py rename to TermTk/TTkCore/drivers/unix_thread.py diff --git a/TermTk/TTkCore/drivers/windows.py b/TermTk/TTkCore/drivers/windows.py new file mode 100644 index 00000000..007bea5c --- /dev/null +++ b/TermTk/TTkCore/drivers/windows.py @@ -0,0 +1,368 @@ +# MIT License +# +# Copyright (c) 2023 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. + +__all__ = ['TTkSignalDriver','TTkInputDriver'] + +import signal + +from ctypes import Structure, Union, byref, wintypes, windll + +from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot + +from TermTk.TTkCore.log import TTkLog + +# Based on the example ported from: +# https://learn.microsoft.com/en-us/windows/console/reading-input-buffer-events +# https://github.com/ceccopierangiolieugenio/pyTermTk -> tests/test.input.win.01.py + +# https://learn.microsoft.com/en-us/windows/console/getstdhandle +STD_INPUT_HANDLE = wintypes.DWORD(-10) # The standard input device. Initially, this is the console input buffer, CONIN$. +STD_OUTPUT_HANDLE = wintypes.DWORD(-11) # The standard output device. Initially, this is the active console screen buffer, CONOUT$. +STD_ERROR_HANDLE = wintypes.DWORD(-12) # The standard error device. Initially, this is the active console screen buffer, CONOUT$. + +INVALID_HANDLE_VALUE = -1 # WinBase.h + +# https://learn.microsoft.com/en-us/windows/console/SetConsoleMode +ENABLE_ECHO_INPUT = 0x0004 # Characters read by the ReadFile or ReadConsole function are written to the active screen buffer as they are typed into the console. This mode can be used only if the ENABLE_LINE_INPUT mode is also enabled. +ENABLE_INSERT_MODE = 0x0020 # When enabled, text entered in a console window will be inserted at the current cursor location and all text following that location will not be overwritten. When disabled, all following text will be overwritten. +ENABLE_LINE_INPUT = 0x0002 # The ReadFile or ReadConsole function returns only when a carriage return character is read. If this mode is disabled, the functions return when one or more characters are available. +ENABLE_MOUSE_INPUT = 0x0010 # If the mouse pointer is within the borders of the console window and the window has the keyboard focus, mouse events generated by mouse movement and button presses are placed in the input buffer. These events are discarded by ReadFile or ReadConsole, even when this mode is enabled. The ReadConsoleInput function can be used to read MOUSE_EVENT input records from the input buffer. +ENABLE_PROCESSED_INPUT = 0x0001 # CTRL+C is processed by the system and is not placed in the input buffer. If the input buffer is being read by ReadFile or ReadConsole, other control keys are processed by the system and are not returned in the ReadFile or ReadConsole buffer. If the ENABLE_LINE_INPUT mode is also enabled, backspace, carriage return, and line feed characters are handled by the system. +ENABLE_QUICK_EDIT_MODE = 0x0040 # This flag enables the user to use the mouse to select and edit text. To enable this mode, use ENABLE_QUICK_EDIT_MODE | ENABLE_EXTENDED_FLAGS. To disable this mode, use ENABLE_EXTENDED_FLAGS without this flag. +ENABLE_WINDOW_INPUT = 0x0008 # User interactions that change the size of the console screen buffer are reported in the console's input buffer. Information about these events can be read from the input buffer by applications using the ReadConsoleInput function, but not by those using ReadFile or ReadConsole. +ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200 # Setting this flag directs the Virtual Terminal processing engine to convert user input received by the console window into Console Virtual Terminal Sequences that can be retrieved by a supporting application through ReadFile or ReadConsole functions. + +ENABLE_PROCESSED_OUTPUT = 0x0001 +ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 +DISABLE_NEWLINE_AUTO_RETURN = 0x0008 +ENABLE_LVB_GRID_WORLDWIDE = 0x0010 + +# https://learn.microsoft.com/en-us/windows/console/input-record-str +FOCUS_EVENT = 0x0010 # The Event member contains a FOCUS_EVENT_RECORD structure. These events are used internally and should be ignored. +KEY_EVENT = 0x0001 # The Event member contains a KEY_EVENT_RECORD structure with information about a keyboard event. +MENU_EVENT = 0x0008 # The Event member contains a MENU_EVENT_RECORD structure. These events are used internally and should be ignored. +MOUSE_EVENT = 0x0002 # The Event member contains a MOUSE_EVENT_RECORD structure with information about a mouse movement or button press event. +WINDOW_BUFFER_SIZE_EVENT = 0x0004 # The Event member contains a WINDOW_BUFFER_SIZE_RECORD structure with information about the new size of the console screen buffer. + +# https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str +# dwButtonState +FROM_LEFT_1ST_BUTTON_PRESSED = 0x0001 # The leftmost mouse button. +FROM_LEFT_2ND_BUTTON_PRESSED = 0x0004 # The second button fom the left. +FROM_LEFT_3RD_BUTTON_PRESSED = 0x0008 # The third button from the left. +FROM_LEFT_4TH_BUTTON_PRESSED = 0x0010 # The fourth button from the left. +RIGHTMOST_BUTTON_PRESSED = 0x0002 # The rightmost mouse button. +# dwControlKeyState +CAPSLOCK_ON = 0x0080 # The CAPS LOCK light is on. +ENHANCED_KEY = 0x0100 # The key is enhanced. See remarks. +LEFT_ALT_PRESSED = 0x0002 # The left ALT key is pressed. +LEFT_CTRL_PRESSED = 0x0008 # The left CTRL key is pressed. +NUMLOCK_ON = 0x0020 # The NUM LOCK light is on. +RIGHT_ALT_PRESSED = 0x0001 # The right ALT key is pressed. +RIGHT_CTRL_PRESSED = 0x0004 # The right CTRL key is pressed. +SCROLLLOCK_ON = 0x0040 # The SCROLL LOCK light is on. +SHIFT_PRESSED = 0x0010 # The SHIFT key is pressed. +# dwEventFlags +DOUBLE_CLICK = 0x0002 # The second click (button press) of a double-click occurred. The first click is returned as a regular button-press event. +MOUSE_HWHEELED = 0x0008 # The horizontal mouse wheel was moved. + # If the high word of the dwButtonState member contains a positive value, the wheel was rotated to the right. Otherwise, the wheel was rotated to the left. +MOUSE_MOVED = 0x0001 # A change in mouse position occurred. +MOUSE_WHEELED = 0x0004 # The vertical mouse wheel was moved. + # If the high word of the dwButtonState member contains a positive value, the wheel was rotated forward, away from the user. Otherwise, the wheel was rotated backward, toward the user. + +# https://docs.microsoft.com/en-us/windows/console/coord-str +# +# typedef struct _COORD { +# SHORT X; +# SHORT Y; +# } COORD, *PCOORD; +class COORD(Structure): + _fields_ = [ + ("X", wintypes.SHORT), + ("Y", wintypes.SHORT)] + + +# https://docs.microsoft.com/en-us/windows/console/key-event-record-str +# +# typedef struct _KEY_EVENT_RECORD { +# BOOL bKeyDown; +# WORD wRepeatCount; +# WORD wVirtualKeyCode; +# WORD wVirtualScanCode; +# union { +# WCHAR UnicodeChar; +# CHAR AsciiChar; +# } uChar; +# DWORD dwControlKeyState; +# } KEY_EVENT_RECORD; +class KEY_EVENT_RECORD(Structure): + class _uChar(Union): + _fields_ = [ + ("UnicodeChar", wintypes.WCHAR) , + ("AsciiChar" , wintypes.CHAR ) ] + + _fields_ = [ + ("bKeyDown" , wintypes.BOOL ), + ("wRepeatCount" , wintypes.WORD ), + ("wVirtualKeyCode" , wintypes.WORD ), + ("wVirtualScanCode" , wintypes.WORD ), + ("uChar" , _uChar ), + ("dwControlKeyState", wintypes.DWORD)] + + +# https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str +# +# typedef struct _MOUSE_EVENT_RECORD { +# COORD dwMousePosition; +# DWORD dwButtonState; +# DWORD dwControlKeyState; +# DWORD dwEventFlags; +# } MOUSE_EVENT_RECORD; +class MOUSE_EVENT_RECORD(Structure): + _fields_ = [ + ("dwMousePosition" , COORD), + ("dwButtonState" , wintypes.DWORD), + ("dwControlKeyState", wintypes.DWORD), + ("dwEventFlags" , wintypes.DWORD)] + + +# https://docs.microsoft.com/en-us/windows/console/window-buffer-size-record-str +# +# typedef struct _WINDOW_BUFFER_SIZE_RECORD { +# COORD dwSize; +# } WINDOW_BUFFER_SIZE_RECORD; +class WINDOW_BUFFER_SIZE_RECORD(Structure): + _fields_ = [("dwSize", COORD)] + + +# https://docs.microsoft.com/en-us/windows/console/menu-event-record-str +# +# typedef struct _MENU_EVENT_RECORD { +# UINT dwCommandId; +# } MENU_EVENT_RECORD, *PMENU_EVENT_RECORD; +class MENU_EVENT_RECORD(Structure): + _fields_ = [("dwCommandId", wintypes.UINT)] + + +# https://docs.microsoft.com/en-us/windows/console/focus-event-record-str +# +# typedef struct _FOCUS_EVENT_RECORD { +# BOOL bSetFocus; +# } FOCUS_EVENT_RECORD; +class FOCUS_EVENT_RECORD(Structure): + _fields_ = [("bSetFocus", wintypes.BOOL)] + + +# https://docs.microsoft.com/en-us/windows/console/input-record-str +# +# typedef struct _INPUT_RECORD { +# WORD EventType; +# union { +# KEY_EVENT_RECORD KeyEvent; +# MOUSE_EVENT_RECORD MouseEvent; +# WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent; +# MENU_EVENT_RECORD MenuEvent; +# FOCUS_EVENT_RECORD FocusEvent; +# } Event; +# } INPUT_RECORD; +class INPUT_RECORD(Structure): + class _Event(Union): + _fields_ = [ + ("KeyEvent" , KEY_EVENT_RECORD ), + ("MouseEvent" , MOUSE_EVENT_RECORD ), + ("WindowBufferSizeEvent", WINDOW_BUFFER_SIZE_RECORD), + ("MenuEvent" , MENU_EVENT_RECORD ), + ("FocusEvent" , FOCUS_EVENT_RECORD )] + + _fields_ = [ + ("EventType", wintypes.WORD), + ("Event" , _Event )] + + +class TTkInputDriver(): + windowResized = pyTTkSignal(int,int) + def __init__(self): + self._run = True + self._initTerminal() + + def _initTerminal(self): + # Get the standard input handle. + # From: + # https://learn.microsoft.com/en-us/windows/console/getstdhandle + # + # HANDLE WINAPI GetStdHandle( + # _In_ DWORD nStdHandle + # ); + GetStdHandle = windll.kernel32.GetStdHandle + GetStdHandle.argtypes = [wintypes.DWORD] + GetStdHandle.restype = wintypes.HANDLE + + self._hStdIn = GetStdHandle(STD_INPUT_HANDLE) + if self._hStdIn == INVALID_HANDLE_VALUE: + raise Exception("GetStdHandle") + + self._hStdOut = GetStdHandle(STD_OUTPUT_HANDLE) + if self._hStdOut == INVALID_HANDLE_VALUE: + raise Exception("GetStdHandle") + + # Save the current input mode, to be restored on exit. + # From: + # https://learn.microsoft.com/en-us/windows/console/GetConsoleMode + # + # BOOL WINAPI GetConsoleMode( + # _In_ HANDLE hConsoleHandle, + # _Out_ LPDWORD lpMode + # ); + self._GetConsoleMode = windll.kernel32.GetConsoleMode + self._GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD] + self._GetConsoleMode.restype = wintypes.BOOL + + self._fdwSaveOldModeIn = wintypes.DWORD() + if not self._GetConsoleMode(self._hStdIn, byref(self._fdwSaveOldModeIn)): + raise Exception("GetConsoleMode") + + self._fdwSaveOldModeOut = wintypes.DWORD() + if not self._GetConsoleMode(self._hStdOut, byref(self._fdwSaveOldModeOut)): + raise Exception("GetConsoleMode") + + # TTkLog.debug(f"{fdwSaveOldModeIn.value=:02x}") + # TTkLog.debug(f"{fdwSaveOldModeOut.value=:02x}") + + # Enable the window and mouse input events. + # From: + # https://learn.microsoft.com/en-us/windows/console/SetConsoleMode + # + # BOOL WINAPI SetConsoleMode( + # _In_ HANDLE hConsoleHandle, + # _In_ DWORD dwMode + # ); + self._SetConsoleMode = windll.kernel32.SetConsoleMode + self._SetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.DWORD] + self._SetConsoleMode.restype = wintypes.BOOL + + fdwModeIn = ENABLE_VIRTUAL_TERMINAL_INPUT + # fdwModeIn = ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT + # fdwModeIn = 0x0218 + if not self._SetConsoleMode(self._hStdIn, fdwModeIn): + raise Exception("SetConsoleMode") + + fdwModeOut = self._fdwSaveOldModeOut.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING + # fdwModeIn = 0x0218 + if not self._SetConsoleMode(self._hStdOut, fdwModeOut): + raise Exception("SetConsoleMode") + + # TTkLog.debug(f"{fdwModeIn=:02x}") + # TTkLog.debug(f"{fdwModeOut=:02x}") + + def close(self): + self._run = False + # Restore input mode on exit. + if not self._SetConsoleMode(self._hStdIn, self._fdwSaveOldModeIn): + raise Exception("SetConsoleMode") + + if not self._SetConsoleMode(self._hStdOut, self._fdwSaveOldModeOut): + raise Exception("SetConsoleMode") + + def cont(self): + pass + + def read(self) -> str|None: + # From: + # https://learn.microsoft.com/en-us/windows/console/ReadConsoleInput + # + # BOOL WINAPI ReadConsoleInput( + # _In_ HANDLE hConsoleInput, + # _Out_ PINPUT_RECORD lpBuffer, + # _In_ DWORD nLength, + # _Out_ LPDWORD lpNumberOfEventsRead + # ); + + ReadConsoleInput = windll.kernel32.ReadConsoleInputW # Unicode + # ReadConsoleInput = windll.kernel32.ReadConsoleInputA # ANSII + # ReadConsoleInput.argtypes = [wintypes.HANDLE, + # wintypes.LPINT, + # wintypes.DWORD, + # wintypes.LPWORD] + ReadConsoleInput.restype = wintypes.BOOL + + # DWORD cNumRead; + # INPUT_RECORD irInBuf[128]; + cNumRead = wintypes.DWORD(0) + irInBuf = (INPUT_RECORD * 256)() + + # Loop to read and handle the next 100 input events. + while self._run: + # Wait for the events. + if not ReadConsoleInput( + self._hStdIn, # input buffer handle + byref(irInBuf), # buffer to read into + 256, # size of read buffer + byref(cNumRead)): # number of records read + raise Exception("ReadConsoleInput") + + # TTkLog.debug(f"{self._hStdIn=} {irInBuf=} {cNumRead=}") + # TTkLog.debug(f"{cNumRead=}") + + # Dispatch the events to the appropriate handler. + saveKeys = [] + for bb in irInBuf[:cNumRead.value]: + # if not bb.EventType: continue + # TTkLog.debug(f"{bb=} {bb.EventType=} {cNumRead.value=}") + + if bb.EventType == KEY_EVENT: + ke = bb.Event.KeyEvent + if ( not ke.bKeyDown or + ke.dwControlKeyState or + ke.wVirtualKeyCode ): + continue + saveKeys.append(ke.uChar.UnicodeChar) + elif bb.EventType == MOUSE_EVENT: + # It is not supposed to receive Mouse Events + # due to ENABLE_VIRTUAL_TERMINAL_PROCESSING + # everything is received as ANSI sequence + pass + elif bb.EventType == WINDOW_BUFFER_SIZE_EVENT: + # TTkLog.debug(f"{bb.Event.WindowBufferSizeEvent=}") + # TTkLog.debug(f"{bb.Event.WindowBufferSizeEvent.dwSize.X=}") + # TTkLog.debug(f"{bb.Event.WindowBufferSizeEvent.dwSize.Y=}") + TTkInputDriver.windowResized.emit(bb.Event.WindowBufferSizeEvent.dwSize.X, bb.Event.WindowBufferSizeEvent.dwSize.Y) + if saveKeys: + yield "".join(saveKeys).encode("utf-16", "surrogatepass").decode("utf-16") + +class TTkSignalDriver(): + sigStop = pyTTkSignal() + sigCont = pyTTkSignal() + sigInt = pyTTkSignal() + + @staticmethod + def init(): + # Register events + # signal.signal(signal.SIGTSTP, TTkSignalDriver._SIGSTOP) # Ctrl-Z + # signal.signal(signal.SIGCONT, TTkSignalDriver._SIGCONT) # Resume + signal.signal(signal.SIGINT, TTkSignalDriver._SIGINT) # Ctrl-C + + def exit(): + signal.signal(signal.SIGINT, signal.SIG_DFL) + + def _SIGSTOP(signum, frame): TTkSignalDriver.sigStop.emit() + def _SIGCONT(signum, frame): TTkSignalDriver.sigCont.emit() + def _SIGINT( signum, frame): TTkSignalDriver.sigInt.emit() \ No newline at end of file diff --git a/TermTk/TTkCore/helper.py b/TermTk/TTkCore/helper.py index e1f88bda..4927f075 100644 --- a/TermTk/TTkCore/helper.py +++ b/TermTk/TTkCore/helper.py @@ -414,7 +414,7 @@ class TTkHelper: if w == widget: widget=None continue - if w.focusPolicy() & TTkK.TabFocus == TTkK.TabFocus: + if w.isEnabled() and w.focusPolicy() & TTkK.TabFocus == TTkK.TabFocus: w.setFocus() w.update() return @@ -436,7 +436,7 @@ class TTkHelper: widget=None if prev: break - if w.focusPolicy() & TTkK.TabFocus == TTkK.TabFocus: + if w.isEnabled() and w.focusPolicy() & TTkK.TabFocus == TTkK.TabFocus: prev = w if prev: prev.setFocus() diff --git a/TermTk/TTkCore/string.py b/TermTk/TTkCore/string.py index bc1ee1db..b8f1f180 100644 --- a/TermTk/TTkCore/string.py +++ b/TermTk/TTkCore/string.py @@ -77,11 +77,12 @@ class TTkString(): @staticmethod def _importString1(text, colors): ret = TTkString() - ret._text = text - ret._colors = colors - ret._baseColor = colors[-1] - ret._hasTab = '\t' in text - ret._checkWidth() + if text and colors: + ret._text = text + ret._colors = colors + ret._baseColor = colors[-1] if colors else TTkColor.RST + ret._hasTab = '\t' in text + ret._checkWidth() return ret @staticmethod diff --git a/TermTk/TTkCore/ttk.py b/TermTk/TTkCore/ttk.py index fe0347cf..2e095268 100644 --- a/TermTk/TTkCore/ttk.py +++ b/TermTk/TTkCore/ttk.py @@ -29,6 +29,7 @@ import queue import threading import platform +from TermTk.TTkCore.drivers import * from TermTk.TTkCore.TTkTerm.input import TTkInput from TermTk.TTkCore.TTkTerm.inputkey import TTkKeyEvent from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent @@ -40,19 +41,18 @@ from TermTk.TTkCore.cfg import TTkCfg, TTkGlbl from TermTk.TTkCore.helper import TTkHelper from TermTk.TTkCore.timer import TTkTimer from TermTk.TTkCore.color import TTkColor -from TermTk.TTkTheme.theme import TTkTheme from TermTk.TTkWidgets.widget import TTkWidget from TermTk.TTkWidgets.container import TTkContainer class TTk(TTkContainer): class _mouseCursor(TTkWidget): __slots__ = ('_cursor','_color') - def __init__(self, input): + def __init__(self): super().__init__(name='MouseCursor') self._cursor = '✠' self._color = TTkColor.RST self.resize(1,1) - input.inputEvent.connect(self._mouseInput) + TTkInput.inputEvent.connect(self._mouseInput) @pyTTkSlot(TTkKeyEvent, TTkMouseEvent) def _mouseInput(self, _, mevt): if mevt is not None: @@ -99,9 +99,11 @@ class TTk(TTkContainer): super().__init__(*args, **kwargs) self._termMouse = True self._termDirectMouse = kwargs.get('mouseTrack',False) - self._input = TTkInput() - self._input.inputEvent.connect(self._processInput) - self._input.pasteEvent.connect(self._processPaste) + TTkInput.inputEvent.connect(self._processInput) + TTkInput.pasteEvent.connect(self._processPaste) + TTkSignalDriver.sigStop.connect(self._SIGSTOP) + TTkSignalDriver.sigCont.connect(self._SIGCONT) + TTkSignalDriver.sigInt.connect( self._SIGINT) self._title = kwargs.get('title','TermTk') self._sigmask = kwargs.get('sigmask', TTkK.NONE) self._showMouseCursor = os.environ.get("TTK_MOUSE",kwargs.get('mouseCursor', False)) @@ -145,11 +147,10 @@ class TTk(TTkContainer): TTkLog.debug(f" Version: {TTkCfg.version}" ) TTkLog.debug( "" ) TTkLog.debug( "Starting Main Loop..." ) + TTkLog.debug(f"screen = ({TTkTerm.getTerminalSize()})") # Register events - signal.signal(signal.SIGTSTP, self._SIGSTOP) # Ctrl-Z - signal.signal(signal.SIGCONT, self._SIGCONT) # Resume - signal.signal(signal.SIGINT, self._SIGINT) # Ctrl-C + TTkSignalDriver.init() TTkLog.debug("Signal Event Registered") @@ -162,28 +163,29 @@ class TTk(TTkContainer): # Keep track of the multiTap to avoid the extra key release self._lastMultiTap = False + TTkInput.init( + mouse=self._termMouse, + directMouse=self._termDirectMouse) TTkTerm.init( title=self._title, - sigmask=self._sigmask, - mouse=self._termMouse, - directMouse=self._termDirectMouse ) + sigmask=self._sigmask) if self._showMouseCursor: TTkTerm.push(TTkTerm.Mouse.DIRECT_ON) - m = TTk._mouseCursor(self._input) + m = TTk._mouseCursor() self.rootLayout().addWidget(m) self._mainLoop() finally: if platform.system() != 'Emscripten': - signal.signal(signal.SIGINT, signal.SIG_DFL) + TTkSignalDriver.exit() self.quit() TTkTerm.exit() def _mainLoop(self): if platform.system() == 'Emscripten': return - self._input.start() + TTkInput.start() @pyTTkSlot(str) def _processPaste(self, txt:str): @@ -325,28 +327,31 @@ class TTk(TTkContainer): '''Tells the application to exit with a return code.''' if self._timer: self._timer.timeout.disconnect(self._time_event) - self._input.inputEvent.clear() + TTkInput.inputEvent.clear() self._paintEvent.set() - self._input.close() + TTkInput.close() - def _SIGSTOP(self, signum, frame): + @pyTTkSlot() + def _SIGSTOP(self): """Reset terminal settings and stop background input read before putting to sleep""" TTkLog.debug("Captured SIGSTOP ") TTkTerm.stop() - self._input.stop() + TTkInput.stop() # TODO: stop the threads os.kill(os.getpid(), signal.SIGSTOP) - def _SIGCONT(self, signum, frame): + @pyTTkSlot() + def _SIGCONT(self): """Set terminal settings and restart background input read""" TTkLog.debug("Captured SIGCONT 'fg/bg'") TTkTerm.cont() - self._input.cont() + TTkInput.cont() TTkHelper.rePaintAll() # TODO: Restart threads # TODO: Redraw the screen - def _SIGINT(self, signum, fraTERMTK_STACKTRACEme): + @pyTTkSlot() + def _SIGINT(self): # If the "TERMTK_STACKTRACE" env variable is defined # a stacktrace file is generated once CTRL+C is pressed # i.e. diff --git a/TermTk/TTkGui/textcursor.py b/TermTk/TTkGui/textcursor.py index 0ed2fc65..8c5d68f6 100644 --- a/TermTk/TTkGui/textcursor.py +++ b/TermTk/TTkGui/textcursor.py @@ -459,7 +459,7 @@ class TTkTextCursor(): splitAfter = self._document._dataLines[line].substring(fr=pos) xFrom = pos xTo = pos - selectRE = '[^ \t\r\n\(\)\[\]\.\,\+\-\*\/]*' + selectRE = r'[^ \t\r\n()[\]\.\,\+\-\*\/]*' if m := splitBefore.search(selectRE+'$'): xFrom -= len(m.group(0)) if m := splitAfter.search('^'+selectRE): diff --git a/TermTk/TTkTestWidgets/keypressview.py b/TermTk/TTkTestWidgets/keypressview.py index 74bf1f3d..7fb4b90b 100644 --- a/TermTk/TTkTestWidgets/keypressview.py +++ b/TermTk/TTkTestWidgets/keypressview.py @@ -22,6 +22,7 @@ __all__ = ['TTkKeyPressView'] +from TermTk.TTkCore.TTkTerm.input import TTkInput from TermTk.TTkCore.TTkTerm.inputkey import TTkKeyEvent, mod2str, key2str from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent from TermTk.TTkCore.helper import TTkHelper @@ -38,7 +39,7 @@ class TTkKeyPressView(TTkWidget): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - TTkHelper._rootWidget._input.inputEvent.connect(self._processInput) + TTkInput.inputEvent.connect(self._processInput) self._keys = [] self._fadeDuration = 2.5 self._anim = TTkPropertyAnimation(self, '_pushFade') diff --git a/TermTk/TTkUiTools/properties/__init__.py b/TermTk/TTkUiTools/properties/__init__.py index d72a9825..e69de29b 100644 --- a/TermTk/TTkUiTools/properties/__init__.py +++ b/TermTk/TTkUiTools/properties/__init__.py @@ -1,33 +0,0 @@ -# from .about import -from .button import * -from .checkbox import * -from .combobox import * -from .container import * -from .frame import * -# from .graph import -# from .image import -from .label import * -from .lineedit import * -from .list_ import * -# from .listwidget import -# from .menubar import -from .menu import * -# from .progressbar import -from .radiobutton import * -from .resizableframe import * -# from .scrollarea import -from .scrollbar import * -# from .spacer import -from .spinbox import * -from .splitter import * -# from .tabwidget import -from .texedit import * -from .widget import * -from .window import * - -# Pickers -from .colorpicker import * -from .filepicker import * - -# Layouts -from .layout import * diff --git a/TermTk/TTkUiTools/properties/list_.py b/TermTk/TTkUiTools/properties/list_.py index cb1c7693..338d19ec 100644 --- a/TermTk/TTkUiTools/properties/list_.py +++ b/TermTk/TTkUiTools/properties/list_.py @@ -22,4 +22,48 @@ __all__ = ['TTkListProperties'] -TTkListProperties = {'properties' : {},'signals' : {},'slots' : {}} \ No newline at end of file +from TermTk.TTkCore.constant import TTkK +from TermTk.TTkWidgets.list_ import TTkList +from TermTk.TTkWidgets.listwidget import TTkListWidget, TTkAbstractListItem + + +TTkListProperties = { + 'properties' : { + 'Selection Mode' : { + 'init': {'name':'selectionMode', 'type':'singleflag', + 'flags':{ + 'Single Seelction' : TTkK.SingleSelection, + 'Multi Selection' : TTkK.MultiSelection, + }}, + 'get': {'cb':lambda w: w.selectionMode(), 'type':'singleflag', + 'flags':{ + 'Single Seelction' : TTkK.SingleSelection, + 'Multi Selection' : TTkK.MultiSelection, + }}, + 'set': {'cb':lambda w,v: w.setSelectionMode(v), 'type':'singleflag', + 'flags':{ + 'Single Seelction' : TTkK.SingleSelection, + 'Multi Selection' : TTkK.MultiSelection, + }}}, + 'DnD Mode' : { + 'init': {'name':'dragDropMode', 'type':'multiflags', + 'flags':{ + 'Allow Drag' : TTkK.DragDropMode.AllowDrag, + 'Allow Drop' : TTkK.DragDropMode.AllowDrop, + }}, + 'get': {'cb':lambda w: w.dragDropMode(), 'type':'multiflags', + 'flags':{ + 'Allow Drag' : TTkK.DragDropMode.AllowDrag, + 'Allow Drop' : TTkK.DragDropMode.AllowDrop, + }}, + 'set': {'cb':lambda w,v: w.setDragDropMode(v), 'type':'multiflags', + 'flags':{ + 'Allow Drag' : TTkK.DragDropMode.AllowDrag, + 'Allow Drop' : TTkK.DragDropMode.AllowDrop, + }}}, + }, + 'signals' : { + 'itemClicked(TTkAbstractListItem)' : {'name': 'itemClicked', 'type' : TTkAbstractListItem}, + 'textClicked(str)' : {'name': 'textClicked', 'type' : str}, + }, + 'slots' : {}} \ No newline at end of file diff --git a/TermTk/TTkUiTools/properties/texedit.py b/TermTk/TTkUiTools/properties/texedit.py index cc91717f..b91b7bb4 100644 --- a/TermTk/TTkUiTools/properties/texedit.py +++ b/TermTk/TTkUiTools/properties/texedit.py @@ -33,6 +33,10 @@ TTkTextEditProperties = { 'init': {'name':'lineNumber', 'type':bool } , 'get': {'cb':TTkTextEdit.getLineNumber, 'type':bool } , 'set': {'cb':TTkTextEdit.setLineNumber, 'type':bool } }, + 'Line Number Starting': { + 'init': {'name':'lineNumberStarting', 'type':int } , + 'get': {'cb':TTkTextEdit.lineNumberStarting, 'type':int } , + 'set': {'cb':TTkTextEdit.setLineNumberStarting, 'type':int } }, 'Read Only' : { 'init': {'name':'readOnly', 'type':bool } , 'get': {'cb':lambda w: w.isReadOnly(), 'type':bool } , @@ -47,7 +51,9 @@ TTkTextEditProperties = { 'textChanged()' : {'name': 'textChanged', 'type': None}, },'slots' : { 'setText(str)' : {'name':'setText', 'type':None}, - 'setColor(TTkColor)' : {'name':'setColor', 'type':TTkColor}, + 'setColor(TTkColor)' : {'name':'setColor', 'type':TTkColor}, + 'setLineNumber(bool)' : {'name':'setLineNumber', 'type':bool}, + 'setLineNumberStarting(int)' : {'name':'setLineNumberStarting', 'type':int}, 'append(str)' : {'name':'append', 'type':None}, 'undo()' : {'name':'undo', 'type':None}, 'redo()' : {'name':'redo', 'type':None}, diff --git a/TermTk/TTkUiTools/uiproperties.py b/TermTk/TTkUiTools/uiproperties.py index 1f25e448..97268089 100644 --- a/TermTk/TTkUiTools/uiproperties.py +++ b/TermTk/TTkUiTools/uiproperties.py @@ -24,7 +24,40 @@ __all__ = ['TTkUiProperties'] from TermTk.TTkLayouts import * from TermTk.TTkWidgets import * -from .properties import * + +# from .properties.about import +from .properties.button import * +from .properties.checkbox import * +from .properties.combobox import * +from .properties.container import * +from .properties.frame import * +# from .properties.graph import +# from .properties.image import +from .properties.label import * +from .properties.lineedit import * +from .properties.list_ import * +# from .properties.listwidget import +# from .properties.menubar import +from .properties.menu import * +# from .properties.progressbar import +from .properties.radiobutton import * +from .properties.resizableframe import * +# from .properties.scrollarea import +from .properties.scrollbar import * +# from .properties.spacer import +from .properties.spinbox import * +from .properties.splitter import * +# from .properties.tabwidget import +from .properties.texedit import * +from .properties.widget import * +from .properties.window import * + +# Pickers +from .properties.colorpicker import * +from .properties.filepicker import * + +# Layouts +from .properties.layout import * TTkUiProperties = { # Widgets @@ -45,7 +78,7 @@ TTkUiProperties = { TTkTextEdit.__name__: TTkTextEditProperties, TTkWidget.__name__: TTkWidgetProperties, TTkWindow.__name__: TTkWindowProperties, - # Pickers + # Pickers TTkColorButtonPicker.__name__ : TTkColorButtonPickerProperties, TTkFileButtonPicker.__name__ : TTkFileButtonPickerProperties, # Layouts diff --git a/TermTk/TTkWidgets/TTkModelView/filetreewidgetitem.py b/TermTk/TTkWidgets/TTkModelView/filetreewidgetitem.py index bffd6ef1..4ea80aeb 100644 --- a/TermTk/TTkWidgets/TTkModelView/filetreewidgetitem.py +++ b/TermTk/TTkWidgets/TTkModelView/filetreewidgetitem.py @@ -49,7 +49,7 @@ class TTkFileTreeWidgetItem(TTkTreeWidgetItem): def _processFilter(self, filter): if self.getType() == TTkFileTreeWidgetItem.FILE: - filterRe = "^"+filter.replace('.','\.').replace('*','.*')+"$" + filterRe = "^"+filter.replace('.',r'\.').replace('*','.*')+"$" if re.match(filterRe, self._raw[0]): self.setHidden(False) else: diff --git a/TermTk/TTkWidgets/TTkModelView/treewidgetitem.py b/TermTk/TTkWidgets/TTkModelView/treewidgetitem.py index cad6035d..bfd24d79 100644 --- a/TermTk/TTkWidgets/TTkModelView/treewidgetitem.py +++ b/TermTk/TTkWidgets/TTkModelView/treewidgetitem.py @@ -156,7 +156,7 @@ class TTkTreeWidgetItem(TTkAbstractItemModel): def setTreeItemParent(self, parent): if parent: widgets = self._setTreeItemParent(parent) - parent.rootLayout().addWidgets(widgets) + parent.layout().addWidgets(widgets) else: # pw = self._parentWidget widgets = self._clearTreeItemParent() diff --git a/TermTk/TTkWidgets/TTkPickers/filepicker.py b/TermTk/TTkWidgets/TTkPickers/filepicker.py index 723240e9..e44007d5 100644 --- a/TermTk/TTkWidgets/TTkPickers/filepicker.py +++ b/TermTk/TTkWidgets/TTkPickers/filepicker.py @@ -362,7 +362,7 @@ class TTkFileDialogPicker(TTkWindow): path, e = os.path.split(path) if e: ret.append(path) - if not path or path=='/': + if not path or path=='/' or path[1:]==":\\": break return ret class TTkFileDialog: diff --git a/TermTk/TTkWidgets/TTkTerminal/terminal.py b/TermTk/TTkWidgets/TTkTerminal/terminal.py index 6a4f8fdf..61976a6c 100644 --- a/TermTk/TTkWidgets/TTkTerminal/terminal.py +++ b/TermTk/TTkWidgets/TTkTerminal/terminal.py @@ -63,7 +63,7 @@ class TTkTerminal(TTkAbstractScrollArea): self.titleChanged = self._terminalView.titleChanged self.bell = self._terminalView.bell self.terminalClosed = pyTTkSignal(TTkTerminal) - self._terminalView.closed.connect(lambda : self.terminalClosed.emit(self)) + self._terminalView.terminalClosed.connect(lambda : self.terminalClosed.emit(self)) def close(self): self._terminalView.close() diff --git a/TermTk/TTkWidgets/TTkTerminal/terminal_screen.py b/TermTk/TTkWidgets/TTkTerminal/terminal_screen.py index daf75621..db956a38 100644 --- a/TermTk/TTkWidgets/TTkTerminal/terminal_screen.py +++ b/TermTk/TTkWidgets/TTkTerminal/terminal_screen.py @@ -24,6 +24,7 @@ __all__ = [''] import collections import unicodedata +from dataclasses import dataclass from TermTk.TTkCore.canvas import TTkCanvas @@ -47,7 +48,49 @@ from .terminal_screen_CSI import _TTkTerminalScreen_CSI from .terminal_screen_C1 import _TTkTerminalScreen_C1 class _TTkTerminalScreen(_TTkTerminalScreen_CSI, _TTkTerminalScreen_C1): + + @dataclass(frozen=False) + class _SelectCursor: + @dataclass(frozen=False) + class _CP: + line: int = 0 + pos: int = 0 + def setVal(self,x,y): + self.pos=x + self.line=y + def clear(self): + self.line = 0 + self.pos = 0 + def toNum(self): + return self.pos | self.line << 16 + anchor: _CP = _CP() + position: _CP = _CP() + def __str__(self) -> str: + return f"a:({self.anchor.pos},{self.anchor.line}) p:({self.position.pos},{self.position.line})" + def select(self, x, y, moveAnchor=True): + x=max(0,x) + y=max(0,y) + self.position.setVal(x,y) + if moveAnchor: + self.anchor.setVal(x,y) + def selectionStart(self): + if self.position.toNum() > self.anchor.toNum(): + return self.anchor + else: + return self.position + def selectionEnd(self): + if self.position.toNum() >= self.anchor.toNum(): + return self.position + else: + return self.anchor + def hasSelection(self): + return self.position!=self.anchor + def clear(self): + self.anchor.clear() + self.position.clear() + __slots__ = ('_lines', '_terminalCursor', + '_selectCursor', '_scrollingRegion', '_bufferSize', '_bufferedLines', '_w', '_h', '_color', '_canvas', @@ -61,13 +104,14 @@ class _TTkTerminalScreen(_TTkTerminalScreen_CSI, _TTkTerminalScreen_C1): self.bufferedLinesChanged = pyTTkSignal() self._w = w self._h = h - self._canvasNewLine = [True]*h + self._canvasNewLine = [False]*h self._canvasLineSize = [0]*h self._last = None self._bufferSize = bufferSize self._bufferedLines = collections.deque(maxlen=bufferSize) self._terminalCursor = (0,0) self._scrollingRegion = (0,h) + self._selectCursor = _TTkTerminalScreen._SelectCursor() self._color = color self._canvas = TTkCanvas(width=w, height=h) @@ -83,8 +127,8 @@ class _TTkTerminalScreen(_TTkTerminalScreen_CSI, _TTkTerminalScreen_C1): def resize(self, w, h): # I normalize the size to the default terminal # to avoid negative or zerosized term - w = max(80,w) - h = max(24,h) + w = max(3,w) + h = max(1,h) ow, oh = self._w, self._h # st,sb = self._scrollingRegion # if oh <= h: # Terminal height decreasing @@ -94,13 +138,14 @@ class _TTkTerminalScreen(_TTkTerminalScreen_CSI, _TTkTerminalScreen_C1): # self._scrollingRegion = (st,sb) self._scrollingRegion = (0,h) if w==ow and h==oh: return + self._selectCursor.clear() self._w, self._h = w, h newCanvas = TTkCanvas(width=w, height=h) s = (0,0,w,h) newCanvas.paintCanvas(self._canvas,s,s,s) self._canvas = newCanvas - self._canvasNewLine += [True]*h + self._canvasNewLine += [False]*h self._canvasLineSize += [0]*h self._canvasNewLine = self._canvasNewLine[:h] self._canvasLineSize = self._canvasLineSize[:h] @@ -128,13 +173,13 @@ class _TTkTerminalScreen(_TTkTerminalScreen_CSI, _TTkTerminalScreen_C1): l = TTkString._getWidthText(ch) # Scroll up if we are at the right border if l+x > w: - self._canvasNewLine[y] = False x=0 y+=1 if y >= sb: self._CSI_S_SU(y-sb+1, None) # scroll up y=sb-1 self._terminalCursor = (x,y) + self._canvasNewLine[y] = True if l==1: # push normal char if irm: self._canvas._data[y][x:x] = [ch] @@ -176,6 +221,8 @@ class _TTkTerminalScreen(_TTkTerminalScreen_CSI, _TTkTerminalScreen_C1): w,h = self._w, self._h st,sb = self._scrollingRegion + self._selectCursor.clear() + lines = line.split('\n') for i,l in enumerate(lines): if i: @@ -198,10 +245,72 @@ class _TTkTerminalScreen(_TTkTerminalScreen_CSI, _TTkTerminalScreen_C1): self._terminalCursor = (x,y) self._pushTxt(lll,irm) + def select(self, x, y, moveAnchor=True): + # line = getLineFromX(x) + # pos = getPosFromX(linne,x) + # Convert x/y in line/pos + self._selectCursor.select(x,y,moveAnchor) + + def getSelected(self): + if not self._selectCursor.hasSelection(): + return "" + ret = [] + + st = self._selectCursor.selectionStart() + en = self._selectCursor.selectionEnd() + + lbl = len(self._bufferedLines) + for i in range(min(st.line,lbl),min(en.line,lbl)): + line = self._bufferedLines[i] + pa = 0 if st.line < i else st.pos + pb = len(line) if en.line > i else en.pos + ret.append(line.substring(fr=pa, to=pb)) + + w,h = self._w, self._h + for y in range(max(0,min(st.line-lbl,h)),max(0,min(en.line-lbl+1,h))): + nl = self._canvasNewLine[y] + ls = self._canvasLineSize[y] + yyy = y+lbl + pa = 0 if st.line < yyy else st.pos + pb = ls if en.line > yyy else min(ls,en.pos) + data = self._canvas._data[y][pa:pb] + colors = self._canvas._colors[y][pa:pb] + line = TTkString._importString1("".join(data),colors) + if nl and ret: + ret[-1] += line + else: + ret.append(line) + return TTkString('\n').join(ret) + + def paintEvent(self, canvas: TTkCanvas, w:int, h:int, ox:int=0, oy:int=0) -> None: w,h = self._w, self._h + st = self._selectCursor.selectionStart() + en = self._selectCursor.selectionEnd() + # draw Buffered lines ll = len(self._bufferedLines) - for y in range(ll-oy): - canvas.drawTTkString(pos=(0,y),text=self._bufferedLines[oy+y]) + color=TTkColor.fg("#ffffff")+TTkColor.bg("#008888") + for y in range(min(h,ll-oy)): + line = self._bufferedLines[oy+y] + if st.line <= (yyy:=(y+oy)) <= en.line: + pa = 0 if st.line < yyy else st.pos + pb = len(line) if en.line > yyy else en.pos + canvas.drawTTkString(pos=(0,y),text=line.setColor(posFrom=pa, posTo=pb,color=color)) + else: + canvas.drawTTkString(pos=(0,y),text=line) + # draw the Canvas s = (-ox,ll-oy,w,h) canvas.paintCanvas(self._canvas,s,s,s) + # canvas.drawText(pos=(0,0),text=f"({self._selectCursor})") + color=TTkColor.fg("#ffffff")+TTkColor.bg("#008844") + for y in range(max(st.line-oy,ll-oy),min(en.line-oy+1,h)): + did = y+oy-ll + data = self._canvas._data[did] + # colors = self._canvas._colors[did] + # nl = self._canvasNewLine[did] + ls = self._canvasLineSize[did] + yyy = y+oy + pa = 0 if st.line < yyy else st.pos + pb = ls if en.line > yyy else min(ls,en.pos) + canvas.drawText(pos=(pa,y), text="".join(data[pa:pb]), color=color) + diff --git a/TermTk/TTkWidgets/TTkTerminal/terminal_screen_CSI.py b/TermTk/TTkWidgets/TTkTerminal/terminal_screen_CSI.py index 77c683e7..e8fb9dd8 100644 --- a/TermTk/TTkWidgets/TTkTerminal/terminal_screen_CSI.py +++ b/TermTk/TTkWidgets/TTkTerminal/terminal_screen_CSI.py @@ -247,9 +247,13 @@ class _TTkTerminalScreen_CSI(): centerCNL[:ps], centerCLS[:ps]): if sz: - self._bufferedLines.append(TTkString._importString1(''.join(d[:sz]),c[:sz])) + txt = TTkString._importString1(''.join(d[:sz]),c[:sz]) else: - self._bufferedLines.append(TTkString()) + txt = TTkString() + if nl: + self._bufferedLines[-1] += txt + else: + self._bufferedLines.append(txt) # from TermTk.TTkCore.log import TTkLog # TTkLog.debug(str(self._bufferedLines[-1])+f" - {sz=} {t=} {ps=} {self._canvasLineSize=}") # Rotate the center part @@ -257,7 +261,7 @@ class _TTkTerminalScreen_CSI(): centerc = centerc[ps:] + [baseColors.copy() for _ in range(ps)] centerd = centerd[:b-t] centerc = centerc[:b-t] - centerCNL = centerCNL[ps:] + [True]*ps + centerCNL = centerCNL[ps:] + [False]*ps centerCLS = centerCLS[ps:] + [0]*ps centerCNL = centerCNL[:b-t] centerCLS = centerCLS[:b-t] diff --git a/TermTk/TTkWidgets/TTkTerminal/terminalview.py b/TermTk/TTkWidgets/TTkTerminal/terminalview.py index f676e378..c07b1a28 100644 --- a/TermTk/TTkWidgets/TTkTerminal/terminalview.py +++ b/TermTk/TTkWidgets/TTkTerminal/terminalview.py @@ -89,18 +89,19 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): reportMove: bool = False sgrMode: bool = False - __slots__ = ('_shell', '_fd', '_inout', '_pid', + __slots__ = ('_selecct', + '_shell', '_fd', '_inout', '_pid', '_quit_pipe', '_resize_pipe', '_mode_normal' - '_clipboard', + '_clipboard', '_selecting', '_buffer_lines', '_buffer_screen', '_keyboard', '_mouse', '_terminal', '_screen_current', '_screen_normal', '_screen_alt', # Signals - 'titleChanged', 'bell', 'closed') + 'titleChanged', 'bell', 'terminalClosed') def __init__(self, *args, **kwargs): self.bell = pyTTkSignal() - self.closed = pyTTkSignal() + self.terminalClosed = pyTTkSignal() self.titleChanged = pyTTkSignal(str) self._shell = os.environ.get('SHELL', 'sh') @@ -119,6 +120,7 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): self._screen_alt = _TTkTerminalScreen() self._screen_current = self._screen_normal self._clipboard = TTkClipboard() + self._selecting = False # self._screen_normal.bell.connect(lambda : _termLog.debug("BELL!!! 🔔🔔🔔")) # self._screen_alt.bell.connect( lambda : _termLog.debug("BELL!!! 🔔🔔🔔")) @@ -175,8 +177,8 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): def resizeEvent(self, w: int, h: int): if ( self._resize_pipe and - self._screen_current._w != w and - self._screen_current._h != h ): + ( self._screen_current._w != w or + self._screen_current._h != h ) ): os.write(self._resize_pipe[1], b'resize') # self._screen_alt.resize(w,h) @@ -193,9 +195,9 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): if self._pid == 0: def _spawnTerminal(argv=[self._shell], env=os.environ): os.execvpe(argv[0], argv, env) - threading.Thread(target=_spawnTerminal).start() + # threading.Thread(target=_spawnTerminal).start() TTkHelper.quit() - # _spawnTerminal() + _spawnTerminal() import sys sys.exit() # os.execvpe(argv[0], argv, env) @@ -245,17 +247,28 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): @pyTTkSlot() def _quit(self): - os.kill(self._pid,0) + if self._pid: + os.kill(self._pid,0) + os.kill(self._pid,15) if self._quit_pipe: - os.write(self._quit_pipe[1], b'quit') + try: + os.write(self._quit_pipe[1], b'quit') + except: + pass def _inputGenerator(self): while rs := select( [self._inout,self._quit_pipe[0],self._resize_pipe[0]], [], [])[0]: if self._quit_pipe[0] in rs: + # os.close(self._quit_pipe[0]) + os.close(self._quit_pipe[1]) + # os.close(self._resize_pipe[0]) + os.close(self._resize_pipe[1]) + os.close(self._fd) return if self._resize_pipe[0] in rs: self._resizeScreen() + os.read(self._resize_pipe[0],100) if self._inout not in rs: continue @@ -274,7 +287,7 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): fcntl.fcntl(self._inout, fcntl.F_SETFL, _fl) except Exception as e: _termLog.error(f"Error: {e=}") - self.closed.emit() + self.terminalClosed.emit() return # out = out.decode('utf-8','ignore') @@ -952,10 +965,44 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): self._inout.write(bah) return True - def mousePressEvent(self, evt): return self._sendMouse(evt) | True - def mouseReleaseEvent(self, evt): return self._sendMouse(evt) - def mouseDragEvent(self, evt): return self._sendMouse(evt) - def wheelEvent(self, evt): return True if self._sendMouse(evt) else super().wheelEvent(evt) + def mousePressEvent(self, evt): + if self._mouse.reportPress: + self._screen_current.select(0,0) + return self._sendMouse(evt) | True + self._selecting = True + x,y = evt.x,evt.y + ox,oy = self.getViewOffsets() + self._screen_current.select(x+ox,y+oy) + self.update() + return True + + def mouseDragEvent(self, evt): + if self._mouse.reportPress: + self._screen_current.select(0,0) + return self._sendMouse(evt) + x,y = evt.x,evt.y + ox,oy = self.getViewOffsets() + self._screen_current.select(x+ox,y+oy,moveAnchor=False) + self.update() + return True + + def mouseReleaseEvent(self, evt): + self._selecting = False + if (selected := self._screen_current.getSelected()): + self._clipboard.setText(selected) + return self._sendMouse(evt) + + def wheelEvent(self, evt): + if self._sendMouse(evt): + return True + ret = super().wheelEvent(evt) + if self._selecting: + x,y = evt.x,evt.y + ox,oy = self.getViewOffsets() + self._screen_current.select(x+ox,y+oy,moveAnchor=False) + self.update() + return ret + def mouseTapEvent(self, evt): return self._sendMouse(evt) def mouseDoubleClickEvent(self, evt): return self._sendMouse(evt) def mouseMoveEvent(self, evt): diff --git a/TermTk/TTkWidgets/button.py b/TermTk/TTkWidgets/button.py index 80b7bfe4..7e45850a 100644 --- a/TermTk/TTkWidgets/button.py +++ b/TermTk/TTkWidgets/button.py @@ -226,6 +226,9 @@ class TTkButton(TTkWidget): def keyEvent(self, evt): if ( evt.type == TTkK.Character and evt.key==" " ) or \ ( evt.type == TTkK.SpecialKey and evt.key == TTkK.Key_Enter ): + if self._checkable: + self._checked = not self._checked + self.toggled.emit(self._checked) self.update() self.clicked.emit() return True @@ -237,10 +240,13 @@ class TTkButton(TTkWidget): style = self.style()['checked'] else: style = self.style()['unchecked'] + if self.hasFocus(): + borderColor = self.style()['focus']['borderColor'] + else: + borderColor = style['borderColor'] else: style = self.currentStyle() - - borderColor = style['borderColor'] + borderColor = style['borderColor'] textColor = style['color'] grid = style['grid'] diff --git a/TermTk/TTkWidgets/kodetab.py b/TermTk/TTkWidgets/kodetab.py index 2cc2f9c9..d98478aa 100644 --- a/TermTk/TTkWidgets/kodetab.py +++ b/TermTk/TTkWidgets/kodetab.py @@ -79,11 +79,11 @@ class _TTkKodeTab(TTkTabWidget): kt._tabBarTopLayout.update() def dragEnterEvent(self, evt) -> bool: - TTkLog.debug(f"leave") + TTkLog.debug(f"Drag Enter") return True def dragLeaveEvent(self, evt) -> bool: - TTkLog.debug(f"leave") + TTkLog.debug(f"Drag Leave") self._frameOverlay = None self.update() return True diff --git a/TermTk/TTkWidgets/lineedit.py b/TermTk/TTkWidgets/lineedit.py index 2d3c22c9..e6aedcb4 100644 --- a/TermTk/TTkWidgets/lineedit.py +++ b/TermTk/TTkWidgets/lineedit.py @@ -66,8 +66,9 @@ class TTkLineEdit(TTkWidget): self._text = TTkString(kwargs.get('text' , '' )) self._inputType = kwargs.get('inputType' , TTkK.Input_Text ) super().__init__(*args, **kwargs) - if self._inputType & TTkK.Input_Number and\ - not self._text.lstrip('-').isdigit(): self._text = TTkString() + if ( self._inputType & TTkK.Input_Number and + not self._isFloat(self._text)): + self._text = TTkString('0') self.setMaximumHeight(1) self.setMinimumSize(1,1) self.setFocusPolicy(TTkK.ClickFocus + TTkK.TabFocus) @@ -156,7 +157,7 @@ class TTkLineEdit(TTkWidget): self._selectionFrom = len(before) self._selectionTo = len(before) - selectRE = '[^ \t\r\n\(\)\[\]\.\,\+\-\*\/]*' + selectRE = r'[^ \t\r\n()[\]\.\,\+\-\*\/]*' if m := before.search(selectRE+'$'): self._selectionFrom -= len(m.group(0)) @@ -181,6 +182,15 @@ class TTkLineEdit(TTkWidget): self.update() return True + @staticmethod + def _isFloat(num): + try: + float(str(num)) + return True + except: + return False + + def pasteEvent(self, txt:str): txt = TTkString().join(txt.split('\n')) @@ -198,8 +208,8 @@ class TTkLineEdit(TTkWidget): post = text.substring(fr=self._cursorPos) text = pre + txt + post - if self._inputType & TTkK.Input_Number and \ - not text.lstrip('-').isdigit(): + if ( self._inputType & TTkK.Input_Number and + not self._isFloat(text) ): return True self.setText(text, self._cursorPos+txt.termWidth()) @@ -210,12 +220,12 @@ class TTkLineEdit(TTkWidget): def keyEvent(self, evt): baseText = self._text if evt.type == TTkK.SpecialKey: - # Don't Handle the special tab key - if evt.key == TTkK.Key_Tab: + # Don't Handle the special focus switch key + if evt.key in ( + TTkK.Key_Tab, TTkK.Key_Up, TTkK.Key_Down): return False - if evt.key == TTkK.Key_Up: pass - elif evt.key == TTkK.Key_Down: pass - elif evt.key == TTkK.Key_Left: + + if evt.key == TTkK.Key_Left: if self._selectionFrom < self._selectionTo: self._cursorPos = self._selectionTo self._cursorPos = self._text.prevPos(self._cursorPos) @@ -244,8 +254,8 @@ class TTkLineEdit(TTkWidget): self._text = self._text.substring(to=prev) + self._text.substring(fr=self._cursorPos) self._cursorPos = prev - if self._inputType & TTkK.Input_Number and \ - not self._text.lstrip('-').isdigit(): + if ( self._inputType & TTkK.Input_Number and + not self._isFloat(self._text) ): self.setText('0', 1) self._pushCursor() @@ -267,8 +277,8 @@ class TTkLineEdit(TTkWidget): post = text.substring(fr=self._cursorPos) text = pre + evt.key + post - if self._inputType & TTkK.Input_Number and \ - not text.lstrip('-').isdigit(): + if ( self._inputType & TTkK.Input_Number and + not self._isFloat(text) ): return True self.setText(text, self._cursorPos+1) diff --git a/TermTk/TTkWidgets/list_.py b/TermTk/TTkWidgets/list_.py index 52850f2a..b6450e30 100644 --- a/TermTk/TTkWidgets/list_.py +++ b/TermTk/TTkWidgets/list_.py @@ -30,9 +30,12 @@ class TTkList(TTkAbstractScrollArea): __slots__ = ( '_listView', 'itemClicked', 'textClicked', # Forwarded Methods - 'items', 'addItem', 'addItemAt', 'indexOf', 'itemAt', - 'moveItem', 'removeAt', 'removeItem', - 'setSelectionMode', 'selectedItems', 'selectedLabels', + 'items', + 'dragDropMode', 'setDragDropMode', + 'addItem', 'addItemAt', 'addItems', 'addItemsAt', + 'indexOf', 'itemAt', 'moveItem', + 'removeAt', 'removeItem', 'removeItems', + 'selectionMode', 'setSelectionMode', 'selectedItems', 'selectedLabels', 'setCurrentRow', 'setCurrentItem', ) def __init__(self, *args, **kwargs): @@ -51,11 +54,17 @@ class TTkList(TTkAbstractScrollArea): self.moveItem = self._listView.moveItem self.removeAt = self._listView.removeAt self.removeItem = self._listView.removeItem + self.removeItems = self._listView.removeItems self.addItem = self._listView.addItem + self.addItems = self._listView.addItems self.addItemAt = self._listView.addItemAt + self.addItemsAt = self._listView.addItemsAt + self.selectionMode = self._listView.selectionMode self.setSelectionMode = self._listView.setSelectionMode self.selectedItems = self._listView.selectedItems self.selectedLabels = self._listView.selectedLabels self.setCurrentRow = self._listView.setCurrentRow self.setCurrentItem = self._listView.setCurrentItem + self.dragDropMode = self._listView.dragDropMode + self.setDragDropMode = self._listView.setDragDropMode diff --git a/TermTk/TTkWidgets/listwidget.py b/TermTk/TTkWidgets/listwidget.py index 05de8644..8a43e667 100644 --- a/TermTk/TTkWidgets/listwidget.py +++ b/TermTk/TTkWidgets/listwidget.py @@ -22,14 +22,17 @@ __all__ = ['TTkAbstractListItem', 'TTkListWidget'] +from dataclasses import dataclass + from TermTk.TTkCore.cfg import TTkCfg from TermTk.TTkCore.constant import TTkK from TermTk.TTkCore.log import TTkLog from TermTk.TTkCore.signal import pyTTkSlot, pyTTkSignal from TermTk.TTkCore.color import TTkColor +from TermTk.TTkCore.canvas import TTkCanvas from TermTk.TTkCore.string import TTkString +from TermTk.TTkGui.drag import TTkDrag from TermTk.TTkWidgets.widget import TTkWidget -from TermTk.TTkWidgets.label import TTkLabel from TermTk.TTkAbstract.abstractscrollview import TTkAbstractScrollView class TTkAbstractListItem(TTkWidget): @@ -37,9 +40,9 @@ class TTkAbstractListItem(TTkWidget): classStyle = TTkWidget.classStyle | { 'default': {'color': TTkColor.RST}, - 'highlighted': {'color': TTkColor.fg('#00FF00')+TTkColor.bg('#0055FF')+TTkColor.UNDERLINE}, - 'hover': {'color': TTkColor.fg('#00FF00')+TTkColor.bg('#0088FF')}, - 'selected': {'color': TTkColor.fg('#00FF00')+TTkColor.bg('#0055FF')}, + 'highlighted': {'color': TTkColor.bg('#008855')+TTkColor.UNDERLINE}, + 'hover': {'color': TTkColor.bg('#0088FF')}, + 'selected': {'color': TTkColor.bg('#0055FF')}, 'clicked': {'color': TTkColor.fg('#FFFF00')}, 'disabled': {'color': TTkColor.fg('#888888')}, } @@ -92,29 +95,39 @@ class TTkAbstractListItem(TTkWidget): self.update() def paintEvent(self, canvas): - style = self.currentStyle() - if style == self.classStyle['hover']: - pass - elif self._highlighted: - style = self.style()['highlighted'] - elif self._selected: - style = self.style()['selected'] + color = (style:=self.currentStyle())['color'] + if self._highlighted: + color = color+self.style()['highlighted']['color'] + if self._selected: + color = color+self.style()['selected']['color'] + if style==self.style()['hover']: + color = color+self.style()['hover']['color'] w = self.width() - canvas.drawTTkString(pos=(0,0), width=w, color=style['color'] ,text=self._text) + canvas.drawTTkString(pos=(0,0), width=w, color=color ,text=self._text) class TTkListWidget(TTkAbstractScrollView): + @dataclass(frozen=True) + class _DropListData: + widget: TTkAbstractScrollView + items: list + '''TTkListWidget''' - __slots__ = ('itemClicked', 'textClicked', '_selectedItems', '_selectionMode', '_highlighted', '_items') + __slots__ = ('itemClicked', 'textClicked', + '_selectedItems', '_selectionMode', + '_highlighted', '_items', + '_dragPos', '_dndMode') def __init__(self, *args, **kwargs): # Default Class Specific Values self._selectionMode = kwargs.get("selectionMode", TTkK.SingleSelection) self._selectedItems = [] self._items = [] self._highlighted = None + self._dragPos = None + self._dndMode = kwargs.get("dragDropMode", TTkK.DragDropMode.NoDragDrop) # Signals - self.itemClicked = pyTTkSignal(TTkWidget) + self.itemClicked = pyTTkSignal(TTkAbstractListItem) self.textClicked = pyTTkSignal(str) # Init Super TTkAbstractScrollView.__init__(self, *args, **kwargs) @@ -149,6 +162,18 @@ class TTkListWidget(TTkAbstractScrollView): self.itemClicked.emit(label) self.textClicked.emit(label.text()) + def dragDropMode(self): + '''dragDropMode''' + return self._dndMode + + def setDragDropMode(self, dndMode): + '''setDragDropMode''' + self._dndMode = dndMode + + def selectionMode(self): + '''selectionMode''' + return self._selectionMode + def setSelectionMode(self, mode): '''setSelectionMode''' self._selectionMode = mode @@ -186,6 +211,10 @@ class TTkListWidget(TTkAbstractScrollView): '''addItem''' self.addItemAt(item, len(self._items), data) + def addItems(self, items): + '''addItems''' + self.addItemAt(items, len(self._items)) + def _placeItems(self): minw = self.width() for item in self._items: @@ -193,16 +222,24 @@ class TTkListWidget(TTkAbstractScrollView): for y,item in enumerate(self._items): item.setGeometry(0,y,minw,1) self.viewChanged.emit() + self.update() def addItemAt(self, item, pos, data=None): '''addItemAt''' if isinstance(item, str) or isinstance(item, TTkString): - #label = TTkAbstractListItem(text=item, width=max(len(item),self.width())) - label = TTkAbstractListItem(text=item, data=data) - return self.addItemAt(label,pos) - item.listItemClicked.connect(self._labelSelectedHandler) - self._items.insert(pos,item) - self.layout().addWidget(item) + item = TTkAbstractListItem(text=item, data=data) + return self.addItemsAt([item],pos) + + def addItemsAt(self, items, pos): + '''addItemsAt''' + for item in items: + if not issubclass(type(item),TTkAbstractListItem): + TTkLog.error(f"{item=} is not an TTkAbstractListItem") + return + for item in items: + item.listItemClicked.connect(self._labelSelectedHandler) + self._items[pos:pos] = items + self.layout().addWidgets(items) self._placeItems() def indexOf(self, item): @@ -226,10 +263,20 @@ class TTkListWidget(TTkAbstractScrollView): def removeItem(self, item): '''removeItem''' - self.removeWidget(item) - self._items.remove(item) - if item in self._selectedItems: - self._selectedItems.remove(item) + self.removeItems([item]) + + def removeItems(self, items): + '''removeItems''' + self.layout().removeWidgets(items) + for item in items.copy(): + item.listItemClicked.disconnect(self._labelSelectedHandler) + item._setSelected(False) + item._setHighlighted(False) + self._items.remove(item) + if item in self._selectedItems: + self._selectedItems.remove(item) + if item == self._highlighted: + self._highlighted = None self._placeItems() def removeAt(self, pos): @@ -256,6 +303,67 @@ class TTkListWidget(TTkAbstractScrollView): elif index <= offy: self.viewMoveTo(offx, index) + def mouseDragEvent(self, evt) -> bool: + if not(self._dndMode & TTkK.DragDropMode.AllowDrag): + return False + if not (items:=self._selectedItems.copy()): + return True + drag = TTkDrag() + data =TTkListWidget._DropListData(widget=self,items=items) + h = min(3,ih:=len(items)) + 2 + (1 if ih>3 else 0) + w = min(20,iw:=max([it.text().termWidth() for it in items[:3]])) + 2 + pm = TTkCanvas(width=w,height=h) + for y,it in enumerate(items[:3],1): + txt = it.text() + if txt.termWidth() < 20: + pm.drawText(pos=(1,y), text=it.text()) + else: + pm.drawText(pos=(1,y), text=it.text(), width=17) + pm.drawText(pos=(18,y), text='...') + if ih>3: + pm.drawText(pos=(1,4), text='...') + pm.drawBox(pos=(0,0),size=(w,h)) + drag.setPixmap(pm) + drag.setData(data) + drag.exec() + return True + + def dragEnterEvent(self, evt): + if not(self._dndMode & TTkK.DragDropMode.AllowDrop): + return False + if issubclass(type(evt.data()),TTkListWidget._DropListData): + return self.dragMoveEvent(evt) + return False + + def dragMoveEvent(self, evt): + offx,offy = self.getViewOffsets() + y=min(evt.y+offy,len(self._items)) + self._dragPos = (offx+evt.x, y) + self.update() + return True + + def dragLeaveEvent(self, evt): + self._dragPos = None + self.update() + return True + + def dropEvent(self, evt) -> bool: + if not(self._dndMode & TTkK.DragDropMode.AllowDrop): + return False + self._dragPos = None + if not issubclass(type(evt.data()) ,TTkListWidget._DropListData): + return False + offx,offy = self.getViewOffsets() + wid = evt.data().widget + items = evt.data().items + if wid and items: + wid.removeItems(items) + for it in items: + it.setCurrentStyle(it.style()['default']) + self.addItemsAt(items,offy+evt.y) + return True + return False + def keyEvent(self, evt): if not self._highlighted: return False if ( evt.type == TTkK.Character and evt.key==" " ) or \ @@ -304,3 +412,19 @@ class TTkListWidget(TTkAbstractScrollView): def focusOutEvent(self): if self._highlighted: self._highlighted._setHighlighted(False) + self._dragPos = None + + # Stupid hack to paint on top of the child widgets + def paintChildCanvas(self): + super().paintChildCanvas() + if self._dragPos: + canvas = self.getCanvas() + x,y = self._dragPos + offx,offy = self.getViewOffsets() + p1 = (0,y-offy-1) + p2 = (0,y-offy) + canvas.drawText(pos=p1,text="╙─╼", color=TTkColor.fg("#FFFF00")+TTkColor.bg("#008855")) + canvas.drawText(pos=p2,text="╓─╼", color=TTkColor.fg("#FFFF00")+TTkColor.bg("#008855")) + + + diff --git a/TermTk/TTkWidgets/tabwidget.py b/TermTk/TTkWidgets/tabwidget.py index 45a82eb7..6a40d43c 100644 --- a/TermTk/TTkWidgets/tabwidget.py +++ b/TermTk/TTkWidgets/tabwidget.py @@ -359,15 +359,15 @@ class TTkTabBar(TTkContainer): self._leftScroller.setSideEnd(sideEnd&TTkK.LEFT) self._updateTabs() - def addTab(self, label, data=None): + def addTab(self, label, data=None, closable=None): '''addTab''' - return self.insertTab(len(self._tabButtons), label=label, data=data) + return self.insertTab(len(self._tabButtons), label=label, data=data, closable=closable) - def insertTab(self, index, label, data=None): + def insertTab(self, index, label, data=None, closable=None): '''insertTab''' if index <= self._currentIndex: self._currentIndex += 1 - button = TTkTabButton(parent=self, text=label, border=not self._small, closable=self._tabClosable, data=data) + button = TTkTabButton(parent=self, text=label, border=not self._small, closable=self._tabClosable if closable is None else closable, data=data) self._tabButtons.insert(index,button) button.clicked.connect(lambda :self.setCurrentIndex(self._tabButtons.index(button))) button.clicked.connect(lambda :self.tabBarClicked.emit(self._tabButtons.index(button))) @@ -668,7 +668,7 @@ class TTkTabWidget(TTkFrame): if index <= newIndex: newIndex -= 1 tw.removeTab(index) - self.insertTab(newIndex, widget, tb.text(), data) + self.insertTab(newIndex, widget, tb.text(), data, tb._closable) self.setCurrentIndex(newIndex) #self._tabChanged(newIndex) elif tw != self: @@ -699,19 +699,19 @@ class TTkTabWidget(TTkFrame): self._tabBarTopLayout.update() return button - def addTab(self, widget, label, data=None): + def addTab(self, widget, label, data=None, closable=None): '''addTab''' widget.hide() self._tabWidgets.append(widget) self.layout().addWidget(widget) - self._tabBar.addTab(label, data) + self._tabBar.addTab(label, data, closable) - def insertTab(self, index, widget, label, data=None): + def insertTab(self, index, widget, label, data=None, closable=None): '''insertTab''' widget.hide() self._tabWidgets.insert(index, widget) self.layout().addWidget(widget) - self._tabBar.insertTab(index, label, data) + self._tabBar.insertTab(index, label, data, closable) @pyTTkSlot(int) def removeTab(self, index): diff --git a/TermTk/TTkWidgets/texedit.py b/TermTk/TTkWidgets/texedit.py index 014dcaba..1b3e407c 100644 --- a/TermTk/TTkWidgets/texedit.py +++ b/TermTk/TTkWidgets/texedit.py @@ -50,15 +50,17 @@ class _TTkTextEditViewLineNumber(TTkAbstractScrollView): 'separatorColor': TTkColor.fg("#888888")}, } - __slots__ = ('_textWrap') - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.setMaximumWidth(20) + __slots__ = ('_textWrap','_startingNumber') + def __init__(self, startingNumber=0, **kwargs): + self._startingNumber = startingNumber self._textWrap = None + super().__init__(**kwargs) + self.setMaximumWidth(2) def _wrapChanged(self): dt = max(1,self._textWrap._lines[-1][0]) - width = 2+floor(log10(dt)) + off = self._startingNumber + width = 1+max(len(str(int(dt+off))),len(str(int(off)))) self.setMaximumWidth(width) self.update() @@ -80,6 +82,7 @@ class _TTkTextEditViewLineNumber(TTkAbstractScrollView): if not self._textWrap: return _, oy = self.getViewOffsets() w, h = self.size() + off = self._startingNumber style = self.currentStyle() color = style['color'] @@ -91,11 +94,11 @@ class _TTkTextEditViewLineNumber(TTkAbstractScrollView): if fr: canvas.drawText(pos=(0,i), text='<', width=w, color=wrapColor) else: - canvas.drawText(pos=(0,i), text=f"{dt}", width=w, color=color) + canvas.drawText(pos=(0,i), text=f"{dt+off}", width=w, color=color) canvas.drawChar(pos=(w-1,i), char='▌', color=separatorColor) else: for y in range(h): - canvas.drawText(pos=(0,y), text=f"{y+oy}", width=w, color=color) + canvas.drawText(pos=(0,y), text=f"{y+oy+off}", width=w, color=color) canvas.drawChar(pos=(w-1,y), char='▌', color=separatorColor) class TTkTextEditView(TTkAbstractScrollView): @@ -799,17 +802,17 @@ class TTkTextEdit(TTkAbstractScrollArea): 'undoAvilable', 'redoAvailable', 'textChanged' ) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, textEditView=None, lineNumber=False, lineNumberStarting=0, **kwargs): + super().__init__(**kwargs) if 'parent' in kwargs: kwargs.pop('parent') - self._textEditView = kwargs.get('textEditView', TTkTextEditView(*args, **kwargs)) + self._textEditView = textEditView if textEditView else TTkTextEditView(**kwargs) # self.setFocusPolicy(self._textEditView.focusPolicy()) # self._textEditView.setFocusPolicy(TTkK.ParentFocus) - self._lineNumber = kwargs.get('lineNumber', False) + self._lineNumber = lineNumber textEditLayout = TTkAbstractScrollViewGridLayout() textEditLayout.addWidget(self._textEditView,0,1) - self._lineNumberView = _TTkTextEditViewLineNumber(visible=self._lineNumber) + self._lineNumberView = _TTkTextEditViewLineNumber(visible=self._lineNumber, startingNumber=lineNumberStarting) self._lineNumberView.setTextWrap(self._textEditView._textWrap) textEditLayout.addWidget(self._lineNumberView,0,0) self.setViewport(textEditLayout) @@ -858,10 +861,19 @@ class TTkTextEdit(TTkAbstractScrollArea): '''getLineNumber''' return self._lineNumberView.isVisible() + @pyTTkSlot(bool) def setLineNumber(self, ln): '''setLineNumber''' self._lineNumberView.setVisible(ln) + def lineNumberStarting(self): + return self._lineNumberView._startingNumber + + @pyTTkSlot(int) + def setLineNumberStarting(self, starting): + self._lineNumberView._startingNumber = starting + self._lineNumberView._wrapChanged() + def setDocument(self, document): '''setDocument''' self._textEditView.setDocument(document) diff --git a/TermTk/TTkWidgets/widget.py b/TermTk/TTkWidgets/widget.py index 15c4001d..18372076 100644 --- a/TermTk/TTkWidgets/widget.py +++ b/TermTk/TTkWidgets/widget.py @@ -153,7 +153,6 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents): self._processStyleEvent(TTkWidget._S_DEFAULT) self._canvas = TTkCanvas( - widget = self, width = self._width , height = self._height ) @@ -167,8 +166,6 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents): self._parent.layout().addWidget(self) self._parent.update(repaint=True, updateLayout=True) - self.update(repaint=True, updateLayout=True) - def __del__(self): ''' .. caution:: Don't touch this! ''' # TTkLog.debug("DESTRUCTOR") diff --git a/demo/showcase/_showcasehelper.py b/demo/showcase/_showcasehelper.py index 0c509503..32c580b2 100755 --- a/demo/showcase/_showcasehelper.py +++ b/demo/showcase/_showcasehelper.py @@ -21,13 +21,20 @@ # SOFTWARE. import sys, os, random +import platform sys.path.append(os.path.join(sys.path[0],'../..')) import TermTk as ttk -zc1 = chr(0x07a6) # Zero width chars oަ -zc2 = chr(0x20D7) # Zero width chars o⃗ -zc3 = chr(0x065f) # Zero width chars oٟ +if platform.system() == 'Windows': + # The windows terminals badly supports zero sized chars + zc1 = 'X' + zc2 = 'Y' + zc3 = 'Z' +else: + zc1 = chr(0x07a6) # Zero width chars oަ + zc2 = chr(0x20D7) # Zero width chars o⃗ + zc3 = chr(0x065f) # Zero width chars oٟ utfwords = [ f"--Zero{zc1}{zc2}{zc3}-1-", f"--Zero-2{zc1}{zc2}{zc3}-", f"--Ze{zc1}{zc2}{zc3}ro-3-", f"{zc1}{zc2}{zc3}--Zero-4-", "Lorem", "i🙻sum", "d😮l😱r", "sit", "am😎t,", "c😱nsectetur", "adi🙻iscing", "elit,", "sed", "do", "eiusmod", "t😜mpor", "incididunt", "ut", "labore", "et", "dolore", "magna", "aliqua.", "Ut", "enim", "ad", "minim", "veniam,", "quis", "nostrud", "exercitation", "ullamco", "laboris", "nisi", "ut", "aliq😞ip", "ex", "ea", "comm😞do", "cons😿quat.", "Duis", "aute", "irure", "dolor", "in", "reprehenderit", "in", "voluptate", "velit", "esse", "cillum", "dolore", "eu", "fugiat", "nulla", "pariatur.", "Excepteur", "sint", "occaecat", "cupidatat", "non", "proident,", "sunt", "in", "cul🙻a", "qui", "officia", "deserunt", "mollit", "anim", "id", "est", "laborum."] diff --git a/demo/showcase/formwidgets02.py b/demo/showcase/formwidgets02.py index ab99cab8..906fe3d0 100755 --- a/demo/showcase/formwidgets02.py +++ b/demo/showcase/formwidgets02.py @@ -29,7 +29,7 @@ sys.path.append(os.path.join(sys.path[0],'../..')) import TermTk as ttk sys.path.append(os.path.join(sys.path[0],'..')) -from showcase._showcasehelper import getUtfSentence +from showcase._showcasehelper import getUtfSentence, zc1 def demoFormWidgets(root=None): win_form1_grid_layout = ttk.TTkGridLayout(columnMinWidth=1) @@ -72,7 +72,7 @@ def demoFormWidgets(root=None): win_form1_grid_layout.addWidget(_wid := ttk.TTkLineEdit(text='Line Edit Test 2 😎 -'),row,2) win_form1_grid_layout.addWidget(_en_dis_cb := ttk.TTkCheckbox(text=" en/dis", checked=True),row,3); _en_dis_cb.clicked.connect(_wid.setEnabled) row += 1; win_form1_grid_layout.addWidget(ttk.TTkLabel(text='Line Edit Test 3'),row,0) - win_form1_grid_layout.addWidget(_wid := ttk.TTkLineEdit(text='Line Edit Test 3 oަ -'),row,2) + win_form1_grid_layout.addWidget(_wid := ttk.TTkLineEdit(text=f'Line Edit Test 3 o{zc1}-'),row,2) win_form1_grid_layout.addWidget(_en_dis_cb := ttk.TTkCheckbox(text=" en/dis", checked=True),row,3); _en_dis_cb.clicked.connect(_wid.setEnabled) row += 1; win_form1_grid_layout.addWidget(ttk.TTkLabel(text='Line Edit Input Number'),row,0) diff --git a/demo/showcase/list.py b/demo/showcase/list.py index c72b060d..f5eddea3 100755 --- a/demo/showcase/list.py +++ b/demo/showcase/list.py @@ -33,23 +33,43 @@ from showcase._showcasehelper import getUtfWord def demoList(root= None): # Define the main Layout - splitter = ttk.TTkSplitter(parent=root, orientation=ttk.TTkK.HORIZONTAL) - frame2 = ttk.TTkFrame(parent=splitter, border=0, layout=ttk.TTkVBoxLayout()) - frame1 = ttk.TTkFrame(parent=splitter, border=0, layout=ttk.TTkVBoxLayout()) - frame3 = ttk.TTkFrame(parent=splitter, border=0, layout=ttk.TTkVBoxLayout()) + retFrame = ttk.TTkFrame(parent=root, border=False, layout=(rootLayout:=ttk.TTkGridLayout())) - # Multi Selection List - ttk.TTkLabel(parent=frame1, text="[ MultiSelect ]",maxHeight=2) - listWidgetMulti = ttk.TTkList(parent=frame1, maxWidth=40, minWidth=10, selectionMode=ttk.TTkK.MultiSelection) + # Define the main Layout + win1 = ttk.TTkWindow(title="Single List", layout=ttk.TTkVBoxLayout()) + win2 = ttk.TTkWindow(title="Multi List", layout=ttk.TTkVBoxLayout()) + win3 = ttk.TTkWindow(title="Log", layout=ttk.TTkVBoxLayout()) + win4 = ttk.TTkWindow(title="Oly Drag Allowed", layout=ttk.TTkVBoxLayout()) + win5 = ttk.TTkWindow(title="Oly Drop Allowed", layout=ttk.TTkVBoxLayout()) + layout1 = ttk.TTkLayout() + + # Place the widgets in the root layout + rootLayout.addWidget(win1,0,0,2,1) + rootLayout.addWidget(win2,0,1,2,1) + rootLayout.addWidget(win3,0,2,2,3) + rootLayout.addItem(layout1,2,0,1,3) + rootLayout.addWidget(win4,2,3) + rootLayout.addWidget(win5,2,4) # Single Selection List - ttk.TTkLabel(parent=frame2, text="[ SingleSelect ]",maxHeight=2) - listWidgetSingle = ttk.TTkList(parent=frame2, maxWidth=40, minWidth=10) + listWidgetSingle = ttk.TTkList(parent=win1, maxWidth=40, minWidth=10, dragDropMode=ttk.TTkK.AllowDragDrop) + + # Multi Selection List + listWidgetMulti = ttk.TTkList(parent=win2, maxWidth=40, minWidth=10, dragDropMode=ttk.TTkK.AllowDragDrop, selectionMode=ttk.TTkK.MultiSelection) + + # Multi Selection List - Drag Allowed + listWidgetDrag = ttk.TTkList(parent=win4, maxWidth=40, minWidth=10, dragDropMode=ttk.TTkK.AllowDrag) + listWidgetDrop = ttk.TTkList(parent=win5, maxWidth=40, minWidth=10, dragDropMode=ttk.TTkK.AllowDrop) # Log Viewer - label1 = ttk.TTkLabel(parent=frame3, text="[ list1 ]",maxHeight=2) - label2 = ttk.TTkLabel(parent=frame3, text="[ list2 ]",maxHeight=2) - ttk.TTkLogViewer(parent=frame3)#, border=True) + label1 = ttk.TTkLabel(pos=(10,0), text="[ list1 ]",maxHeight=2) + label2 = ttk.TTkLabel(pos=(10,1), text="[ list2 ]",maxHeight=2) + ttk.TTkLogViewer(parent=win3) + + btn_mv1 = ttk.TTkButton(pos=(0,0), text=" >> ") + btn_mv2 = ttk.TTkButton(pos=(0,1), text=" << ") + btn_del = ttk.TTkButton(pos=(0,2), text="Delete") + layout1.addWidgets([label1,label2,btn_mv1,btn_mv2,btn_del]) @ttk.pyTTkSlot(str) def _listCallback1(label): @@ -61,16 +81,42 @@ def demoList(root= None): ttk.TTkLog.info(f'Clicked label2: "{label}" - selected: {[str(s) for s in listWidgetMulti.selectedLabels()]}') label2.setText(f'[ list2 ] clicked "{label}" - {[str(s) for s in listWidgetMulti.selectedLabels()]}') + @ttk.pyTTkSlot() + def _moveToRight2(): + for i in listWidgetSingle.selectedItems().copy(): + listWidgetSingle.removeItem(i) + listWidgetMulti.addItemAt(i,0) + + @ttk.pyTTkSlot() + def _moveToLeft1(): + for i in listWidgetMulti.selectedItems().copy(): + listWidgetMulti.removeItem(i) + listWidgetSingle.addItemAt(i,0) + + @ttk.pyTTkSlot() + def _delSelected(): + items = listWidgetMulti.selectedItems() + listWidgetMulti.removeItems(items) + items = listWidgetSingle.selectedItems() + listWidgetSingle.removeItems(items) + + + btn_mv1.clicked.connect(_moveToRight2) + btn_mv2.clicked.connect(_moveToLeft1) + btn_del.clicked.connect(_delSelected) + + # Connect the signals to the 2 slots defines listWidgetSingle.textClicked.connect(_listCallback1) listWidgetMulti.textClicked.connect(_listCallback2) # populate the lists with random entries - for i in range(100): - listWidgetSingle.addItem(f"{i}) {getUtfWord()} {getUtfWord()}") - listWidgetMulti.addItem(f"{getUtfWord()} {getUtfWord()}") + for i in range(50): + listWidgetSingle.addItem(f"S-{i}) {getUtfWord()} {getUtfWord()}") + listWidgetMulti.addItem( f"M-{i}){getUtfWord()} {getUtfWord()}") + listWidgetDrag.addItem( f"D-{i}){getUtfWord()} {getUtfWord()}") - return splitter + return retFrame def main(): parser = argparse.ArgumentParser() diff --git a/demo/showcase/textedit.py b/demo/showcase/textedit.py index c37951da..b8af2a8e 100755 --- a/demo/showcase/textedit.py +++ b/demo/showcase/textedit.py @@ -31,7 +31,7 @@ sys.path.append(os.path.join(sys.path[0],'../..')) import TermTk as ttk sys.path.append(os.path.join(sys.path[0],'..')) -from showcase._showcasehelper import getUtfColoredSentence +from showcase._showcasehelper import getUtfColoredSentence, zc1, zc2, zc3 class superSimpleHorizontalLine(ttk.TTkWidget): def paintEvent(self, canvas): @@ -52,7 +52,7 @@ def demoTextEdit(root=None, document=None): # If no document is passed a default one is created, # In this showcase I want to be able to share the same # document among 2 textEdit widgets - te = ttk.TTkTextEdit(document=document, lineNumber=True) + te = ttk.TTkTextEdit(document=document, lineNumber=True, lineNumberStarting=1) te.setReadOnly(False) @@ -74,10 +74,6 @@ def demoTextEdit(root=None, document=None): te.append( " |.|.|.|.|.||.|.|.||.|.|.") te.append("") - - zc1 = chr(0x07a6) - zc2 = chr(0x20D7) - zc3 = chr(0x065f) te.append( " - | | | | | -") te.append(f"Zero Size: - o{zc1} o{zc2} o{zc3} o{zc1}{zc2} o{zc1}{zc2}{zc3} -") te.append( " - | | | | | -") @@ -127,7 +123,7 @@ def demoTextEdit(root=None, document=None): # Empty columns/cells are 1 char wide due to "columnMinWidth=1" parameter in the GridLayout # 1 3 8 11 # 0 2 4 5 6 7 9 10 12 - # 0 [ ] FG [ ] BG [ ] LineNumber + # 0 [ ] FG [ ] BG [ ] LineNumber [ 0]Starting Number # 1 ┌─────┐ ┌─────┐ ╒═══╕╒═══╕╒═══╕╒═══╕ ┌──────┐┌──────┐ # 2 │ │ │ │ │ a ││ a ││ a ││ a │ │ UNDO ││ REDO │ # 3 └─────┘ └─────┘ └───┘└───┘└───┘└───┘ ╘══════╛└──────┘ ┕━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┙ @@ -140,6 +136,7 @@ def demoTextEdit(root=None, document=None): fontLayout.addWidget(btn_bgColor := ttk.TTkColorButtonPicker(border=True, enabled=False, maxSize=(7 ,3)),1,2) fontLayout.addWidget(cb_linenumber := ttk.TTkCheckbox(text=" LineNumber", checked=True),0,4,1,3) + fontLayout.addWidget(sb_linenumber := ttk.TTkSpinBox(value=1, maxWidth=5, maximum=10000, minimum=-10000, enabled=True),0,7,1,1) # Char style buttons fontLayout.addWidget(btn_bold := ttk.TTkButton(border=True, maxSize=(5,3), checkable=True, text=ttk.TTkString( 'a' , ttk.TTkColor.BOLD) ),1,4) @@ -209,7 +206,9 @@ def demoTextEdit(root=None, document=None): cb_fg.clicked.connect(lambda _: _setStyle()) cb_bg.clicked.connect(lambda _: _setStyle()) - cb_linenumber.stateChanged.connect(lambda x: te.setLineNumber(x==ttk.TTkK.Checked)) + cb_linenumber.toggled.connect(te.setLineNumber) + cb_linenumber.toggled.connect(sb_linenumber.setEnabled) + sb_linenumber.valueChanged.connect(te.setLineNumberStarting) btn_fgColor.colorSelected.connect(lambda _: _setStyle()) btn_bgColor.colorSelected.connect(lambda _: _setStyle()) diff --git a/docs/MDNotes/Resources.md b/docs/MDNotes/Resources.md index 426a5ea2..b8f0782b 100644 --- a/docs/MDNotes/Resources.md +++ b/docs/MDNotes/Resources.md @@ -98,15 +98,3 @@ Check as reference: Pty Demo: - https://docs.python.org/3/library/pty.html#example - -Run Python - pyTermTk on Wine32: -```bash -~/.var/app/net.lutris.Lutris/data/lutris/runners/wine/lutris-GE-Proton8-5-x86_64/bin/wine /home/one/.wine/drive_c/windows/system32/cmd.exe - -# Install python from https://www.python.org/downloads/windows/ -# Copy the pyTermTk demo and TermTk folder in -# ~/.wine/drive_c/users/one/AppData/Local/Programs/Python/Python310-32 - -cd C:\users\one\AppData\Local\Programs\Python\Python310-32 -python.exe demo/demo.py -``` diff --git a/docs/MDNotes/msWindows/Init Sequence.md b/docs/MDNotes/msWindows/Init Sequence.md new file mode 100644 index 00000000..db89da15 --- /dev/null +++ b/docs/MDNotes/msWindows/Init Sequence.md @@ -0,0 +1,36 @@ +# How it Was +TTk: +- __init__() + ```python + self._input = TTkInput() + self._input.inputEvent.connect(self._processInput) + self._input.pasteEvent.connect(self._processPaste) + ``` +- mainLoop() + ```python + TTkTerm.registerResizeCb(self._win_resize_cb) + TTkTerm.init( + title=self._title, + sigmask=self._sigmask, + mouse=self._termMouse, + directMouse=self._termDirectMouse ) + ``` + +# How it Should Be +- __init__() + ```python + TTkInput.inputEvent.connect(self._processInput) + TTkInput.pasteEvent.connect(self._processPaste) + TTkSignalDriver.sigStop.connect(self._SIGSTOP) + TTkSignalDriver.sigCont.connect(self._SIGCONT) + TTkSignalDriver.sigInt.connect( self._SIGINT) + ``` +- mainLoop() + ```python + TTkInput.init( + mouse=self._termMouse, + directMouse=self._termDirectMouse) + TTkTerm.init( + title=self._title, + sigmask=self._sigmask) + ``` \ No newline at end of file diff --git a/docs/MDNotes/msWindows/Resources.md b/docs/MDNotes/msWindows/Resources.md new file mode 100644 index 00000000..f8e34eeb --- /dev/null +++ b/docs/MDNotes/msWindows/Resources.md @@ -0,0 +1,31 @@ +# Run Python - pyTermTk on Wine32: +```bash +# cmd in the terminal +~/.var/app/net.lutris.Lutris/data/lutris/runners/wine/lutris-GE-Proton8-5-x86_64/bin/wine cmd +# cmd in a wine window +~/.var/app/net.lutris.Lutris/data/lutris//runners/wine/lutris-GE-Proton8-5-x86_64/bin/wine wineconsole + +# Install python from https://www.python.org/downloads/windows/ +# Copy the pyTermTk demo and TermTk folder in +# ~/.wine/drive_c/users/one/AppData/Local/Programs/Python/Python310-32 + +C: +cd C:\users\one\AppData\Local\Programs\Python\Python310-32 +python.exe demo/demo.py +python.exe tests/test.input.win.py +``` + +# termios wrappers + - termiWin -> https://github.com/veeso/termiWin + +# Competitors with MS-Win support + +### Textual -> https://github.com/Textualize/textual +https://github.com/Textualize/textual/blob/main/src/textual/drivers/win32.py + +### TheVTPyProject -> https://github.com/srccircumflex/TheVTPyProject + +# Incompatible code (the one using termios): + - TermTk/TTkCore/TTkTerm/readinputlinux.py + - TermTk/TTkCore/TTkTerm/readinputlinux_thread.py + - TermTk/TTkCore/TTkTerm/term_unix.py diff --git a/setup.ttkDesigner.py b/setup.ttkDesigner.py index 7ae3f8f4..8a560eb9 100644 --- a/setup.ttkDesigner.py +++ b/setup.ttkDesigner.py @@ -33,7 +33,7 @@ setup( package_data={'ttkDesigner': ['tui/*']}, python_requires=">=3.9", install_requires=[ - 'pyTermTk>=0.30.0a115', + 'pyTermTk>=0.36.0a', 'pyperclip', 'Pillow'], entry_points={ diff --git a/tests/pytest/conftest.py b/tests/pytest/conftest.py index 7523a2b9..e29ebc03 100644 --- a/tests/pytest/conftest.py +++ b/tests/pytest/conftest.py @@ -27,11 +27,11 @@ import sys from mock_term import Mock_TTkTerm from mock_input import Mock_TTkInput -moduleTerm = type(sys)('TermTk.TTkCore.TTkTerm.term') +moduleTerm = type(sys)('TermTk.TTkCore.drivers.term_unix') moduleTerm.TTkTerm = Mock_TTkTerm moduleInput = type(sys)('TermTk.TTkCore.TTkTerm.input') moduleInput.TTkInput = Mock_TTkInput -sys.modules['TermTk.TTkCore.TTkTerm.term'] = moduleTerm +sys.modules['TermTk.TTkCore.drivers.term_unix'] = moduleTerm sys.modules['TermTk.TTkCore.TTkTerm.input'] = moduleInput \ No newline at end of file diff --git a/tests/pytest/mock_input.py b/tests/pytest/mock_input.py index 0f64eefa..8173d09c 100644 --- a/tests/pytest/mock_input.py +++ b/tests/pytest/mock_input.py @@ -23,12 +23,20 @@ # Thanks to: https://stackoverflow.com/questions/43162722/mocking-a-module-import-in-pytest class Mock_TTkInput(): - def __init__(self): pass - def close(self): pass - def stop(self): pass - def cont(self): pass - def get_key(self, callback=None): pass - def start(self): pass + @staticmethod + def init(mouse, directMouse):pass + @staticmethod + def setMouse(mouse, directMouse): pass + @staticmethod + def close(): pass + @staticmethod + def stop(): pass + @staticmethod + def cont(): pass + @staticmethod + def get_key( callback=None): pass + @staticmethod + def start(): pass class inputEvent(): def connect(*args): diff --git a/tests/pytest/mock_term.py b/tests/pytest/mock_term.py index 003f0506..3e514f2e 100644 --- a/tests/pytest/mock_term.py +++ b/tests/pytest/mock_term.py @@ -79,7 +79,7 @@ class Mock_TTkTerm(): @staticmethod def exit(): pass @staticmethod - def init(title,sigmask,mouse,directMouse): pass + def init(title,sigmask): pass @staticmethod def getTerminalSize(): return 250,70 diff --git a/tests/sandbox/sandbox.html b/tests/sandbox/sandbox.html index 18ea6023..5e36d6d5 100644 --- a/tests/sandbox/sandbox.html +++ b/tests/sandbox/sandbox.html @@ -231,11 +231,11 @@ pyodide.runPython(` import sys import TermTk as ttk + from TermTk.TTkCore.TTkTerm.input import TTkInput import pyodideProxy def ttk_input(val): - if ttk.TTkHelper._rootWidget and ttk.TTkHelper._rootWidget._input: - ttk.TTkHelper._rootWidget._input.key_process(val) + TTkInput.key_process(val) def ttk_resize(w,h): ttk.TTkLog.debug(f"Resize: {w=} {h=}") diff --git a/tests/test.input.curses.py b/tests/test.input.curses.py new file mode 100755 index 00000000..49b2a0d4 --- /dev/null +++ b/tests/test.input.curses.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2021 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. + +import sys, os + +import curses + +print("Retrieve Keyboard, Mouse press/drag/wheel Events") +print("Press q or to exit") + +def reset(): + # Reset + sys.stdout.write("\033[?1000l") + sys.stdout.write("\033[?1002l") + sys.stdout.write("\033[?1015l") + sys.stdout.write("\033[?1006l") + sys.stdout.write("\033[?1049l") # Switch to normal screen + sys.stdout.write("\033[?2004l") # Paste Bracketed mode + sys.stdout.flush() + +stdscr = curses.initscr() + +curses.curs_set(0) +stdscr.keypad(1) +curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION) +curses.mouseinterval(0) +print('\033[?1003h') + +# reset() + +while True: + c = stdscr.getch() + print(f"{c=}\r") + if c == ord('q'): + break # Exit the while loop + elif c == curses.KEY_HOME: + print("HOME\r") + elif c == curses.KEY_MOUSE: + m = curses.getmouse() + y, x = stdscr.getyx() + print(f"Mouse {m=} {(x,y)=}\r") + elif c == curses.KEY_RESIZE: + print(f"Resize\r") + +print('\033[?1003l') +curses.endwin() +curses.flushinp() \ No newline at end of file diff --git a/tests/test.input.py b/tests/test.input.py index 12c72d14..591d7b29 100755 --- a/tests/test.input.py +++ b/tests/test.input.py @@ -26,7 +26,8 @@ import sys, os import logging sys.path.append(os.path.join(sys.path[0],'..')) -from TermTk import TTkLog, TTkK, TTkInput, TTkTerm +from TermTk import TTkLog, TTkK, TTkTerm +from TermTk.TTkCore.TTkTerm.input import TTkInput def message_handler(mode, context, message): log = logging.debug @@ -54,7 +55,6 @@ def winCallback(width, height): TTkTerm.registerResizeCb(winCallback) -input = TTkInput() def keyCallback(kevt=None, mevt=None): if mevt is not None: @@ -65,7 +65,7 @@ def keyCallback(kevt=None, mevt=None): else: TTkLog.info(f"Key Event: Special '{kevt}'") if kevt.key == "q": - input.close() + TTkInput.close() return False return True @@ -73,11 +73,12 @@ def pasteCallback(txt:str): TTkLog.info(f"PASTE = {txt}") return True -input.inputEvent.connect(keyCallback) -input.pasteEvent.connect(pasteCallback) - +TTkInput.inputEvent.connect(keyCallback) +TTkInput.pasteEvent.connect(pasteCallback) +TTkInput.init(mouse=True, directMouse=True) +# TTkInput.init(mouse=True, directMouse=False) try: - input.start() + TTkInput.start() finally: - TTkTerm.push(TTkTerm.Mouse.OFF + TTkTerm.Mouse.DIRECT_OFF) + TTkInput.close() TTkTerm.setEcho(True) diff --git a/tests/test.input.raw.py b/tests/test.input.raw.py index 8d027920..cf65b5a5 100755 --- a/tests/test.input.raw.py +++ b/tests/test.input.raw.py @@ -48,17 +48,13 @@ def reset(): reset() -# TTkTerm.push("\033[?2004h") # Paste Bracketed mode -TTkTerm.push("\033[?2004l") # disable Paste Bracketed mode -TTkTerm.push("\033[?1049h") # Switch to alternate screen -TTkTerm.push("\033[?1000h") -# TTkTerm.push("\033[?1002h") -TTkTerm.push("\033[?1003h") -# TTkTerm.push("\033[?1006h") -TTkTerm.push("\033[?1015h") +TTkTerm.push("\033[?2004h") # Paste Bracketed mode +# TTkTerm.push("\033[?1000h") +TTkTerm.push("\033[?1002h") +# TTkTerm.push("\033[?1003h") TTkTerm.push("\033[?1006h") -TTkTerm.push("\033[?25l") - +# TTkTerm.push("\033[?1015h") +# TTkTerm.push("\033[?1049h") # Switch to alternate screen # TTkTerm.push(TTkTerm.Mouse.ON) # TTkTerm.push(TTkTerm.Mouse.DIRECT_ON) TTkTerm.setEcho(False) diff --git a/tests/test.input.win.01.py b/tests/test.input.win.01.py new file mode 100755 index 00000000..f31b4b79 --- /dev/null +++ b/tests/test.input.win.01.py @@ -0,0 +1,393 @@ +# MIT License +# +# Copyright (c) 2023 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. + +import sys + +from ctypes import Structure, Union, byref, wintypes, windll + +# Example ported from: +# https://learn.microsoft.com/en-us/windows/console/reading-input-buffer-events + +# https://learn.microsoft.com/en-us/windows/console/getstdhandle +STD_INPUT_HANDLE = wintypes.DWORD(-10) # The standard input device. Initially, this is the console input buffer, CONIN$. +STD_OUTPUT_HANDLE = wintypes.DWORD(-11) # The standard output device. Initially, this is the active console screen buffer, CONOUT$. +STD_ERROR_HANDLE = wintypes.DWORD(-12) # The standard error device. Initially, this is the active console screen buffer, CONOUT$. + +INVALID_HANDLE_VALUE = -1 # WinBase.h + +# https://learn.microsoft.com/en-us/windows/console/SetConsoleMode +ENABLE_ECHO_INPUT = 0x0004 # Characters read by the ReadFile or ReadConsole function are written to the active screen buffer as they are typed into the console. This mode can be used only if the ENABLE_LINE_INPUT mode is also enabled. +ENABLE_INSERT_MODE = 0x0020 # When enabled, text entered in a console window will be inserted at the current cursor location and all text following that location will not be overwritten. When disabled, all following text will be overwritten. +ENABLE_LINE_INPUT = 0x0002 # The ReadFile or ReadConsole function returns only when a carriage return character is read. If this mode is disabled, the functions return when one or more characters are available. +ENABLE_MOUSE_INPUT = 0x0010 # If the mouse pointer is within the borders of the console window and the window has the keyboard focus, mouse events generated by mouse movement and button presses are placed in the input buffer. These events are discarded by ReadFile or ReadConsole, even when this mode is enabled. The ReadConsoleInput function can be used to read MOUSE_EVENT input records from the input buffer. +ENABLE_PROCESSED_INPUT = 0x0001 # CTRL+C is processed by the system and is not placed in the input buffer. If the input buffer is being read by ReadFile or ReadConsole, other control keys are processed by the system and are not returned in the ReadFile or ReadConsole buffer. If the ENABLE_LINE_INPUT mode is also enabled, backspace, carriage return, and line feed characters are handled by the system. +ENABLE_QUICK_EDIT_MODE = 0x0040 # This flag enables the user to use the mouse to select and edit text. To enable this mode, use ENABLE_QUICK_EDIT_MODE | ENABLE_EXTENDED_FLAGS. To disable this mode, use ENABLE_EXTENDED_FLAGS without this flag. +ENABLE_WINDOW_INPUT = 0x0008 # User interactions that change the size of the console screen buffer are reported in the console's input buffer. Information about these events can be read from the input buffer by applications using the ReadConsoleInput function, but not by those using ReadFile or ReadConsole. +ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200 # Setting this flag directs the Virtual Terminal processing engine to convert user input received by the console window into Console Virtual Terminal Sequences that can be retrieved by a supporting application through ReadFile or ReadConsole functions. + +ENABLE_PROCESSED_OUTPUT = 0x0001 +ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 +DISABLE_NEWLINE_AUTO_RETURN = 0x0008 +ENABLE_LVB_GRID_WORLDWIDE = 0x0010 + +# https://learn.microsoft.com/en-us/windows/console/input-record-str +FOCUS_EVENT = 0x0010 # The Event member contains a FOCUS_EVENT_RECORD structure. These events are used internally and should be ignored. +KEY_EVENT = 0x0001 # The Event member contains a KEY_EVENT_RECORD structure with information about a keyboard event. +MENU_EVENT = 0x0008 # The Event member contains a MENU_EVENT_RECORD structure. These events are used internally and should be ignored. +MOUSE_EVENT = 0x0002 # The Event member contains a MOUSE_EVENT_RECORD structure with information about a mouse movement or button press event. +WINDOW_BUFFER_SIZE_EVENT = 0x0004 # The Event member contains a WINDOW_BUFFER_SIZE_RECORD structure with information about the new size of the console screen buffer. + + +# https://docs.microsoft.com/en-us/windows/console/coord-str +# +# typedef struct _COORD { +# SHORT X; +# SHORT Y; +# } COORD, *PCOORD; +class COORD(Structure): + _fields_ = [ + ("X", wintypes.SHORT), + ("Y", wintypes.SHORT)] + + +# https://docs.microsoft.com/en-us/windows/console/key-event-record-str +# +# typedef struct _KEY_EVENT_RECORD { +# BOOL bKeyDown; +# WORD wRepeatCount; +# WORD wVirtualKeyCode; +# WORD wVirtualScanCode; +# union { +# WCHAR UnicodeChar; +# CHAR AsciiChar; +# } uChar; +# DWORD dwControlKeyState; +# } KEY_EVENT_RECORD; +class KEY_EVENT_RECORD(Structure): + class _uChar(Union): + _fields_ = [ + ("UnicodeChar", wintypes.WCHAR) , + ("AsciiChar" , wintypes.CHAR ) ] + + _fields_ = [ + ("bKeyDown" , wintypes.BOOL ), + ("wRepeatCount" , wintypes.WORD ), + ("wVirtualKeyCode" , wintypes.WORD ), + ("wVirtualScanCode" , wintypes.WORD ), + ("uChar" , _uChar ), + ("dwControlKeyState", wintypes.DWORD)] + + +# https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str +# +# typedef struct _MOUSE_EVENT_RECORD { +# COORD dwMousePosition; +# DWORD dwButtonState; +# DWORD dwControlKeyState; +# DWORD dwEventFlags; +# } MOUSE_EVENT_RECORD; +class MOUSE_EVENT_RECORD(Structure): + _fields_ = [ + ("dwMousePosition" , COORD), + ("dwButtonState" , wintypes.DWORD), + ("dwControlKeyState", wintypes.DWORD), + ("dwEventFlags" , wintypes.DWORD)] + + +# https://docs.microsoft.com/en-us/windows/console/window-buffer-size-record-str +# +# typedef struct _WINDOW_BUFFER_SIZE_RECORD { +# COORD dwSize; +# } WINDOW_BUFFER_SIZE_RECORD; +class WINDOW_BUFFER_SIZE_RECORD(Structure): + _fields_ = [("dwSize", COORD)] + + +# https://docs.microsoft.com/en-us/windows/console/menu-event-record-str +# +# typedef struct _MENU_EVENT_RECORD { +# UINT dwCommandId; +# } MENU_EVENT_RECORD, *PMENU_EVENT_RECORD; +class MENU_EVENT_RECORD(Structure): + _fields_ = [("dwCommandId", wintypes.UINT)] + + +# https://docs.microsoft.com/en-us/windows/console/focus-event-record-str +# +# typedef struct _FOCUS_EVENT_RECORD { +# BOOL bSetFocus; +# } FOCUS_EVENT_RECORD; +class FOCUS_EVENT_RECORD(Structure): + _fields_ = [("bSetFocus", wintypes.BOOL)] + + +# https://docs.microsoft.com/en-us/windows/console/input-record-str +# +# typedef struct _INPUT_RECORD { +# WORD EventType; +# union { +# KEY_EVENT_RECORD KeyEvent; +# MOUSE_EVENT_RECORD MouseEvent; +# WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent; +# MENU_EVENT_RECORD MenuEvent; +# FOCUS_EVENT_RECORD FocusEvent; +# } Event; +# } INPUT_RECORD; +class INPUT_RECORD(Structure): + class _Event(Union): + _fields_ = [ + ("KeyEvent" , KEY_EVENT_RECORD ), + ("MouseEvent" , MOUSE_EVENT_RECORD ), + ("WindowBufferSizeEvent", WINDOW_BUFFER_SIZE_RECORD), + ("MenuEvent" , MENU_EVENT_RECORD ), + ("FocusEvent" , FOCUS_EVENT_RECORD )] + + _fields_ = [ + ("EventType", wintypes.WORD), + ("Event" , _Event )] + + +def reset(): + # Reset + sys.stdout.write("\033[?1000l") + # sys.stdout.write("\033[?1002l") + sys.stdout.write("\033[?1003l") + sys.stdout.write("\033[?1006l") + sys.stdout.write("\033[?1015l") + # sys.stdout.write("\033[?1049l") # Switch to normal screen + # sys.stdout.write("\033[?2004l") # Paste Bracketed mode + sys.stdout.flush() + +def init(): + sys.stdout.write("\x1b[?1000h") + sys.stdout.write("\x1b[?1003h") + sys.stdout.write("\x1b[?1006h") + sys.stdout.write("\x1b[?1015h") + sys.stdout.flush() + +# DWORD cNumRead, fdwMode, i; +# INPUT_RECORD irInBuf[128]; +# int counter=0; + +# // Get the standard input handle. +# +# hStdIn = GetStdHandle(STD_INPUT_HANDLE); +# if (hStdIn == INVALID_HANDLE_VALUE) +# ErrorExit("GetStdHandle"); +# +# From: +# https://learn.microsoft.com/en-us/windows/console/getstdhandle +# +# HANDLE WINAPI GetStdHandle( +# _In_ DWORD nStdHandle +# ); + +GetStdHandle = windll.kernel32.GetStdHandle +GetStdHandle.argtypes = [wintypes.DWORD] +GetStdHandle.restype = wintypes.HANDLE + +hStdIn = GetStdHandle(STD_INPUT_HANDLE) +if hStdIn == INVALID_HANDLE_VALUE: + raise Exception("GetStdHandle") + +hStdOut = GetStdHandle(STD_OUTPUT_HANDLE) +if hStdOut == INVALID_HANDLE_VALUE: + raise Exception("GetStdHandle") + +# // Save the current input mode, to be restored on exit. +# +# if (! GetConsoleMode(hStdIn, &fdwSaveOldModeIn) ) +# ErrorExit("GetConsoleMode"); +# +# From: +# https://learn.microsoft.com/en-us/windows/console/GetConsoleMode +# +# BOOL WINAPI GetConsoleMode( +# _In_ HANDLE hConsoleHandle, +# _Out_ LPDWORD lpMode +# ); + +GetConsoleMode = windll.kernel32.GetConsoleMode +GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD] +GetConsoleMode.restype = wintypes.BOOL + +fdwSaveOldModeIn = wintypes.DWORD() +if not GetConsoleMode(hStdIn, byref(fdwSaveOldModeIn)): + raise Exception("GetConsoleMode") + +fdwSaveOldModeOut = wintypes.DWORD() +if not GetConsoleMode(hStdOut, byref(fdwSaveOldModeOut)): + raise Exception("GetConsoleMode") + +print(f"{fdwSaveOldModeIn.value=:02x}") +print(f"{fdwSaveOldModeOut.value=:02x}") + +# // Enable the window and mouse input events. +# +# fdwMode = ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT; +# if (! SetConsoleMode(hStdIn, fdwMode) ) +# ErrorExit("SetConsoleMode"); +# +# From: +# https://learn.microsoft.com/en-us/windows/console/SetConsoleMode +# +# BOOL WINAPI SetConsoleMode( +# _In_ HANDLE hConsoleHandle, +# _In_ DWORD dwMode +# ); + +SetConsoleMode = windll.kernel32.SetConsoleMode +SetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.DWORD] +SetConsoleMode.restype = wintypes.BOOL + +fdwModeIn = ENABLE_VIRTUAL_TERMINAL_INPUT +# fdwModeIn = 0x0218 +if not SetConsoleMode(hStdIn, fdwModeIn): + raise Exception("SetConsoleMode") + +fdwModeOut = fdwSaveOldModeOut.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING +# fdwModeIn = 0x0218 +if not SetConsoleMode(hStdOut, fdwModeOut): + raise Exception("SetConsoleMode") + +print(f"{fdwModeIn=:02x}") +print(f"{fdwModeOut=:02x}") + +init() + +# From: +# https://learn.microsoft.com/en-us/windows/console/ReadConsoleInput +# +# BOOL WINAPI ReadConsoleInput( +# _In_ HANDLE hConsoleInput, +# _Out_ PINPUT_RECORD lpBuffer, +# _In_ DWORD nLength, +# _Out_ LPDWORD lpNumberOfEventsRead +# ); + +ReadConsoleInput = windll.kernel32.ReadConsoleInputW # Unicode +# ReadConsoleInput = windll.kernel32.ReadConsoleInputA # ANSII +# ReadConsoleInput.argtypes = [wintypes.HANDLE, +# wintypes.LPINT, +# wintypes.DWORD, +# wintypes.LPWORD] +ReadConsoleInput.restype = wintypes.BOOL + + +# DWORD cNumRead; +# INPUT_RECORD irInBuf[128]; +cNumRead = wintypes.DWORD(0) +irInBuf = (INPUT_RECORD * 256)() + +# // Loop to read and handle the next 100 input events. +# +# while (counter++ <= 100) +# { +for _ in range(50): +# // Wait for the events. +# +# if (! ReadConsoleInput( +# hStdIn, // input buffer handle +# irInBuf, // buffer to read into +# 128, // size of read buffer +# &cNumRead) ) // number of records read +# ErrorExit("ReadConsoleInput"); + if not ReadConsoleInput( + hStdIn, # input buffer handle + byref(irInBuf), # buffer to read into + 256, # size of read buffer + byref(cNumRead)): # number of records read + raise Exception("ReadConsoleInput") + + # print(f"{hStdIn=} {irInBuf=} {cNumRead=}") + print(f"{cNumRead=}") + +# // Dispatch the events to the appropriate handler. +# +# for (i = 0; i < cNumRead; i++) +# { + for bb in irInBuf[:cNumRead.value]: + # if not bb.EventType: continue + print(f"{bb=} {bb.EventType=}") + + +# switch(irInBuf[i].EventType) +# { +# case KEY_EVENT: // keyboard input +# KeyEventProc(irInBuf[i].Event.KeyEvent); +# break; + if bb.EventType == KEY_EVENT: + print(f"{bb.Event.KeyEvent=}") + print(f"{bb.Event.KeyEvent.bKeyDown=}") + print(f"{bb.Event.KeyEvent.wRepeatCount=}") + print(f"{bb.Event.KeyEvent.wVirtualKeyCode=}") + print(f"{bb.Event.KeyEvent.wVirtualScanCode=}") + print(f"{bb.Event.KeyEvent.uChar.UnicodeChar=}") + print(f"{bb.Event.KeyEvent.uChar.AsciiChar=}") + print(f"{bb.Event.KeyEvent.dwControlKeyState=}") + +# case MOUSE_EVENT: // mouse input +# MouseEventProc(irInBuf[i].Event.MouseEvent); +# break; + elif bb.EventType == MOUSE_EVENT: + print(f"{bb.Event.MouseEvent=}") + print(f"{bb.Event.MouseEvent.dwMousePosition.X=}") + print(f"{bb.Event.MouseEvent.dwMousePosition.Y=}") + print(f"{bb.Event.MouseEvent.dwButtonState=}") + print(f"{bb.Event.MouseEvent.dwControlKeyState=}") + print(f"{bb.Event.MouseEvent.dwEventFlags=}") + +# case WINDOW_BUFFER_SIZE_EVENT: // scrn buf. resizing +# ResizeEventProc( irInBuf[i].Event.WindowBufferSizeEvent ); +# break; + elif bb.EventType == WINDOW_BUFFER_SIZE_EVENT: + print(f"{bb.Event.WindowBufferSizeEvent=}") + print(f"{bb.Event.WindowBufferSizeEvent.dwSize.X=}") + print(f"{bb.Event.WindowBufferSizeEvent.dwSize.Y=}") + +# case FOCUS_EVENT: // disregard focus events +# +# case MENU_EVENT: // disregard menu events +# break; +# +# default: +# ErrorExit("Unknown event type"); +# break; +# } +# } +# } + +# // Restore input mode on exit. +# +# SetConsoleMode(hStdIn, fdwSaveOldModeIn); +if not SetConsoleMode(hStdIn, fdwSaveOldModeIn): + raise Exception("SetConsoleMode") + +if not SetConsoleMode(hStdOut, fdwSaveOldModeOut): + raise Exception("SetConsoleMode") + +# return 0; +# + +reset() +print('OK') diff --git a/tests/test.input.win.02.py b/tests/test.input.win.02.py new file mode 100755 index 00000000..e625ee5c --- /dev/null +++ b/tests/test.input.win.02.py @@ -0,0 +1,436 @@ +# MIT License +# +# Copyright (c) 2023 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. + +import sys,os + +from ctypes import Structure, Union, byref, wintypes, windll + +sys.path.append(os.path.join(sys.path[0],'..')) + +from TermTk.TTkCore.constant import TTkK +from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent + +# Example ported from: +# https://learn.microsoft.com/en-us/windows/console/reading-input-buffer-events + +# https://learn.microsoft.com/en-us/windows/console/getstdhandle +STD_INPUT_HANDLE = wintypes.DWORD(-10) # The standard input device. Initially, this is the console input buffer, CONIN$. +STD_OUTPUT_HANDLE = wintypes.DWORD(-11) # The standard output device. Initially, this is the active console screen buffer, CONOUT$. +STD_ERROR_HANDLE = wintypes.DWORD(-12) # The standard error device. Initially, this is the active console screen buffer, CONOUT$. + +INVALID_HANDLE_VALUE = -1 # WinBase.h + +# https://learn.microsoft.com/en-us/windows/console/SetConsoleMode +ENABLE_ECHO_INPUT = 0x0004 # Characters read by the ReadFile or ReadConsole function are written to the active screen buffer as they are typed into the console. This mode can be used only if the ENABLE_LINE_INPUT mode is also enabled. +ENABLE_INSERT_MODE = 0x0020 # When enabled, text entered in a console window will be inserted at the current cursor location and all text following that location will not be overwritten. When disabled, all following text will be overwritten. +ENABLE_LINE_INPUT = 0x0002 # The ReadFile or ReadConsole function returns only when a carriage return character is read. If this mode is disabled, the functions return when one or more characters are available. +ENABLE_MOUSE_INPUT = 0x0010 # If the mouse pointer is within the borders of the console window and the window has the keyboard focus, mouse events generated by mouse movement and button presses are placed in the input buffer. These events are discarded by ReadFile or ReadConsole, even when this mode is enabled. The ReadConsoleInput function can be used to read MOUSE_EVENT input records from the input buffer. +ENABLE_PROCESSED_INPUT = 0x0001 # CTRL+C is processed by the system and is not placed in the input buffer. If the input buffer is being read by ReadFile or ReadConsole, other control keys are processed by the system and are not returned in the ReadFile or ReadConsole buffer. If the ENABLE_LINE_INPUT mode is also enabled, backspace, carriage return, and line feed characters are handled by the system. +ENABLE_QUICK_EDIT_MODE = 0x0040 # This flag enables the user to use the mouse to select and edit text. To enable this mode, use ENABLE_QUICK_EDIT_MODE | ENABLE_EXTENDED_FLAGS. To disable this mode, use ENABLE_EXTENDED_FLAGS without this flag. +ENABLE_WINDOW_INPUT = 0x0008 # User interactions that change the size of the console screen buffer are reported in the console's input buffer. Information about these events can be read from the input buffer by applications using the ReadConsoleInput function, but not by those using ReadFile or ReadConsole. +ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200 # Setting this flag directs the Virtual Terminal processing engine to convert user input received by the console window into Console Virtual Terminal Sequences that can be retrieved by a supporting application through ReadFile or ReadConsole functions. + +ENABLE_PROCESSED_OUTPUT = 0x0001 +ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 +DISABLE_NEWLINE_AUTO_RETURN = 0x0008 +ENABLE_LVB_GRID_WORLDWIDE = 0x0010 + +# https://learn.microsoft.com/en-us/windows/console/input-record-str +FOCUS_EVENT = 0x0010 # The Event member contains a FOCUS_EVENT_RECORD structure. These events are used internally and should be ignored. +KEY_EVENT = 0x0001 # The Event member contains a KEY_EVENT_RECORD structure with information about a keyboard event. +MENU_EVENT = 0x0008 # The Event member contains a MENU_EVENT_RECORD structure. These events are used internally and should be ignored. +MOUSE_EVENT = 0x0002 # The Event member contains a MOUSE_EVENT_RECORD structure with information about a mouse movement or button press event. +WINDOW_BUFFER_SIZE_EVENT = 0x0004 # The Event member contains a WINDOW_BUFFER_SIZE_RECORD structure with information about the new size of the console screen buffer. + +# https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str +# dwButtonState +FROM_LEFT_1ST_BUTTON_PRESSED = 0x0001 # The leftmost mouse button. +FROM_LEFT_2ND_BUTTON_PRESSED = 0x0004 # The second button fom the left. +FROM_LEFT_3RD_BUTTON_PRESSED = 0x0008 # The third button from the left. +FROM_LEFT_4TH_BUTTON_PRESSED = 0x0010 # The fourth button from the left. +RIGHTMOST_BUTTON_PRESSED = 0x0002 # The rightmost mouse button. +# dwControlKeyState +CAPSLOCK_ON = 0x0080 # The CAPS LOCK light is on. +ENHANCED_KEY = 0x0100 # The key is enhanced. See remarks. +LEFT_ALT_PRESSED = 0x0002 # The left ALT key is pressed. +LEFT_CTRL_PRESSED = 0x0008 # The left CTRL key is pressed. +NUMLOCK_ON = 0x0020 # The NUM LOCK light is on. +RIGHT_ALT_PRESSED = 0x0001 # The right ALT key is pressed. +RIGHT_CTRL_PRESSED = 0x0004 # The right CTRL key is pressed. +SCROLLLOCK_ON = 0x0040 # The SCROLL LOCK light is on. +SHIFT_PRESSED = 0x0010 # The SHIFT key is pressed. +# dwEventFlags +DOUBLE_CLICK = 0x0002 # The second click (button press) of a double-click occurred. The first click is returned as a regular button-press event. +MOUSE_HWHEELED = 0x0008 # The horizontal mouse wheel was moved. + # If the high word of the dwButtonState member contains a positive value, the wheel was rotated to the right. Otherwise, the wheel was rotated to the left. +MOUSE_MOVED = 0x0001 # A change in mouse position occurred. +MOUSE_WHEELED = 0x0004 # The vertical mouse wheel was moved. + # If the high word of the dwButtonState member contains a positive value, the wheel was rotated forward, away from the user. Otherwise, the wheel was rotated backward, toward the user. + +# https://docs.microsoft.com/en-us/windows/console/coord-str +# +# typedef struct _COORD { +# SHORT X; +# SHORT Y; +# } COORD, *PCOORD; +class COORD(Structure): + _fields_ = [ + ("X", wintypes.SHORT), + ("Y", wintypes.SHORT)] + + +# https://docs.microsoft.com/en-us/windows/console/key-event-record-str +# +# typedef struct _KEY_EVENT_RECORD { +# BOOL bKeyDown; +# WORD wRepeatCount; +# WORD wVirtualKeyCode; +# WORD wVirtualScanCode; +# union { +# WCHAR UnicodeChar; +# CHAR AsciiChar; +# } uChar; +# DWORD dwControlKeyState; +# } KEY_EVENT_RECORD; +class KEY_EVENT_RECORD(Structure): + class _uChar(Union): + _fields_ = [ + ("UnicodeChar", wintypes.WCHAR) , + ("AsciiChar" , wintypes.CHAR ) ] + + _fields_ = [ + ("bKeyDown" , wintypes.BOOL ), + ("wRepeatCount" , wintypes.WORD ), + ("wVirtualKeyCode" , wintypes.WORD ), + ("wVirtualScanCode" , wintypes.WORD ), + ("uChar" , _uChar ), + ("dwControlKeyState", wintypes.DWORD)] + + +# https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str +# +# typedef struct _MOUSE_EVENT_RECORD { +# COORD dwMousePosition; +# DWORD dwButtonState; +# DWORD dwControlKeyState; +# DWORD dwEventFlags; +# } MOUSE_EVENT_RECORD; +class MOUSE_EVENT_RECORD(Structure): + _fields_ = [ + ("dwMousePosition" , COORD), + ("dwButtonState" , wintypes.DWORD), + ("dwControlKeyState", wintypes.DWORD), + ("dwEventFlags" , wintypes.DWORD)] + + +# https://docs.microsoft.com/en-us/windows/console/window-buffer-size-record-str +# +# typedef struct _WINDOW_BUFFER_SIZE_RECORD { +# COORD dwSize; +# } WINDOW_BUFFER_SIZE_RECORD; +class WINDOW_BUFFER_SIZE_RECORD(Structure): + _fields_ = [("dwSize", COORD)] + + +# https://docs.microsoft.com/en-us/windows/console/menu-event-record-str +# +# typedef struct _MENU_EVENT_RECORD { +# UINT dwCommandId; +# } MENU_EVENT_RECORD, *PMENU_EVENT_RECORD; +class MENU_EVENT_RECORD(Structure): + _fields_ = [("dwCommandId", wintypes.UINT)] + + +# https://docs.microsoft.com/en-us/windows/console/focus-event-record-str +# +# typedef struct _FOCUS_EVENT_RECORD { +# BOOL bSetFocus; +# } FOCUS_EVENT_RECORD; +class FOCUS_EVENT_RECORD(Structure): + _fields_ = [("bSetFocus", wintypes.BOOL)] + + +# https://docs.microsoft.com/en-us/windows/console/input-record-str +# +# typedef struct _INPUT_RECORD { +# WORD EventType; +# union { +# KEY_EVENT_RECORD KeyEvent; +# MOUSE_EVENT_RECORD MouseEvent; +# WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent; +# MENU_EVENT_RECORD MenuEvent; +# FOCUS_EVENT_RECORD FocusEvent; +# } Event; +# } INPUT_RECORD; +class INPUT_RECORD(Structure): + class _Event(Union): + _fields_ = [ + ("KeyEvent" , KEY_EVENT_RECORD ), + ("MouseEvent" , MOUSE_EVENT_RECORD ), + ("WindowBufferSizeEvent", WINDOW_BUFFER_SIZE_RECORD), + ("MenuEvent" , MENU_EVENT_RECORD ), + ("FocusEvent" , FOCUS_EVENT_RECORD )] + + _fields_ = [ + ("EventType", wintypes.WORD), + ("Event" , _Event )] + + +def reset(): + # Reset + sys.stdout.write("\033[?1000l") + # sys.stdout.write("\033[?1002l") + sys.stdout.write("\033[?1003l") + sys.stdout.write("\x1b[?1004l") # UnSet Bracheted Paste Mode + sys.stdout.write("\033[?1006l") + sys.stdout.write("\033[?1015l") + # sys.stdout.write("\033[?1049l") # Switch to normal screen + # sys.stdout.write("\033[?2004l") # Paste Bracketed mode + sys.stdout.flush() + +def init(): + sys.stdout.write("\x1b[?1000h") + sys.stdout.write("\x1b[?1003h") + sys.stdout.write("\x1b[?1004h") # Set Bracheted Paste Mode + sys.stdout.write("\x1b[?1006h") + sys.stdout.write("\x1b[?1015h") + sys.stdout.flush() + +# Get the standard input handle. +# From: +# https://learn.microsoft.com/en-us/windows/console/getstdhandle +# +# HANDLE WINAPI GetStdHandle( +# _In_ DWORD nStdHandle +# ); + +GetStdHandle = windll.kernel32.GetStdHandle +GetStdHandle.argtypes = [wintypes.DWORD] +GetStdHandle.restype = wintypes.HANDLE + +hStdIn = GetStdHandle(STD_INPUT_HANDLE) +if hStdIn == INVALID_HANDLE_VALUE: + raise Exception("GetStdHandle") + +hStdOut = GetStdHandle(STD_OUTPUT_HANDLE) +if hStdOut == INVALID_HANDLE_VALUE: + raise Exception("GetStdHandle") + +# Save the current input mode, to be restored on exit. +# From: +# https://learn.microsoft.com/en-us/windows/console/GetConsoleMode +# +# BOOL WINAPI GetConsoleMode( +# _In_ HANDLE hConsoleHandle, +# _Out_ LPDWORD lpMode +# ); + +GetConsoleMode = windll.kernel32.GetConsoleMode +GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD] +GetConsoleMode.restype = wintypes.BOOL + +fdwSaveOldModeIn = wintypes.DWORD() +if not GetConsoleMode(hStdIn, byref(fdwSaveOldModeIn)): + raise Exception("GetConsoleMode") + +fdwSaveOldModeOut = wintypes.DWORD() +if not GetConsoleMode(hStdOut, byref(fdwSaveOldModeOut)): + raise Exception("GetConsoleMode") + +print(f"{fdwSaveOldModeIn.value=:02x}") +print(f"{fdwSaveOldModeOut.value=:02x}") + +# Enable the window and mouse input events. +# From: +# https://learn.microsoft.com/en-us/windows/console/SetConsoleMode +# +# BOOL WINAPI SetConsoleMode( +# _In_ HANDLE hConsoleHandle, +# _In_ DWORD dwMode +# ); + +SetConsoleMode = windll.kernel32.SetConsoleMode +SetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.DWORD] +SetConsoleMode.restype = wintypes.BOOL + +fdwModeIn = ENABLE_VIRTUAL_TERMINAL_INPUT +# fdwModeIn = ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT +# fdwModeIn = 0x0218 +if not SetConsoleMode(hStdIn, fdwModeIn): + raise Exception("SetConsoleMode") + +fdwModeOut = fdwSaveOldModeOut.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING +# fdwModeIn = 0x0218 +if not SetConsoleMode(hStdOut, fdwModeOut): + raise Exception("SetConsoleMode") + +print(f"{fdwModeIn=:02x}") +print(f"{fdwModeOut=:02x}") + +init() + +# From: +# https://learn.microsoft.com/en-us/windows/console/ReadConsoleInput +# +# BOOL WINAPI ReadConsoleInput( +# _In_ HANDLE hConsoleInput, +# _Out_ PINPUT_RECORD lpBuffer, +# _In_ DWORD nLength, +# _Out_ LPDWORD lpNumberOfEventsRead +# ); + +ReadConsoleInput = windll.kernel32.ReadConsoleInputW # Unicode +# ReadConsoleInput = windll.kernel32.ReadConsoleInputA # ANSII +# ReadConsoleInput.argtypes = [wintypes.HANDLE, +# wintypes.LPINT, +# wintypes.DWORD, +# wintypes.LPWORD] +ReadConsoleInput.restype = wintypes.BOOL + +# DWORD cNumRead; +# INPUT_RECORD irInBuf[128]; +cNumRead = wintypes.DWORD(0) +irInBuf = (INPUT_RECORD * 256)() + +# Loop to read and handle the next 100 input events. +for _ in range(50): + # Wait for the events. + if not ReadConsoleInput( + hStdIn, # input buffer handle + byref(irInBuf), # buffer to read into + 256, # size of read buffer + byref(cNumRead)): # number of records read + raise Exception("ReadConsoleInput") + + # print(f"{hStdIn=} {irInBuf=} {cNumRead=}") + print(f"{cNumRead=}") + + # Dispatch the events to the appropriate handler. + lastMousePress = 0 + saveKeyb = b"" + saveKeys = b"" + listKeys = [] + for bb in irInBuf[:cNumRead.value]: + # if not bb.EventType: continue + # print(f"{bb=} {bb.EventType=} {cNumRead.value=}") + + if bb.EventType == MOUSE_EVENT: + x = bb.Event.MouseEvent.dwMousePosition.X + y = bb.Event.MouseEvent.dwMousePosition.Y + print(f"{bb.Event.MouseEvent.dwControlKeyState=}") + print(f"{bb.Event.MouseEvent.dwEventFlags=}") + bstate = bb.Event.MouseEvent.dwButtonState + cstate = bb.Event.MouseEvent.dwControlKeyState + + key = TTkMouseEvent.NoButton + evt = TTkMouseEvent.Move + mod = TTkK.NoModifier + tap = 0 + # Release the mouse + if not bstate and lastMousePress: + pass + + # Ignore the input if another button is pressed while holding the previous + if lastMousePress and lastMousePress & bstate: + continue + + # Release the mouse if another button is pressed + # while still holding the first one + if lastMousePress and lastMousePress != (bstate&lastMousePress): + pass + + if cstate & CAPSLOCK_ON: pass + if cstate & ENHANCED_KEY: pass + if cstate & LEFT_ALT_PRESSED: mod |= TTkK.AltModifier + if cstate & LEFT_CTRL_PRESSED: mod |= TTkK.ControlModifier + if cstate & NUMLOCK_ON: pass + if cstate & RIGHT_ALT_PRESSED: mod |= TTkK.AltModifier + if cstate & RIGHT_CTRL_PRESSED: mod |= TTkK.ControlModifier + if cstate & SCROLLLOCK_ON: pass + if cstate & SHIFT_PRESSED: mod |= TTkK.ShiftModifier + + # Exclude extra button pressed at the same time + if bstate & RIGHTMOST_BUTTON_PRESSED: + key = TTkMouseEvent.RightButton + lastMousePress = RIGHTMOST_BUTTON_PRESSED + elif bstate & FROM_LEFT_1ST_BUTTON_PRESSED: + key = TTkMouseEvent.LeftButton + lastMousePress = FROM_LEFT_1ST_BUTTON_PRESSED + elif bstate & FROM_LEFT_2ND_BUTTON_PRESSED: + key = TTkMouseEvent.MidButton + lastMousePress = FROM_LEFT_2ND_BUTTON_PRESSED + elif bstate & FROM_LEFT_3RD_BUTTON_PRESSED: + lastMousePress = 0 # FROM_LEFT_3RD_BUTTON_PRESSED + elif bstate & FROM_LEFT_4TH_BUTTON_PRESSED: + lastMousePress = 0 # FROM_LEFT_4TH_BUTTON_PRESSED + mevt = TTkMouseEvent(x, y, key, evt, mod, tap, "") + print(f"{str(mevt)=}") + + elif bb.EventType == WINDOW_BUFFER_SIZE_EVENT: + print(f"{bb.Event.WindowBufferSizeEvent=}") + print(f"{bb.Event.WindowBufferSizeEvent.dwSize.X=}") + print(f"{bb.Event.WindowBufferSizeEvent.dwSize.Y=}") + elif bb.EventType == KEY_EVENT: + # if not bb.Event.KeyEvent.bKeyDown: + # saveKeys += bb.Event.KeyEvent.uChar.UnicodeChar + # saveKeyb += bb.Event.KeyEvent.uChar.AsciiChar + # listKeys.append() + key = bb.Event.KeyEvent.uChar.UnicodeChar + if bb.Event.KeyEvent.bKeyDown or key == "\x1b": + if (bb.Event.KeyEvent.dwControlKeyState + and bb.Event.KeyEvent.wVirtualKeyCode ): + continue + listKeys.append(key) + #continue + print( + # f" evt:{bb.Event.KeyEvent}" + + f"\tkd:{bb.Event.KeyEvent.bKeyDown}" + + f"\trc:{bb.Event.KeyEvent.wRepeatCount}" + + f"\tVKC:{bb.Event.KeyEvent.wVirtualKeyCode}" + + f"\tVSC:{bb.Event.KeyEvent.wVirtualScanCode}" + + f"\tUC:{ord(bb.Event.KeyEvent.uChar.UnicodeChar):x}" + + f"\tAC:{bb.Event.KeyEvent.uChar.AsciiChar}" + + f"\tCKS:{bb.Event.KeyEvent.dwControlKeyState} -> {listKeys=}" + ) + + print(f"{listKeys=}") + kk = "".join(listKeys) + print(f"{kk=}") + kk = kk.encode("utf-16", "surrogatepass") + print(f"{kk=}") + kk = kk.decode("utf-16") + print(f"{kk=}") + + +# Restore input mode on exit. +if not SetConsoleMode(hStdIn, fdwSaveOldModeIn): + raise Exception("SetConsoleMode") + +if not SetConsoleMode(hStdOut, fdwSaveOldModeOut): + raise Exception("SetConsoleMode") + +# return 0; +# + +reset() +print('OK') diff --git a/tests/test.pty.006.terminal.04.py b/tests/test.pty.006.terminal.04.py new file mode 100755 index 00000000..f340f091 --- /dev/null +++ b/tests/test.pty.006.terminal.04.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 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. + +# This test is based on: +# pyte - In memory VTXXX-compatible terminal emulator. +# Terminal Emulator Example +# https://github.com/selectel/pyte/blob/master/examples/terminal_emulator.py +# +# pty — Pseudo-terminal utilities¶ +# https://docs.python.org/3/library/pty.html#example +# +# Using a pseudo-terminal to interact with interactive Python in a subprocess +# by Thomas Billinger +# https://gist.github.com/thomasballinger/7979808 +# +# Run interactive Bash with popen and a dedicated TTY Python +# https://stackoverflow.com/questions/41542960/run-interactive-bash-with-popen-and-a-dedicated-tty-python + +import os +import pty +import sys +import threading +import argparse +from select import select + +sys.path.append(os.path.join(sys.path[0],'..')) +import TermTk as ttk + +parser = argparse.ArgumentParser() +parser.add_argument('-d', help='Debug (Add LogViewer Panel)', action='store_true') +args = parser.parse_args() + +# ttk.TTkLog.use_default_file_logging() +root = ttk.TTk(layout=ttk.TTkGridLayout(), mouseTrack=True) + +split = ttk.TTkSplitter(parent=root, orientation=ttk.TTkK.VERTICAL) + +split.addItem(top := ttk.TTkLayout()) + +if args.d: + split.addWidget(ttk.TTkLogViewer(follow=False ), title='Log', size=20) + +quitBtn = ttk.TTkButton(text="QUIT", border=True) +quitBtn.clicked.connect(ttk.TTkHelper.quit) + +cb_c = ttk.TTkCheckbox(pos=(0,3),size=(20,1), text="CTRL-C (VINTR) ", checked=ttk.TTkK.Checked) +cb_s = ttk.TTkCheckbox(pos=(0,4),size=(20,1), text="CTRL-S (VSTOP) ", checked=ttk.TTkK.Checked) +cb_z = ttk.TTkCheckbox(pos=(0,5),size=(20,1), text="CTRL-Z (VSUSP) ", checked=ttk.TTkK.Checked) +cb_q = ttk.TTkCheckbox(pos=(0,6),size=(20,1), text="CTRL-Q (VSTART)", checked=ttk.TTkK.Checked) + +cb_c.stateChanged.connect(lambda x: ttk.TTkTerm.setSigmask(ttk.TTkTerm.Sigmask.CTRL_C,x==ttk.TTkK.Checked)) +cb_s.stateChanged.connect(lambda x: ttk.TTkTerm.setSigmask(ttk.TTkTerm.Sigmask.CTRL_S,x==ttk.TTkK.Checked)) +cb_z.stateChanged.connect(lambda x: ttk.TTkTerm.setSigmask(ttk.TTkTerm.Sigmask.CTRL_Z,x==ttk.TTkK.Checked)) +cb_q.stateChanged.connect(lambda x: ttk.TTkTerm.setSigmask(ttk.TTkTerm.Sigmask.CTRL_Q,x==ttk.TTkK.Checked)) + +win = ttk.TTkWindow(pos=(10,0), size=(100,30), title="Terminallo n.2", border=True, layout=ttk.TTkVBoxLayout(), flags = ttk.TTkK.WindowFlag.WindowMinMaxButtonsHint|ttk.TTkK.WindowFlag.WindowCloseButtonHint) +term = ttk.TTkTerminal(parent=win) +term.bell.connect(lambda : ttk.TTkLog.debug("BELL!!! 🔔🔔🔔")) +term.titleChanged.connect(win.setTitle) +term.runShell() +term.terminalClosed.connect(win.close) +win.closed.connect(term.close) + +top.addWidgets([quitBtn, cb_c, cb_s, cb_z, cb_q, win]) + +term.setFocus() + +root.mainloop() \ No newline at end of file diff --git a/tests/test.pty.006.terminal.05.py b/tests/test.pty.006.terminal.05.py new file mode 100755 index 00000000..84866981 --- /dev/null +++ b/tests/test.pty.006.terminal.05.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 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. + +import os, sys, argparse +from select import select + +sys.path.append(os.path.join(sys.path[0],'..')) +import TermTk as ttk + +parser = argparse.ArgumentParser() +parser.add_argument('-d', help='Debug (Add LogViewer Panel)', action='store_true') +args = parser.parse_args() + +# ttk.TTkLog.use_default_file_logging() +root = ttk.TTk(layout=ttk.TTkGridLayout(), mouseTrack=True) + +split = ttk.TTkSplitter(parent=root, orientation=ttk.TTkK.VERTICAL) + +split.addItem(top := ttk.TTkLayout()) + +if args.d: + split.addWidget(ttk.TTkLogViewer(follow=False ), title='Log', size=20) + +quitBtn = ttk.TTkButton(text="QUIT", border=True) +quitBtn.clicked.connect(ttk.TTkHelper.quit) + +cb_c = ttk.TTkCheckbox(pos=(0,3),size=(20,1), text="CTRL-C (VINTR) ", checked=ttk.TTkK.Checked) +cb_s = ttk.TTkCheckbox(pos=(0,4),size=(20,1), text="CTRL-S (VSTOP) ", checked=ttk.TTkK.Checked) +cb_z = ttk.TTkCheckbox(pos=(0,5),size=(20,1), text="CTRL-Z (VSUSP) ", checked=ttk.TTkK.Checked) +cb_q = ttk.TTkCheckbox(pos=(0,6),size=(20,1), text="CTRL-Q (VSTART)", checked=ttk.TTkK.Checked) + +cb_c.stateChanged.connect(lambda x: ttk.TTkTerm.setSigmask(ttk.TTkTerm.Sigmask.CTRL_C,x==ttk.TTkK.Checked)) +cb_s.stateChanged.connect(lambda x: ttk.TTkTerm.setSigmask(ttk.TTkTerm.Sigmask.CTRL_S,x==ttk.TTkK.Checked)) +cb_z.stateChanged.connect(lambda x: ttk.TTkTerm.setSigmask(ttk.TTkTerm.Sigmask.CTRL_Z,x==ttk.TTkK.Checked)) +cb_q.stateChanged.connect(lambda x: ttk.TTkTerm.setSigmask(ttk.TTkTerm.Sigmask.CTRL_Q,x==ttk.TTkK.Checked)) + +win = ttk.TTkWindow(pos=(10,0), size=(100,30), title="Terminallo n.2", border=True, layout=ttk.TTkVBoxLayout(), flags = ttk.TTkK.WindowFlag.WindowMinMaxButtonsHint|ttk.TTkK.WindowFlag.WindowCloseButtonHint) +term = ttk.TTkTerminal(parent=win) +term.bell.connect(lambda : ttk.TTkLog.debug("BELL!!! 🔔🔔🔔")) +term.titleChanged.connect(win.setTitle) +term.runShell() +term.terminalClosed.connect(win.close) +win.closed.connect(term.close) + +winT = ttk.TTkWindow(pos=(20,10), size=(100,30), title="TextEdit", border=True, layout=ttk.TTkVBoxLayout(), flags = ttk.TTkK.WindowFlag.WindowMinMaxButtonsHint|ttk.TTkK.WindowFlag.WindowCloseButtonHint) +ttk.TTkTextEdit(parent=winT, readOnly=False, lineNumber=True) + +top.addWidgets([quitBtn, cb_c, cb_s, cb_z, cb_q, win, winT]) + +term.setFocus() + +root.mainloop() \ No newline at end of file diff --git a/tests/test.ui.014.list.py b/tests/test.ui.014.list.01.py similarity index 100% rename from tests/test.ui.014.list.py rename to tests/test.ui.014.list.01.py diff --git a/tests/test.ui.014.list.03.py b/tests/test.ui.014.list.03.py new file mode 100755 index 00000000..9e7505f3 --- /dev/null +++ b/tests/test.ui.014.list.03.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 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. + +import sys, os, argparse, math, random + +sys.path.append(os.path.join(sys.path[0],'..')) +import TermTk as ttk + +zc1 = chr(0x07a6) # Zero width chars oަ +zc2 = chr(0x20D7) # Zero width chars o⃗ +zc3 = chr(0x065f) # Zero width chars oٟ +utfwords = [ + f"--Zero{zc1}{zc2}{zc3}-1-", f"--Zero-2{zc1}{zc2}{zc3}-", f"--Ze{zc1}{zc2}{zc3}ro-3-", f"{zc1}{zc2}{zc3}--Zero-4-", + "d😮l😱r", "sit", "am😎t,", "c😱nsectetur", "t😜mpor", "inci😜di😜dunt", "u😜t", "l😜abore", "et", "d😜olore", "m😜a😜gna", "ali😜qua😜.", "Ut", "enim", "😜a😜d😜", "minim", "veniam,", "😜q😜uis", "😜nostrud", "exer😜c😜i😜tation", "ullamco", "labo😜ris", "n😜isi", "ut", "aliq😞ip", "e😜x😜", "ea", "comm😞do", "cons😿quat.", "Duis", "aute", "irure", "dolor", "in", "reprehenderit", "in", "voluptate", "velit", "esse", "cillum", "dolore", "eu", "fugiat", "nulla", "pariatur.", "Excepteur", "sint", "occaecat", "cupidatat", "non", "proident,", "sunt", "in", "cul🙻a", "qui", "officia", "deserunt", "mollit", "anim", "id", "est", "laborum."] +words = ["Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit,", "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore", "magna", "aliqua.", "Ut", "enim", "ad", "minim", "veniam,", "quis", "nostrud", "exercitation", "ullamco", "laboris", "nisi", "ut", "aliquip", "ex", "ea", "commodo", "consequat.", "Duis", "aute", "irure", "dolor", "in", "reprehenderit", "in", "voluptate", "velit", "esse", "cillum", "dolore", "eu", "fugiat", "nulla", "pariatur.", "Excepteur", "sint", "occaecat", "cupidatat", "non", "proident,", "sunt", "in", "culpa", "qui", "officia", "deserunt", "mollit", "anim", "id", "est", "laborum."] +def getWord(): + return random.choice(utfwords) + # return random.choice(words) + +parser = argparse.ArgumentParser() +parser.add_argument('-t', help='Track Mouse', action='store_true') +args = parser.parse_args() +mouseTrack = args.t + +root = ttk.TTk(title="pyTermTk List Demo", mouseTrack=mouseTrack) + +# Define the main Layout +frame1 = ttk.TTkResizableFrame(parent=root, pos=( 0, 0), size=(30,30), title="Single List", border=0, layout=ttk.TTkVBoxLayout()) +frame2 = ttk.TTkResizableFrame(parent=root, pos=(30, 0), size=(30,30), title="Multi List", border=0, layout=ttk.TTkVBoxLayout()) +frame3 = ttk.TTkResizableFrame(parent=root, pos=(60, 0), size=(80,30), title="Log", border=0, layout=ttk.TTkVBoxLayout()) + +# Single Selection List +listWidgetSingle = ttk.TTkList(parent=frame1, maxWidth=40, minWidth=10) + +# Multi Selection List +listWidgetMulti = ttk.TTkList(parent=frame2, maxWidth=40, minWidth=10, selectionMode=ttk.TTkK.MultiSelection) + +# Log Viewer +label1 = ttk.TTkLabel(parent=root, pos=(10,30), text="[ list1 ]",maxHeight=2) +label2 = ttk.TTkLabel(parent=root, pos=(10,31), text="[ list2 ]",maxHeight=2) +ttk.TTkLogViewer(parent=frame3)#, border=True) + +btn_mv1 = ttk.TTkButton(parent=root, pos=(0,30), text=" >> ") +btn_mv2 = ttk.TTkButton(parent=root, pos=(0,31), text=" << ") +btn_del = ttk.TTkButton(parent=root, pos=(0,32), text="Delete") + +@ttk.pyTTkSlot(str) +def _listCallback1(label): + ttk.TTkLog.info(f'Clicked label1: "{label}"') + label1.setText(f'[ list1 ] clicked "{label}" - Selected: {[str(s) for s in listWidgetSingle.selectedLabels()]}') + +@ttk.pyTTkSlot(str) +def _listCallback2(label): + ttk.TTkLog.info(f'Clicked label2: "{label}" - selected: {[str(s) for s in listWidgetMulti.selectedLabels()]}') + label2.setText(f'[ list2 ] clicked "{label}" - {[str(s) for s in listWidgetMulti.selectedLabels()]}') + +@ttk.pyTTkSlot() +def _moveToRight2(): + for i in listWidgetSingle.selectedItems().copy(): + listWidgetSingle.removeItem(i) + listWidgetMulti.addItemAt(i,0) + +@ttk.pyTTkSlot() +def _moveToLeft1(): + for i in listWidgetMulti.selectedItems().copy(): + listWidgetMulti.removeItem(i) + listWidgetSingle.addItemAt(i,0) + +@ttk.pyTTkSlot() +def _delSelected(): + items = listWidgetMulti.selectedItems() + listWidgetMulti.removeItems(items) + items = listWidgetSingle.selectedItems() + listWidgetSingle.removeItems(items) + + +btn_mv1.clicked.connect(_moveToRight2) +btn_mv2.clicked.connect(_moveToLeft1) +btn_del.clicked.connect(_delSelected) + + +# Connect the signals to the 2 slots defines +listWidgetSingle.textClicked.connect(_listCallback1) +listWidgetMulti.textClicked.connect(_listCallback2) + +# populate the lists with random entries +for i in range(10): + listWidgetSingle.addItem(f"S-{i}) {getWord()} {getWord()}") + listWidgetMulti.addItem(f"M-{i}) {getWord()} {getWord()}") + +root.mainloop() diff --git a/tests/test.ui.014.list.04.py b/tests/test.ui.014.list.04.py new file mode 100755 index 00000000..64590d63 --- /dev/null +++ b/tests/test.ui.014.list.04.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 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. + +import sys, os, argparse, math, random + +sys.path.append(os.path.join(sys.path[0],'..')) +import TermTk as ttk + +zc1 = chr(0x07a6) # Zero width chars oަ +zc2 = chr(0x20D7) # Zero width chars o⃗ +zc3 = chr(0x065f) # Zero width chars oٟ +utfwords = [ + f"--Zero{zc1}{zc2}{zc3}-1-", f"--Zero-2{zc1}{zc2}{zc3}-", f"--Ze{zc1}{zc2}{zc3}ro-3-", f"{zc1}{zc2}{zc3}--Zero-4-", + "d😮l😱r", "sit", "am😎t,", "c😱nsectetur", "t😜mpor", "inci😜di😜dunt", "u😜t", "l😜abore", "et", "d😜olore", "m😜a😜gna", "ali😜qua😜.", "Ut", "enim", "😜a😜d😜", "minim", "veniam,", "😜q😜uis", "😜nostrud", "exer😜c😜i😜tation", "ullamco", "labo😜ris", "n😜isi", "ut", "aliq😞ip", "e😜x😜", "ea", "comm😞do", "cons😿quat.", "Duis", "aute", "irure", "dolor", "in", "reprehenderit", "in", "voluptate", "velit", "esse", "cillum", "dolore", "eu", "fugiat", "nulla", "pariatur.", "Excepteur", "sint", "occaecat", "cupidatat", "non", "proident,", "sunt", "in", "cul🙻a", "qui", "officia", "deserunt", "mollit", "anim", "id", "est", "laborum."] +words = ["Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit,", "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore", "magna", "aliqua.", "Ut", "enim", "ad", "minim", "veniam,", "quis", "nostrud", "exercitation", "ullamco", "laboris", "nisi", "ut", "aliquip", "ex", "ea", "commodo", "consequat.", "Duis", "aute", "irure", "dolor", "in", "reprehenderit", "in", "voluptate", "velit", "esse", "cillum", "dolore", "eu", "fugiat", "nulla", "pariatur.", "Excepteur", "sint", "occaecat", "cupidatat", "non", "proident,", "sunt", "in", "culpa", "qui", "officia", "deserunt", "mollit", "anim", "id", "est", "laborum."] +def getWord(): + return random.choice(utfwords) + # return random.choice(words) + +parser = argparse.ArgumentParser() +parser.add_argument('-t', help='Track Mouse', action='store_true') +args = parser.parse_args() +mouseTrack = args.t + +root = ttk.TTk(title="pyTermTk List Demo", mouseTrack=mouseTrack) + +# Define the main Layout +frame1 = ttk.TTkWindow(parent=root, pos=( 0, 0), size=(30,30), title="Single List", border=0, layout=ttk.TTkVBoxLayout()) +frame2 = ttk.TTkWindow(parent=root, pos=(30, 0), size=(30,30), title="Multi List", border=0, layout=ttk.TTkVBoxLayout()) +frame3 = ttk.TTkWindow(parent=root, pos=(60, 0), size=(80,30), title="Log", border=0, layout=ttk.TTkVBoxLayout()) + +# Single Selection List +listWidgetSingle = ttk.TTkList(parent=frame1, maxWidth=40, minWidth=10) + +# Multi Selection List +listWidgetMulti = ttk.TTkList(parent=frame2, maxWidth=40, minWidth=10, selectionMode=ttk.TTkK.MultiSelection) + +# Log Viewer +label1 = ttk.TTkLabel(parent=root, pos=(10,30), text="[ list1 ]",maxHeight=2) +label2 = ttk.TTkLabel(parent=root, pos=(10,31), text="[ list2 ]",maxHeight=2) +ttk.TTkLogViewer(parent=frame3)#, border=True) + +btn_mv1 = ttk.TTkButton(parent=root, pos=(0,30), text=" >> ") +btn_mv2 = ttk.TTkButton(parent=root, pos=(0,31), text=" << ") +btn_del = ttk.TTkButton(parent=root, pos=(0,32), text="Delete") + +@ttk.pyTTkSlot(str) +def _listCallback1(label): + ttk.TTkLog.info(f'Clicked label1: "{label}"') + label1.setText(f'[ list1 ] clicked "{label}" - Selected: {[str(s) for s in listWidgetSingle.selectedLabels()]}') + +@ttk.pyTTkSlot(str) +def _listCallback2(label): + ttk.TTkLog.info(f'Clicked label2: "{label}" - selected: {[str(s) for s in listWidgetMulti.selectedLabels()]}') + label2.setText(f'[ list2 ] clicked "{label}" - {[str(s) for s in listWidgetMulti.selectedLabels()]}') + +@ttk.pyTTkSlot() +def _moveToRight2(): + for i in listWidgetSingle.selectedItems().copy(): + listWidgetSingle.removeItem(i) + listWidgetMulti.addItemAt(i,0) + +@ttk.pyTTkSlot() +def _moveToLeft1(): + for i in listWidgetMulti.selectedItems().copy(): + listWidgetMulti.removeItem(i) + listWidgetSingle.addItemAt(i,0) + +@ttk.pyTTkSlot() +def _delSelected(): + items = listWidgetMulti.selectedItems() + listWidgetMulti.removeItems(items) + items = listWidgetSingle.selectedItems() + listWidgetSingle.removeItems(items) + + +btn_mv1.clicked.connect(_moveToRight2) +btn_mv2.clicked.connect(_moveToLeft1) +btn_del.clicked.connect(_delSelected) + + +# Connect the signals to the 2 slots defines +listWidgetSingle.textClicked.connect(_listCallback1) +listWidgetMulti.textClicked.connect(_listCallback2) + +# populate the lists with random entries +for i in range(10): + listWidgetSingle.addItem(f"S-{i}) {getWord()} {getWord()}") + listWidgetMulti.addItem(f"M-{i}) {getWord()} {getWord()}") + +root.mainloop() diff --git a/tests/timeit/21.weakref.01.py b/tests/timeit/21.weakref.01.py new file mode 100755 index 00000000..4eb4b1e5 --- /dev/null +++ b/tests/timeit/21.weakref.01.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 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. + +import sys, os, weakref + +import timeit +from threading import Lock + +class Foo(): + def a(self,v): + return v+v*v + +f1 = Foo() +f21 = Foo() +f22 = Foo() + +a1 = f1.a +a21 = weakref.WeakMethod(f21.a) +a22 = weakref.WeakMethod(f22.a) +a31 = weakref.ref(_a31:=f21.a) +a32 = weakref.ref(_a32:=f22.a) + +del f22,_a32 + +def test1(v=a1,ff=f1): return sum([ v(x) for x in range(100)]) + +def test2(v=a21,ff=f21): return sum([v()(x) if v() else 0 for x in range(100)]) +def test3(v=a22,ff=f21): return sum([v()(x) if v() else 0 for x in range(100)]) +def test4(v=a21,ff=f21): return sum([ _v(x) if (_v:=v()) else 0 for x in range(100)]) +def test5(v=a22,ff=f21): return sum([ _v(x) if (_v:=v()) else 0 for x in range(100)]) + +def test6(v=a31,ff=f21): return sum([v()(x) if v() else 0 for x in range(100)]) +def test7(v=a32,ff=f21): return sum([v()(x) if v() else 0 for x in range(100)]) +def test8(v=a31,ff=f21): return sum([ _v(x) if (_v:=v()) else 0 for x in range(100)]) +def test9(v=a32,ff=f21): return sum([ _v(x) if (_v:=v()) else 0 for x in range(100)]) + +loop = 10000 + +a = {} + +iii = 1 +while (testName := f'test{iii}') and (testName in globals()): + 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"test{iii:02}) | {result / loop:.10f} sec. | {loop / result : 15.3f} Fps ╞╡-> {globals()[testName](*a)}") + iii+=1 + diff --git a/tests/timeit/22.queue.01.py b/tests/timeit/22.queue.01.py new file mode 100755 index 00000000..0406005c --- /dev/null +++ b/tests/timeit/22.queue.01.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 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. + +import sys, os, weakref + +import timeit +from queue import Queue + +qu = Queue() +r = 100 + +def test1(q=qu): + ret = 0 + for i in range(r): + ret += i + return ret + +def test2(q=qu): + ret = 0 + for i in range(r): + qu.put(i) + while x := q.get(): + ret += x + return ret + +def test3(q=qu): + ret = 0 + ar = [] + for i in range(r): + ar.append(i) + for x in ar: + ret += x + return ret + +def test4(q=qu): + return sum(i for i in range(r)) + +def test5(q=qu): + ar = [] + for i in range(r): + ar.append(i) + return sum(ar) + +def test6(q=qu): + ret = 0 + ar = [] + for i in range(r): + ar.append(i) + while ar: + ret += ar.pop() + return ret + +loop = 1000 + +a = {} + +iii = 1 +while (testName := f'test{iii}') and (testName in globals()): + 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"test{iii:02}) | {result / loop:.10f} sec. | {loop / result : 15.3f} Fps ╞╡-> {globals()[testName](*a)}") + iii+=1 + diff --git a/tests/test.generic.006.weakref.01.py b/tests/weakref/test.01.py similarity index 100% rename from tests/test.generic.006.weakref.01.py rename to tests/weakref/test.01.py diff --git a/tests/test.generic.006.weakref.02.py b/tests/weakref/test.02.py similarity index 100% rename from tests/test.generic.006.weakref.02.py rename to tests/weakref/test.02.py diff --git a/tests/test.generic.006.weakref.03.py b/tests/weakref/test.03.py similarity index 100% rename from tests/test.generic.006.weakref.03.py rename to tests/weakref/test.03.py diff --git a/tests/weakref/test.04.gc.01.py b/tests/weakref/test.04.gc.01.py new file mode 100755 index 00000000..d41e6883 --- /dev/null +++ b/tests/weakref/test.04.gc.01.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 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. + +# Example inspired by +# https://stackoverflow.com/questions/39838793/python-object-is-being-referenced-by-an-object-i-cannot-find + +import gc, weakref + +class Foo(object): + __slots__ = ('a','b') + def __init__(self, a=1234) -> None: + self.a = a + self.b = lambda : self.a + + def f(self): + return self.a + + # def __del__(self): + # print(f"Deleted {self}") + +def pobjs(): + for i,o in enumerate(gc.get_objects()[-100:]): + ss = str(o) + if "Foo" in ss: + print(f" * {i} - {ss}") + +v1 = {'b':2345} + +print(f"\nStart {gc.isenabled()=}") +# print(f"{gc.set_debug(gc.DEBUG_LEAK)=}") + +print("\n############# Phase 1 ##################") +foo = Foo(v1) +print(f"{gc.get_referents(foo)=}") +print(f"{gc.get_count()=}") +print(f"{foo.a=} - {foo.b=} - {foo.f()=}") +del foo +print(f"{gc.collect()=}") + +print("\n############# Phase 2 ##################") +foo = Foo(v1) +bar = foo.a +print(f"{gc.get_referents(foo)=}") +print(f"{gc.get_count()=}") +print(f"{foo.a=} - {foo.b=} - {foo.f()=} - {bar=}") +del foo +print(f"{gc.collect()=}") +print(f"{bar=}") + +print("\n############# Phase 3 ##################") +foo = Foo(v1) +bar = foo.b +print(f"{gc.get_referents(foo)=}") +print(f"{gc.get_count()=}") +print(f"{foo.a=} - {foo.b=} - {foo.f()=} - {bar()=}") +del foo +print(f"{gc.collect()=}") +print(f"{bar()=}") + +print("\n############# Phase 4 ##################") +foo = Foo(v1) +bar = foo.b +print(f"{gc.get_referents(foo)=}") +print(f"{gc.get_referents(v1)=}") +print(f"{gc.get_count()=}") +print(f"{foo.a=} - {foo.b=} - {foo.f()=} - {bar()=}") +del foo +pobjs() +print(f"{gc.collect()=}") +print(f"{bar()=}") +del bar +pobjs() +print(f"{gc.collect()=}") +pobjs() + +print("\n############# Phase 5 ##################") +foo = Foo(v1) +bar = weakref.ref(foo.b) +xx = foo.f +baz = weakref.ref(xx) +print(f"{gc.get_referents(foo)=}") +print(f"{gc.get_referents(v1)=}") +print(f"{gc.get_count()=}") +print(f"{foo.a=} - {foo.b=} - {foo.f()=} - {bar()()=}") +del foo +pobjs() +print(f"{gc.collect()=}") +print(f"{bar()() if bar() else None=}") +del bar +pobjs() +print(f"{gc.collect()=}") +pobjs() + +print(f"{gc.garbage=}") +print(f"End {gc.get_count()=}") diff --git a/tests/weakref/test.04.gc.02.py b/tests/weakref/test.04.gc.02.py new file mode 100755 index 00000000..9131f429 --- /dev/null +++ b/tests/weakref/test.04.gc.02.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 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. + +# Example inspired by +# https://stackoverflow.com/questions/39838793/python-object-is-being-referenced-by-an-object-i-cannot-find + +import gc, weakref, time + +class Foo(): + __slots__ = ('__weakref__','a','b') + def __init__(self, a=1234) -> None: + self.a = a + self.b = lambda : self.a + + def f(self): + return self.a + + # def __del__(self): + # print(f"Deleted {self}") + +def pobjs(): + for i,o in enumerate(gc.get_objects()[-100:]): + ss = str(o) + if "Foo" in ss: + print(f" * {i} - {ss}") + +v1 = {'b':2345} + +print(f"\nStart {gc.isenabled()=}") +# print(f"{gc.set_debug(gc.DEBUG_LEAK)=}") + +def _gccb(phase,info): + print(f" ---> {gc.garbage=}") + print(f" ---> {phase=} {info=}") + +# gc.callbacks.append(_gccb) + +print("\n############# Phase 1 ##################") +foo = Foo(v1) +bar =foo.b + +wrfoo = weakref.ref(foo) +wrbar = weakref.ref(bar) +wrf = weakref.WeakMethod(foo.f) + +# print(f"{gc.get_referents(foo)=}") +# print(f"{gc.get_referrers(foo)=}") +# print(f"{gc.get_referents(v1)=}") +# print(f"{gc.get_referrers(v1)=}") +# print(f"{gc.get_count()=}") +print(f"{foo.a=} - {foo.b=} - {foo.f()=} - {bar()=}") +print(f"{wrfoo()=} {wrbar()=} {wrf()=}") +del foo +print(f"{gc.collect()=}") +print(f"{bar()}") +# print(f"{gc.get_referents(v1)=}") +# print(f"{gc.get_referrers(v1)=}") +print(f"{wrfoo()=} {wrbar()=} {wrf()=}") +bar = None +print(f"{wrfoo()=} {wrbar()=} {wrf()=}") +time.sleep(4) +print(f"{wrfoo()=} {wrbar()=} {wrf()=}") +print(f"{gc.collect()=}") +print(f"{wrfoo()=} {wrbar()=} {wrf()=}") + +print(f"{gc.garbage=}") +print(f"End {gc.get_count()=}") diff --git a/tests/weakref/test.04.gc.03.py b/tests/weakref/test.04.gc.03.py new file mode 100755 index 00000000..e678cb66 --- /dev/null +++ b/tests/weakref/test.04.gc.03.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 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. + +# Example inspired by +# https://stackoverflow.com/questions/39838793/python-object-is-being-referenced-by-an-object-i-cannot-find + +import gc, weakref, time + +class Bar(): + __slots__ = ('_foo') + def __init__(self, foo) -> None: + self._foo = foo + +class Foo(): + __slots__ = ('__weakref__','a','b','_bar') + def __init__(self, a=1234) -> None: + self._bar = Bar(self) + self.a = a + self.b = lambda : self.a + + def f(self): + return self.a + + # def __del__(self): + # print(f"Deleted {self}") + +def pobjs(): + for i,o in enumerate(gc.get_objects()[-100:]): + ss = str(o) + if "Foo" in ss: + print(f" * {i} - {ss}") + +def _ref(o): + print(f"\n### -> Referents - {o}") + for i,r in enumerate(gc.get_referents(o)): + print(f" - {i} ) {r}") + print(f"\n### -> Referrers - {o}") + for i,r in enumerate(gc.get_referrers(o)): + print(f" - {i} ) {r}") + print("") + +v1 = {'b':2345} + +print(f"\nStart {gc.isenabled()=}") +# print(f"{gc.set_debug(gc.DEBUG_LEAK)=}") + +def _gccb(phase,info): + print(f" ---> {gc.garbage=}") + print(f" ---> {phase=} {info=}") + +# gc.callbacks.append(_gccb) + +print("\n############# Phase 1 ##################") +foo = Foo(v1) +bar =foo.b + +wrfoo = weakref.ref(foo) +wrbar = weakref.ref(bar) +wrf = weakref.WeakMethod(foo.f) + +# print(f"{gc.get_referents(foo)=}") +# print(f"{gc.get_referrers(foo)=}") +# print(f"{gc.get_referents(v1)=}") +# print(f"{gc.get_referrers(v1)=}") +# print(f"{gc.get_count()=}") +_ref(foo) + +print(f"{foo.a=} - {foo.b=} - {foo.f()=} - {bar()=}") +print(f"{wrfoo()=} {wrbar()=} {wrf()=}") +del foo +print(f"{gc.collect()=}") +print(f"{bar()}") +# print(f"{gc.get_referents(v1)=}") +# print(f"{gc.get_referrers(v1)=}") +print(f"{wrfoo()=} {wrbar()=} {wrf()=}") +bar = None +print(f"{wrfoo()=} {wrbar()=} {wrf()=}") +time.sleep(4) +print(f"{wrfoo()=} {wrbar()=} {wrf()=}") +print(f"{gc.collect()=}") +print(f"{wrfoo()=} {wrbar()=} {wrf()=}") + +print(f"{gc.garbage=}") +print(f"End {gc.get_count()=}") diff --git a/tests/weakref/test.05.TermTk.01.py b/tests/weakref/test.05.TermTk.01.py new file mode 100755 index 00000000..e924dd3f --- /dev/null +++ b/tests/weakref/test.05.TermTk.01.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 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. + +import sys, os +import gc, weakref, time + +sys.path.append(os.path.join(sys.path[0],'../..')) +sys.path.append(os.path.join(sys.path[0],'.')) +import TermTk as ttk + +def pobjs(): + for i,o in enumerate(gc.get_objects()[-100:]): + ss = str(o) + if "Foo" in ss: + print(f" * {i} - {ss}") + +print(f"\nStart {gc.isenabled()=}") + +def _gccb(phase,info): + print(f" ---> {gc.garbage=}") + print(f" ---> {phase=} {info=}") +# gc.callbacks.append(_gccb) + +def _ref(o): + print(f"\n### -> Referents - {o}") + for i,r in enumerate(gc.get_referents(o)): + print(f" - {i} ) {r}") + print(f"\n### -> Referrers - {o}") + for i,r in enumerate(gc.get_referrers(o)): + print(f" - {i} ) {r}") + for ii,rr in enumerate(gc.get_referrers(r)): + print(f" | {ii} ) {rr}") + print("") + print("") + +print("\n############# Phase 1 ##################") +# wid = ttk.TTkWidget() +wid = ttk.TTkButton() +# wid = ttk.TTkLabel() +# wid = ttk.TTkGraph() +# wid = ttk.TTkSpacer() +# wid = ttk.TTkSplitter() +# wid = ttk.TTkCanvas() +# sizef = wid.size +sizef = [] + +ttk.TTkHelper._updateWidget = set() +ttk.TTkHelper._updateBuffer = set() + +wrwid = weakref.ref(wid) +# wrsizef = weakref.ref(sizef) +wrsizef = wrwid +# wrsizef2 = weakref.WeakMethod(wid.size) +wrsizef2 = wrwid + +_ref(wid) + +print(f"{wrwid()=} {wrsizef()=} {wrsizef2()=}") +del wid +print(f"{gc.collect()=}") +# print(f"{sizef()}") +print(f"{wrwid()=} {wrsizef()=} {wrsizef2()=}") +sizef = None +print(f"{wrwid()=} {wrsizef()=} {wrsizef2()=}") +# time.sleep(4) +# print(f"{wrwid()=} {wrsizef()=} {wrsizef2()=}") +print(f"{gc.collect()=}") +print(f"{wrwid()=} {wrsizef()=} {wrsizef2()=}") + +print(f"{gc.garbage=}") +print(f"End {gc.get_count()=}") diff --git a/tests/weakref/test.05.TermTk.02.py b/tests/weakref/test.05.TermTk.02.py new file mode 100755 index 00000000..e12812ad --- /dev/null +++ b/tests/weakref/test.05.TermTk.02.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 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. + +import sys, os +import gc, weakref, time + +sys.path.append(os.path.join(sys.path[0],'../..')) +sys.path.append(os.path.join(sys.path[0],'.')) +import TermTk as ttk + +def pobjs(): + for i,o in enumerate(gc.get_objects()[-100:]): + ss = str(o) + if "Foo" in ss: + print(f" * {i} - {ss}") + +print(f"\nStart {gc.isenabled()=}") + +def _gccb(phase,info): + print(f" ---> {gc.garbage=}") + print(f" ---> {phase=} {info=}") +# gc.callbacks.append(_gccb) + +def _ref(o): + print(f"\n### -> Referents - {o}") + for i,r in enumerate(gc.get_referents(o)): + print(f" - {i} ) {r}") + print(f"\n### -> Referrers - {o}") + for i,r in enumerate(gc.get_referrers(o)): + print(f" - {i} ) {r}") + for ii,rr in enumerate(gc.get_referrers(r)): + print(f" | {ii} ) {rr}") + print("") + print("") + + +class TestWid(ttk.TTkWidget): + __slots__ = ('_a','_b') + def __init__(self, *args, **kwargs): + self.setDefaultSize(kwargs, 10, 10) + super().__init__(*args, **kwargs) + self._b = ttk.pyTTkSignal(bool) + self.setFocusPolicy(ttk.TTkK.ClickFocus + ttk.TTkK.TabFocus) + + def mousePressEvent(self, evt): + # TTkLog.debug(f"{self._text} Test Mouse {evt}") + self.update() + return True + + def paintEvent(self, canvas): + canvas.fill(pos=(0,0), size=(2,2)) + +print("\n############# Phase 1 ##################") +# wid = ttk.TTkWidget() +wid = ttk.TTkButton() +# wid = ttk.TTkGraph() +# wid = ttk.TTkSpacer() +# wid = ttk.TTkSplitter() +# wid = TestWid() +# sizef = wid.size +sizef = [] + +wrwid = weakref.ref(wid) +# wrsizef = weakref.ref(sizef) +wrsizef = wrwid +# wrsizef2 = weakref.WeakMethod(wid.size) +wrsizef2 = wrwid + +_ref(wid) + +print(f"{wrwid()=} {wrsizef()=} {wrsizef2()=}") +del wid +print(f"{gc.collect()=}") +# print(f"{sizef()}") +print(f"{wrwid()=} {wrsizef()=} {wrsizef2()=}") +sizef = None +print(f"{wrwid()=} {wrsizef()=} {wrsizef2()=}") +# time.sleep(4) +# print(f"{wrwid()=} {wrsizef()=} {wrsizef2()=}") +print(f"{gc.collect()=}") +print(f"{wrwid()=} {wrsizef()=} {wrsizef2()=}") + +print(f"{gc.garbage=}") +print(f"End {gc.get_count()=}") diff --git a/tests/weakref/test.05.TermTk.03.signals.py b/tests/weakref/test.05.TermTk.03.signals.py new file mode 100755 index 00000000..8446847a --- /dev/null +++ b/tests/weakref/test.05.TermTk.03.signals.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 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. + +import sys, os +import gc, weakref, time + +sys.path.append(os.path.join(sys.path[0],'../..')) +sys.path.append(os.path.join(sys.path[0],'.')) +import TermTk as ttk + +def pobjs(): + for i,o in enumerate(gc.get_objects()[-100:]): + ss = str(o) + if "Foo" in ss: + print(f" * {i} - {ss}") + +print(f"\nStart {gc.isenabled()=}") + +def _gccb(phase,info): + print(f" ---> {gc.garbage=}") + print(f" ---> {phase=} {info=}") +# gc.callbacks.append(_gccb) + +def _ref(o): + print(f"\n### -> Referents - {o}") + for i,r in enumerate(gc.get_referents(o)): + print(f" - {i} ) {r}") + print(f"\n### -> Referrers - {o}") + for i,r in enumerate(gc.get_referrers(o)): + print(f" - {i} ) {r}") + for ii,rr in enumerate(gc.get_referrers(r)): + print(f" | {ii} ) {rr}") + print("") + print("") + +print("\n############# Phase 1 ##################") + +root = ttk.TTkWidget() +# wid = ttk.TTkWidget() +wid = ttk.TTkButton() +# wid = ttk.TTkLabel() +# wid = ttk.TTkGraph() +# wid = ttk.TTkSpacer() +# wid = ttk.TTkSplitter() +# sizef = wid.size +sizef = [] + +root.closed.connect(wid.close) + +ttk.TTkHelper._updateWidget = set() +ttk.TTkHelper._updateBuffer = set() + +wrwid = weakref.ref(wid) +# wrsizef = weakref.ref(sizef) +wrsizef = wrwid +# wrsizef2 = weakref.WeakMethod(wid.size) +wrsizef2 = wrwid + +_ref(wid) + +print(f"{wrwid()=} {wrsizef()=} {wrsizef2()=}") +del wid +print(f"{gc.collect()=}") +# print(f"{sizef()}") +print(f"{wrwid()=} {wrsizef()=} {wrsizef2()=}") +sizef = None +print(f"{wrwid()=} {wrsizef()=} {wrsizef2()=}") +# time.sleep(4) +# print(f"{wrwid()=} {wrsizef()=} {wrsizef2()=}") +print(f"{gc.collect()=}") +print(f"{wrwid()=} {wrsizef()=} {wrsizef2()=}") + +print(f"{gc.garbage=}") +print(f"End {gc.get_count()=}") diff --git a/tools/check.import.sh b/tools/check.import.sh index 8c7e41fc..f29f88a4 100755 --- a/tools/check.import.sh +++ b/tools/check.import.sh @@ -14,18 +14,11 @@ __check(){ -e "log.py:import inspect" \ -e "log.py:import logging" \ -e "log.py:from collections.abc import Callable, Set" \ - -e "from time" -e "input.py:import platform" \ - -e "readinputlinux.py:import sys, os" \ - -e "readinputlinux.py:from select import select" \ - -e "readinputlinux_thread.py:import sys, os" \ - -e "readinputlinux_thread.py:from select import select" \ - -e "readinputlinux_thread.py:import threading" \ - -e "readinputlinux_thread.py:import queue" \ + -e "input.py:import platform" \ + -e "input.py:from time import time" \ -e "term.py:import importlib.util" \ -e "term.*.py:import sys, os, signal" \ -e "term.*.py:from .term_base import TTkTermBase" \ - -e "term_pyodide.py:import pyodideProxy" \ - -e "term_unix.py:from threading import Thread, Lock" \ -e "timer.py:import importlib" \ -e "timer_unix.py:import threading" \ -e "timer_pyodide.py:import pyodideProxy" \ @@ -40,12 +33,34 @@ __check(){ -e "string.py:import unicodedata" \ -e "progressbar.py:import math" \ -e "uiloader.py:import json" \ - -e "uiproperties.py:from .properties import *" \ + -e "uiproperties.py:from .properties.* import" \ -e "util.py:import zlib, pickle, base64" \ -e "propertyanimation.py:from inspect import getfullargspec" \ -e "propertyanimation.py:from types import LambdaType" \ -e "propertyanimation.py:import time, math" \ - -e "terminal.py:from select import select" + -e "terminal.py:from select import select" | + grep -v \ + -e "TTkTerm/input.py:from ..drivers import TTkInputDriver" \ + -e "TTkTerm/term.py:from ..drivers import *" \ + -e "drivers/unix_thread.py:import sys, os" \ + -e "drivers/unix_thread.py:from select import select" \ + -e "drivers/unix_thread.py:import threading" \ + -e "drivers/unix_thread.py:import queue" \ + -e "drivers/unix.py:import sys, os, re" \ + -e "drivers/unix.py:import signal" \ + -e "drivers/unix.py:from select import select" \ + -e "drivers/windows.py:import signal" \ + -e "drivers/windows.py:from ctypes import Structure, Union, byref, wintypes, windll" \ + -e "drivers/term_windows.py:import sys, os" \ + -e "drivers/term_windows.py:from threading import Thread, Lock" \ + -e "drivers/term_windows.py:from ..TTkTerm.term_base import TTkTermBase" \ + -e "drivers/term_windows.py:from .windows import *" \ + -e "drivers/term_unix.py:from ..TTkTerm.term_base import TTkTermBase" \ + -e "drivers/term_unix.py:from threading import Thread, Lock" \ + -e "drivers/term_pyodide.py:import pyodideProxy" \ + -e "drivers/term_pyodide.py:from ..TTkTerm.term_base import TTkTermBase" \ + -e "drivers/__init__.py:import importlib.util" \ + -e "drivers/__init__.py:import platform" } ; if __check ; then diff --git a/ttkDesigner/app/propertyeditor.py b/ttkDesigner/app/propertyeditor.py index bbdbfa2b..527a6ada 100644 --- a/ttkDesigner/app/propertyeditor.py +++ b/ttkDesigner/app/propertyeditor.py @@ -183,7 +183,7 @@ class PropertyEditor(ttk.TTkGridLayout): # Color Fields def _processTTkColor(name, prop): getval = prop['get']['cb'](domw) - value = ttk.TTkWidget(layout=ttk.TTkHBoxLayout(), height=1) + value = ttk.TTkContainer(layout=ttk.TTkHBoxLayout(), height=1) value.layout().addWidget(_cb := ttk.TTkColorButtonPicker(color=getval, height=1)) value.layout().addWidget(_rc := ttk.TTkButton(text=ttk.TTkString('x',ttk.TTkColor.fg('#FFAA00')),maxWidth=3)) _cb.colorSelected.connect(_bound(prop['set']['cb'],domw,lambda v:v)) diff --git a/ttkDesigner/app/superobj/__init__.py b/ttkDesigner/app/superobj/__init__.py index 97c78875..00d94415 100644 --- a/ttkDesigner/app/superobj/__init__.py +++ b/ttkDesigner/app/superobj/__init__.py @@ -25,10 +25,12 @@ from .supercontrol import SuperControlWidget from .superwidget import SuperWidget from .superwidgetcontainer import SuperWidgetContainer -from .superwidgettextedit import SuperWidgetTextEdit +from .superwidgetabstractscrollarea import SuperWidgetAbstractScrollArea +# from .superwidgettextedit import SuperWidgetTextEdit from .superwidgetradiobutton import SuperWidgetRadioButton from .superwidgetframe import SuperWidgetFrame from .superwidgetsplitter import SuperWidgetSplitter +# from .superwidgetlist import SuperWidgetList from .superwidgetmenubutton import SuperWidgetMenuButton from .superlayout import SuperLayout diff --git a/ttkDesigner/app/superobj/superwidget.py b/ttkDesigner/app/superobj/superwidget.py index 7688e516..216da1e0 100644 --- a/ttkDesigner/app/superobj/superwidget.py +++ b/ttkDesigner/app/superobj/superwidget.py @@ -99,14 +99,17 @@ class SuperWidget(ttk.TTkContainer): def swFromWidget(wid:object, *args, **kwargs): swClass = so.SuperWidget for c, sc in { - ttk.TTkTextEdit: so.SuperWidgetTextEdit, + # ttk.TTkTextEdit: so.SuperWidgetTextEdit, ttk.TTkRadioButton: so.SuperWidgetRadioButton, # ttk.TTkResizableFrame: so.SuperWidgetFrame, # ttk.TTkWindow: so.SuperWidgetFrame, ttk.TTkSplitter: so.SuperWidgetSplitter, + # ttk.TTkList: so.SuperWidgetList, ttk.TTkMenuButton: so.SuperWidgetMenuButton, ttk.TTkFrame: so.SuperWidgetFrame, + ttk.TTkAbstractScrollArea: so.SuperWidgetAbstractScrollArea, ttk.TTkContainer: so.SuperWidgetContainer, + ttk.TTkWidget: so.SuperWidget, }.items(): if c in type(wid).mro(): swClass = sc diff --git a/ttkDesigner/app/superobj/superwidgetabstractscrollarea.py b/ttkDesigner/app/superobj/superwidgetabstractscrollarea.py new file mode 100644 index 00000000..bcf33ad8 --- /dev/null +++ b/ttkDesigner/app/superobj/superwidgetabstractscrollarea.py @@ -0,0 +1,44 @@ +# MIT License +# +# Copyright (c) 2023 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. + +import TermTk as ttk +import ttkDesigner.app.superobj as so +from .superobj import SuperObject + + +class SuperWidgetAbstractScrollArea(so.SuperWidgetContainer): + @staticmethod + def _swFromWidget(wid, swClass, *args, **kwargs): + return swClass(wid=wid, *args, **kwargs) + + def getSuperProperties(self): + additions, exceptions, exclude = super().getSuperProperties() + exclude += ['Layout','Padding'] + return additions, exceptions, exclude + + def dumpDict(self): + wid = self._wid + ret = { + 'class' : wid.__class__.__name__, + 'params' : SuperObject.dumpParams(wid,exclude=['Layout','Padding']), + } + return ret diff --git a/ttkDesigner/app/superobj/superwidgetlist.py b/ttkDesigner/app/superobj/superwidgetlist.py new file mode 100644 index 00000000..4734e41c --- /dev/null +++ b/ttkDesigner/app/superobj/superwidgetlist.py @@ -0,0 +1,29 @@ +# MIT License +# +# Copyright (c) 2023 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. + +import TermTk as ttk +import ttkDesigner.app.superobj as so +from .superobj import SuperObject + + +class SuperWidgetList(so.SuperWidgetAbstractScrollArea): + pass diff --git a/ttkDesigner/app/widgetbox.py b/ttkDesigner/app/widgetbox.py index 3d68d518..86544028 100644 --- a/ttkDesigner/app/widgetbox.py +++ b/ttkDesigner/app/widgetbox.py @@ -56,7 +56,8 @@ dWidgets = { }, 'Widgets':{ "Label" : { "class":ttk.TTkLabel, "params":{'size':(20,1), 'text':'Label'}}, - "List" : { "class":ttk.TTkListWidget, "params":{'size':(20,1)}, "disabled": True}, + "List" : { "class":ttk.TTkList, "params":{'size':(20,5)}}, + # "List Widget" : { "class":ttk.TTkListWidget, "params":{'size':(20,5)}}, "Scroll Area" : { "class":ttk.TTkScrollArea, "params":{'size':(20,5)}, "disabled": True}, "Spacer" : { "class":ttk.TTkSpacer, "params":{'size':(10,5)}}, "Tab Widget" : { "class":ttk.TTkTabWidget, "params":{'size':(20,3)}, "disabled": True}, diff --git a/tests/test.input.win.py b/tutorial/examples/TTkWidget/Focus.01.py old mode 100755 new mode 100644 similarity index 54% rename from tests/test.input.win.py rename to tutorial/examples/TTkWidget/Focus.01.py index 5e835751..fbc3a719 --- a/tests/test.input.win.py +++ b/tutorial/examples/TTkWidget/Focus.01.py @@ -2,7 +2,7 @@ # MIT License # -# Copyright (c) 2021 Eugenio Parodi +# Copyright (c) 2023 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 @@ -22,36 +22,21 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +# Those 2 lines are required to use the TermTk library straight from the main folder import sys, os -import logging +sys.path.append(os.path.join(sys.path[0],'../../..')) -sys.path.append(os.path.join(sys.path[0],'..')) -from TermTk import TTkLog, TTkK, TTkGridLayout, TTk, TTkLogViewer, TTkHelper +import TermTk as ttk -def keyCallback(kevt=None, mevt=None): - if mevt is not None: - TTkLog.info(f"Mouse Event: {mevt}") - if kevt is not None: - if kevt.type == TTkK.Character: - TTkLog.info(f"Key Event: char '{kevt.key}' {kevt}") - else: - TTkLog.info(f"Key Event: Special '{kevt}'") - if kevt.key == "q": - input.close() - return False - return True +root=ttk.TTk() -def pasteCallback(txt:str): - TTkLog.info(f"PASTE:") - for s in txt.split('\n'): - TTkLog.info(f" | {s}") - return True +btn1 = ttk.TTkButton(parent=root, pos=(5,2), text='Button 1 - unfocussed') +btn2 = ttk.TTkButton(parent=root, pos=(5,3), text='Button 2 - unfocussed') +btn3 = ttk.TTkButton(parent=root, pos=(5,4), text='Button 3 - unfocussed') +btn4 = ttk.TTkButton(parent=root, pos=(5,5), text='Button 4 - focussed') +btn5 = ttk.TTkButton(parent=root, pos=(5,6), text='Button 5 - unfocussed') -root = TTk(layout=TTkGridLayout()) +# Force the focus on the button 4 +btn4.setFocus() -TTkLogViewer(parent=root) - -TTkHelper._rootWidget._input.inputEvent.connect(keyCallback) -TTkHelper._rootWidget._input.pasteEvent.connect(pasteCallback) - -root.mainloop() +root.mainloop() \ No newline at end of file