From 1ee8f664520107788491383de4a3fba2a03e93c7 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 29 Sep 2023 18:56:55 +0100 Subject: [PATCH 01/52] Fix wrong layout used by the renewed treeWidget --- TermTk/TTkWidgets/TTkModelView/treewidgetitem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() From 2ffbb3e8e8e7c8f2779a527ff84e3de12e510e2d Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 29 Sep 2023 19:03:17 +0100 Subject: [PATCH 02/52] FIX Canvas error paint with transparent widget and removed unused references --- TermTk/TTkCore/canvas.py | 10 +++------- TermTk/TTkWidgets/widget.py | 3 --- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/TermTk/TTkCore/canvas.py b/TermTk/TTkCore/canvas.py index eab9dbfb..a82de402 100644 --- a/TermTk/TTkCore/canvas.py +++ b/TermTk/TTkCore/canvas.py @@ -36,21 +36,19 @@ 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 +61,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 +634,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/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") From 18fe572eda3c668bf14d5e634977898e292b9185 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 29 Sep 2023 19:03:40 +0100 Subject: [PATCH 03/52] Added few tests --- tests/timeit/21.weakref.01.py | 68 ++++++++++ tests/timeit/22.queue.01.py | 84 +++++++++++++ .../test.01.py} | 0 .../test.02.py} | 0 .../test.03.py} | 0 tests/weakref/test.04.gc.01.py | 116 ++++++++++++++++++ tests/weakref/test.04.gc.02.py | 88 +++++++++++++ tests/weakref/test.04.gc.03.py | 105 ++++++++++++++++ tests/weakref/test.05.TermTk.01.py | 92 ++++++++++++++ tests/weakref/test.05.TermTk.02.py | 105 ++++++++++++++++ tests/weakref/test.05.TermTk.03.signals.py | 95 ++++++++++++++ 11 files changed, 753 insertions(+) create mode 100755 tests/timeit/21.weakref.01.py create mode 100755 tests/timeit/22.queue.01.py rename tests/{test.generic.006.weakref.01.py => weakref/test.01.py} (100%) rename tests/{test.generic.006.weakref.02.py => weakref/test.02.py} (100%) rename tests/{test.generic.006.weakref.03.py => weakref/test.03.py} (100%) create mode 100755 tests/weakref/test.04.gc.01.py create mode 100755 tests/weakref/test.04.gc.02.py create mode 100755 tests/weakref/test.04.gc.03.py create mode 100755 tests/weakref/test.05.TermTk.01.py create mode 100755 tests/weakref/test.05.TermTk.02.py create mode 100755 tests/weakref/test.05.TermTk.03.signals.py 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..775c20ae --- /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()=}") From 9a4862fa229d70b2f6e04920115c89d6ade68e33 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 29 Sep 2023 19:15:28 +0100 Subject: [PATCH 04/52] Fix test error --- tests/weakref/test.05.TermTk.01.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/weakref/test.05.TermTk.01.py b/tests/weakref/test.05.TermTk.01.py index 775c20ae..e924dd3f 100755 --- a/tests/weakref/test.05.TermTk.01.py +++ b/tests/weakref/test.05.TermTk.01.py @@ -56,7 +56,7 @@ def _ref(o): print("\n############# Phase 1 ##################") # wid = ttk.TTkWidget() -# wid = ttk.TTkButton() +wid = ttk.TTkButton() # wid = ttk.TTkLabel() # wid = ttk.TTkGraph() # wid = ttk.TTkSpacer() From 71e9ad132cb466e6089cbb8213303da1051282be Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Tue, 3 Oct 2023 13:52:42 +0100 Subject: [PATCH 05/52] Terminal update, added select variable --- TermTk/TTkWidgets/TTkTerminal/terminal.py | 2 +- .../TTkWidgets/TTkTerminal/terminal_screen.py | 3 +- TermTk/TTkWidgets/TTkTerminal/terminalview.py | 41 +++++++-- tests/test.pty.006.terminal.04.py | 89 +++++++++++++++++++ tests/test.pty.006.terminal.05.py | 73 +++++++++++++++ 5 files changed, 199 insertions(+), 9 deletions(-) create mode 100755 tests/test.pty.006.terminal.04.py create mode 100755 tests/test.pty.006.terminal.05.py 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..452aef77 100644 --- a/TermTk/TTkWidgets/TTkTerminal/terminal_screen.py +++ b/TermTk/TTkWidgets/TTkTerminal/terminal_screen.py @@ -198,10 +198,11 @@ class _TTkTerminalScreen(_TTkTerminalScreen_CSI, _TTkTerminalScreen_C1): self._terminalCursor = (x,y) self._pushTxt(lll,irm) - def paintEvent(self, canvas: TTkCanvas, w:int, h:int, ox:int=0, oy:int=0) -> None: + def paintEvent(self, canvas: TTkCanvas, w:int, h:int, ox:int=0, oy:int=0, select:list=None) -> None: w,h = self._w, self._h ll = len(self._bufferedLines) for y in range(ll-oy): canvas.drawTTkString(pos=(0,y),text=self._bufferedLines[oy+y]) s = (-ox,ll-oy,w,h) canvas.paintCanvas(self._canvas,s,s,s) + canvas.drawText(pos=(0,0),text=f"({select})") diff --git a/TermTk/TTkWidgets/TTkTerminal/terminalview.py b/TermTk/TTkWidgets/TTkTerminal/terminalview.py index f676e378..f517a1e5 100644 --- a/TermTk/TTkWidgets/TTkTerminal/terminalview.py +++ b/TermTk/TTkWidgets/TTkTerminal/terminalview.py @@ -89,7 +89,8 @@ 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', @@ -97,10 +98,10 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): '_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') @@ -110,6 +111,7 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): self._mode_normal = True self._quit_pipe = None self._resize_pipe = None + self._select = None self._terminal = TTkTerminalView._Terminal() self._keyboard = TTkTerminalView._Keyboard() self._mouse = TTkTerminalView._Mouse() @@ -252,10 +254,15 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): 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]) 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 +281,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,9 +959,29 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): self._inout.write(bah) return True - def mousePressEvent(self, evt): return self._sendMouse(evt) | True + def mousePressEvent(self, evt): + if self._mouse.reportPress: + self._select = None + return self._sendMouse(evt) | True + x,y = evt.x,evt.y + ox,oy = self.getViewOffsets() + self._select = [(x+ox,y+oy)] + self.update() + return True + + def mouseDragEvent(self, evt): + if self._mouse.reportPress: + self._select = None + return self._sendMouse(evt) + if not self._select: + return True + x,y = evt.x,evt.y + ox,oy = self.getViewOffsets() + self._select[1:] = [(x+ox,y+oy)] + self.update() + return 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 mouseTapEvent(self, evt): return self._sendMouse(evt) def mouseDoubleClickEvent(self, evt): return self._sendMouse(evt) @@ -966,7 +993,7 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): def paintEvent(self, canvas: TTkCanvas): w,h = self.size() ox,oy = self.getViewOffsets() - self._screen_current.paintEvent(canvas,w,h,ox,oy) + self._screen_current.paintEvent(canvas,w,h,ox,oy,self._select) 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 From deae7ceee419a0d1df9006ad41c3ab203176d357 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Tue, 3 Oct 2023 14:08:33 +0100 Subject: [PATCH 06/52] Fix: #176 - Float formatting not allowed as number in the LineEdit --- TermTk/TTkWidgets/lineedit.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/TermTk/TTkWidgets/lineedit.py b/TermTk/TTkWidgets/lineedit.py index 2d3c22c9..d916e7c1 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) @@ -181,6 +182,15 @@ class TTkLineEdit(TTkWidget): self.update() return True + @staticmethod + def _isFloat(num): + try: + float(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()) @@ -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) From be196d0bc053471af6120a3ee491d78dcfc82fa4 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Tue, 3 Oct 2023 14:33:21 +0100 Subject: [PATCH 07/52] updated ttkDesigner version --- setup.ttkDesigner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.ttkDesigner.py b/setup.ttkDesigner.py index 7ae3f8f4..7056376f 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.34.0a', 'pyperclip', 'Pillow'], entry_points={ From 63195e0cba00d34d8b1de1b1f6f2821ad1f1b21d Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 6 Oct 2023 09:01:54 +0100 Subject: [PATCH 08/52] fix terminal buffer lines composition --- .../TTkWidgets/TTkTerminal/terminal_screen.py | 10 ++++----- .../TTkTerminal/terminal_screen_CSI.py | 10 ++++++--- TermTk/TTkWidgets/TTkTerminal/terminalview.py | 22 ++++++++++++------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/TermTk/TTkWidgets/TTkTerminal/terminal_screen.py b/TermTk/TTkWidgets/TTkTerminal/terminal_screen.py index 452aef77..a107f8eb 100644 --- a/TermTk/TTkWidgets/TTkTerminal/terminal_screen.py +++ b/TermTk/TTkWidgets/TTkTerminal/terminal_screen.py @@ -61,7 +61,7 @@ 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 @@ -83,8 +83,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 @@ -100,7 +100,7 @@ class _TTkTerminalScreen(_TTkTerminalScreen_CSI, _TTkTerminalScreen_C1): 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 +128,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] 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 f517a1e5..ce8cc438 100644 --- a/TermTk/TTkWidgets/TTkTerminal/terminalview.py +++ b/TermTk/TTkWidgets/TTkTerminal/terminalview.py @@ -177,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) @@ -195,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) @@ -247,17 +247,23 @@ 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[0]) os.close(self._quit_pipe[1]) - os.close(self._resize_pipe[0]) + # os.close(self._resize_pipe[0]) os.close(self._resize_pipe[1]) + os.close(self._fd) return if self._resize_pipe[0] in rs: From 3925c5ca6dbaa6c06dd2392a0682f631fce98de8 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 6 Oct 2023 09:25:04 +0100 Subject: [PATCH 09/52] Avoid tab focus if widget is disabled --- TermTk/TTkCore/helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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() From 13b44a36891cab5e039f488c35471322cf0e5931 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 6 Oct 2023 09:25:40 +0100 Subject: [PATCH 10/52] Fixed keyboard action on checkable buttons --- TermTk/TTkWidgets/button.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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'] From 5a96919c3b4e0c0aa2af4b3ed72ccb770aff97bf Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 6 Oct 2023 09:27:38 +0100 Subject: [PATCH 11/52] Enhancement: #178 Allow up/down focus switch in the LineEdit --- TermTk/TTkWidgets/lineedit.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/TermTk/TTkWidgets/lineedit.py b/TermTk/TTkWidgets/lineedit.py index d916e7c1..5d039a36 100644 --- a/TermTk/TTkWidgets/lineedit.py +++ b/TermTk/TTkWidgets/lineedit.py @@ -185,7 +185,7 @@ class TTkLineEdit(TTkWidget): @staticmethod def _isFloat(num): try: - float(num) + float(str(num)) return True except: return False @@ -220,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) From 92054bb753f5a27652df5454546f0c3a433266e1 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 6 Oct 2023 21:54:20 +0100 Subject: [PATCH 12/52] FIX: #180 Crash when removed item from the ListWidget --- TermTk/TTkWidgets/listwidget.py | 7 +- ....ui.014.list.py => test.ui.014.list.01.py} | 0 tests/test.ui.014.list.03.py | 105 ++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) rename tests/{test.ui.014.list.py => test.ui.014.list.01.py} (100%) create mode 100755 tests/test.ui.014.list.03.py diff --git a/TermTk/TTkWidgets/listwidget.py b/TermTk/TTkWidgets/listwidget.py index 05de8644..097e8acd 100644 --- a/TermTk/TTkWidgets/listwidget.py +++ b/TermTk/TTkWidgets/listwidget.py @@ -226,10 +226,15 @@ class TTkListWidget(TTkAbstractScrollView): def removeItem(self, item): '''removeItem''' - self.removeWidget(item) + item.listItemClicked.disconnect(self._labelSelectedHandler) + item._setSelected(False) + item._setHighlighted(False) + self.layout().removeWidget(item) 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): 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..619dfb4f --- /dev/null +++ b/tests/test.ui.014.list.03.py @@ -0,0 +1,105 @@ +#!/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, argparse, math, random + +sys.path.append(os.path.join(sys.path[0],'..')) +import TermTk as ttk + +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(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(): + for i in listWidgetMulti.selectedItems().copy(): + listWidgetMulti.removeItem(i) + for i in listWidgetSingle.selectedItems().copy(): + listWidgetSingle.removeItem(i) + + +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"{i}) {getWord()} {getWord()}") + listWidgetMulti.addItem(f"{getWord()} {getWord()}") + +root.mainloop() From 5750a7bd6efdf82f05e6b39b26f19fa25ea19641 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Sun, 8 Oct 2023 00:10:58 +0100 Subject: [PATCH 13/52] Q/A: #185 Add simple focus example --- tutorial/examples/TTkWidget/Focus.01.py | 42 +++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tutorial/examples/TTkWidget/Focus.01.py diff --git a/tutorial/examples/TTkWidget/Focus.01.py b/tutorial/examples/TTkWidget/Focus.01.py new file mode 100644 index 00000000..fbc3a719 --- /dev/null +++ b/tutorial/examples/TTkWidget/Focus.01.py @@ -0,0 +1,42 @@ +#!/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. + +# Those 2 lines are required to use the TermTk library straight from the main folder +import sys, os +sys.path.append(os.path.join(sys.path[0],'../../..')) + +import TermTk as ttk + +root=ttk.TTk() + +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') + +# Force the focus on the button 4 +btn4.setFocus() + +root.mainloop() \ No newline at end of file From 6eedf773c73ee082e728e49bc76a1e87a27fc911 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Sun, 8 Oct 2023 00:17:39 +0100 Subject: [PATCH 14/52] Initial implementation of the DragDrop in the list widget --- TermTk/TTkWidgets/kodetab.py | 4 +-- TermTk/TTkWidgets/list_.py | 3 +- TermTk/TTkWidgets/listwidget.py | 62 +++++++++++++++++++++++++++------ tests/test.ui.014.list.03.py | 12 +++---- 4 files changed, 62 insertions(+), 19 deletions(-) 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/list_.py b/TermTk/TTkWidgets/list_.py index 52850f2a..6c68eb98 100644 --- a/TermTk/TTkWidgets/list_.py +++ b/TermTk/TTkWidgets/list_.py @@ -31,7 +31,7 @@ class TTkList(TTkAbstractScrollArea): '_listView', 'itemClicked', 'textClicked', # Forwarded Methods 'items', 'addItem', 'addItemAt', 'indexOf', 'itemAt', - 'moveItem', 'removeAt', 'removeItem', + 'moveItem', 'removeAt', 'removeItem', 'removeItems', 'setSelectionMode', 'selectedItems', 'selectedLabels', 'setCurrentRow', 'setCurrentItem', ) @@ -51,6 +51,7 @@ 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.addItemAt = self._listView.addItemAt self.setSelectionMode = self._listView.setSelectionMode diff --git a/TermTk/TTkWidgets/listwidget.py b/TermTk/TTkWidgets/listwidget.py index 097e8acd..b79644d7 100644 --- a/TermTk/TTkWidgets/listwidget.py +++ b/TermTk/TTkWidgets/listwidget.py @@ -27,9 +27,10 @@ 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): @@ -193,6 +194,7 @@ 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''' @@ -226,15 +228,20 @@ class TTkListWidget(TTkAbstractScrollView): def removeItem(self, item): '''removeItem''' - item.listItemClicked.disconnect(self._labelSelectedHandler) - item._setSelected(False) - item._setHighlighted(False) - self.layout().removeWidget(item) - self._items.remove(item) - if item in self._selectedItems: - self._selectedItems.remove(item) - if item == self._highlighted: - self._highlighted = None + 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): @@ -261,6 +268,41 @@ class TTkListWidget(TTkAbstractScrollView): elif index <= offy: self.viewMoveTo(offx, index) + def mouseDragEvent(self, evt) -> bool: + TTkLog.debug("Start DnD") + if not (items:=self._selectedItems.copy()): + return True + drag = TTkDrag() + data = (self,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() + pm.drawText(pos=(1,y), text=it.text()) + if txt.termWidth() >= 20: + 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 dropEvent(self, evt) -> bool: + TTkLog.debug(f"Drop pos={evt.pos()}") + offx,offy = self.getViewOffsets() + wid,items = evt.data() + # check the correct wid type + if wid and items: + wid.removeItems(items) + for it in reversed(items): + it.setCurrentStyle(it.style()['default']) + self.addItemAt(it,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 \ diff --git a/tests/test.ui.014.list.03.py b/tests/test.ui.014.list.03.py index 619dfb4f..8200919d 100755 --- a/tests/test.ui.014.list.03.py +++ b/tests/test.ui.014.list.03.py @@ -82,10 +82,10 @@ def _moveToLeft1(): @ttk.pyTTkSlot() def _delSelected(): - for i in listWidgetMulti.selectedItems().copy(): - listWidgetMulti.removeItem(i) - for i in listWidgetSingle.selectedItems().copy(): - listWidgetSingle.removeItem(i) + items = listWidgetMulti.selectedItems() + listWidgetMulti.removeItems(items) + items = listWidgetSingle.selectedItems() + listWidgetSingle.removeItems(items) btn_mv1.clicked.connect(_moveToRight2) @@ -99,7 +99,7 @@ listWidgetMulti.textClicked.connect(_listCallback2) # populate the lists with random entries for i in range(10): - listWidgetSingle.addItem(f"{i}) {getWord()} {getWord()}") - listWidgetMulti.addItem(f"{getWord()} {getWord()}") + listWidgetSingle.addItem(f"S-{i}) {getWord()} {getWord()}") + listWidgetMulti.addItem(f"M-{i}) {getWord()} {getWord()}") root.mainloop() From 873755e2154caeb2d970200b6b201b3c0fe8d9f0 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Sun, 8 Oct 2023 16:17:10 +0100 Subject: [PATCH 15/52] Added drop marker in the list widget --- TermTk/TTkWidgets/listwidget.py | 37 ++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/TermTk/TTkWidgets/listwidget.py b/TermTk/TTkWidgets/listwidget.py index b79644d7..9713a74b 100644 --- a/TermTk/TTkWidgets/listwidget.py +++ b/TermTk/TTkWidgets/listwidget.py @@ -107,13 +107,17 @@ class TTkAbstractListItem(TTkWidget): class TTkListWidget(TTkAbstractScrollView): '''TTkListWidget''' - __slots__ = ('itemClicked', 'textClicked', '_selectedItems', '_selectionMode', '_highlighted', '_items') + __slots__ = ('itemClicked', 'textClicked', + '_selectedItems', '_selectionMode', + '_highlighted', '_items', + '_dragPos') 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 # Signals self.itemClicked = pyTTkSignal(TTkWidget) self.textClicked = pyTTkSignal(str) @@ -290,8 +294,24 @@ class TTkListWidget(TTkAbstractScrollView): drag.exec() return True + def dragEnterEvent(self, evt): + return self.dragMoveEvent(evt) + + 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: TTkLog.debug(f"Drop pos={evt.pos()}") + self._dragPos = None offx,offy = self.getViewOffsets() wid,items = evt.data() # check the correct wid type @@ -351,3 +371,18 @@ 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")) + + From eea8b00ab536c92f65a01e4edc448fb9f9e1a4dd Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Sun, 8 Oct 2023 16:57:19 +0100 Subject: [PATCH 16/52] Added dataclass in the listWidget drop --- TermTk/TTkWidgets/listwidget.py | 20 ++++++++++++++++---- tests/test.ui.014.list.03.py | 13 ++++++++++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/TermTk/TTkWidgets/listwidget.py b/TermTk/TTkWidgets/listwidget.py index 9713a74b..76e22c34 100644 --- a/TermTk/TTkWidgets/listwidget.py +++ b/TermTk/TTkWidgets/listwidget.py @@ -22,6 +22,8 @@ __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 @@ -106,6 +108,11 @@ class TTkAbstractListItem(TTkWidget): canvas.drawTTkString(pos=(0,0), width=w, color=style['color'] ,text=self._text) class TTkListWidget(TTkAbstractScrollView): + @dataclass(frozen=True) + class _DropListData: + widget: TTkAbstractScrollView + items: list + '''TTkListWidget''' __slots__ = ('itemClicked', 'textClicked', '_selectedItems', '_selectionMode', @@ -277,7 +284,7 @@ class TTkListWidget(TTkAbstractScrollView): if not (items:=self._selectedItems.copy()): return True drag = TTkDrag() - data = (self,items) + 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) @@ -295,7 +302,9 @@ class TTkListWidget(TTkAbstractScrollView): return True def dragEnterEvent(self, evt): - return self.dragMoveEvent(evt) + if issubclass(type(evt.data()),TTkListWidget._DropListData): + return self.dragMoveEvent(evt) + return False def dragMoveEvent(self, evt): offx,offy = self.getViewOffsets() @@ -312,9 +321,11 @@ class TTkListWidget(TTkAbstractScrollView): def dropEvent(self, evt) -> bool: TTkLog.debug(f"Drop pos={evt.pos()}") self._dragPos = None + if not issubclass(type(evt.data()) ,TTkListWidget._DropListData): + return False offx,offy = self.getViewOffsets() - wid,items = evt.data() - # check the correct wid type + wid = evt.data().widget + items = evt.data().items if wid and items: wid.removeItems(items) for it in reversed(items): @@ -386,3 +397,4 @@ class TTkListWidget(TTkAbstractScrollView): canvas.drawText(pos=p2,text="╓─╼", color=TTkColor.fg("#FFFF00")+TTkColor.bg("#008855")) + diff --git a/tests/test.ui.014.list.03.py b/tests/test.ui.014.list.03.py index 8200919d..9e7505f3 100755 --- a/tests/test.ui.014.list.03.py +++ b/tests/test.ui.014.list.03.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 @@ -27,9 +27,16 @@ import sys, os, argparse, math, random sys.path.append(os.path.join(sys.path[0],'..')) import TermTk as ttk -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."] +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(words) + return random.choice(utfwords) + # return random.choice(words) parser = argparse.ArgumentParser() parser.add_argument('-t', help='Track Mouse', action='store_true') From 54d4b10da68e08bc4a566b87db67118cf275eb20 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Mon, 9 Oct 2023 08:59:19 +0100 Subject: [PATCH 17/52] Tuned list widget colors to make them more readable --- TermTk/TTkWidgets/listwidget.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/TermTk/TTkWidgets/listwidget.py b/TermTk/TTkWidgets/listwidget.py index 76e22c34..d489e0a6 100644 --- a/TermTk/TTkWidgets/listwidget.py +++ b/TermTk/TTkWidgets/listwidget.py @@ -40,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')}, } @@ -95,17 +95,17 @@ 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) From 4bd41a2c0fea2da1098587f7441c35bcc60dc418 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Mon, 9 Oct 2023 09:23:22 +0100 Subject: [PATCH 18/52] Improved ListWidget with service functions --- TermTk/TTkWidgets/list_.py | 8 ++++++-- TermTk/TTkWidgets/listwidget.py | 33 +++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/TermTk/TTkWidgets/list_.py b/TermTk/TTkWidgets/list_.py index 6c68eb98..b2d9e6af 100644 --- a/TermTk/TTkWidgets/list_.py +++ b/TermTk/TTkWidgets/list_.py @@ -30,8 +30,10 @@ class TTkList(TTkAbstractScrollArea): __slots__ = ( '_listView', 'itemClicked', 'textClicked', # Forwarded Methods - 'items', 'addItem', 'addItemAt', 'indexOf', 'itemAt', - 'moveItem', 'removeAt', 'removeItem', 'removeItems', + 'items', + 'addItem', 'addItemAt', 'addItems', 'addItemsAt', + 'indexOf', 'itemAt', 'moveItem', + 'removeAt', 'removeItem', 'removeItems', 'setSelectionMode', 'selectedItems', 'selectedLabels', 'setCurrentRow', 'setCurrentItem', ) @@ -53,7 +55,9 @@ class TTkList(TTkAbstractScrollArea): 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.setSelectionMode = self._listView.setSelectionMode self.selectedItems = self._listView.selectedItems self.selectedLabels = self._listView.selectedLabels diff --git a/TermTk/TTkWidgets/listwidget.py b/TermTk/TTkWidgets/listwidget.py index d489e0a6..bab2f486 100644 --- a/TermTk/TTkWidgets/listwidget.py +++ b/TermTk/TTkWidgets/listwidget.py @@ -198,6 +198,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: @@ -210,12 +214,19 @@ class TTkListWidget(TTkAbstractScrollView): 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): @@ -290,8 +301,10 @@ class TTkListWidget(TTkAbstractScrollView): pm = TTkCanvas(width=w,height=h) for y,it in enumerate(items[:3],1): txt = it.text() - pm.drawText(pos=(1,y), text=it.text()) - if txt.termWidth() >= 20: + 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='...') @@ -328,9 +341,9 @@ class TTkListWidget(TTkAbstractScrollView): items = evt.data().items if wid and items: wid.removeItems(items) - for it in reversed(items): + for it in items: it.setCurrentStyle(it.style()['default']) - self.addItemAt(it,offy+evt.y) + self.addItemsAt(items,offy+evt.y) return True return False From 56ecf087f2e7353619df07a5196a5990a9503460 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Mon, 9 Oct 2023 18:39:13 +0100 Subject: [PATCH 19/52] Added dragDropMode in the list widget --- TermTk/TTkCore/canvas.py | 1 - TermTk/TTkCore/constant.py | 12 ++++ TermTk/TTkWidgets/listwidget.py | 20 +++++- demo/showcase/list.py | 78 +++++++++++++++++----- tests/test.ui.014.list.04.py | 112 ++++++++++++++++++++++++++++++++ 5 files changed, 203 insertions(+), 20 deletions(-) create mode 100755 tests/test.ui.014.list.04.py diff --git a/TermTk/TTkCore/canvas.py b/TermTk/TTkCore/canvas.py index a82de402..7ffc0543 100644 --- a/TermTk/TTkCore/canvas.py +++ b/TermTk/TTkCore/canvas.py @@ -37,7 +37,6 @@ class TTkCanvas: ''' __slots__ = ( '_width', '_height', '_newWidth', '_newHeight', - '_theme', '_data', '_colors', '_bufferedData', '_bufferedColors', '_visible', '_transparent', '_doubleBuffer') diff --git a/TermTk/TTkCore/constant.py b/TermTk/TTkCore/constant.py index efd19d96..833200e4 100644 --- a/TermTk/TTkCore/constant.py +++ b/TermTk/TTkCore/constant.py @@ -128,6 +128,18 @@ 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''' + NoDragDrop = DragDropMode.NoDragDrop + AllowDrag = DragDropMode.AllowDrag + AllowDrop = DragDropMode.AllowDrop + 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/TTkWidgets/listwidget.py b/TermTk/TTkWidgets/listwidget.py index bab2f486..74025c89 100644 --- a/TermTk/TTkWidgets/listwidget.py +++ b/TermTk/TTkWidgets/listwidget.py @@ -117,7 +117,7 @@ class TTkListWidget(TTkAbstractScrollView): __slots__ = ('itemClicked', 'textClicked', '_selectedItems', '_selectionMode', '_highlighted', '_items', - '_dragPos') + '_dragPos', '_dndMode') def __init__(self, *args, **kwargs): # Default Class Specific Values self._selectionMode = kwargs.get("selectionMode", TTkK.SingleSelection) @@ -125,6 +125,8 @@ class TTkListWidget(TTkAbstractScrollView): self._items = [] self._highlighted = None self._dragPos = None + self._dndMode = kwargs.get("dragDropMode", + TTkK.DragDropMode.AllowDrag | TTkK.DragDropMode.AllowDrop ) # Signals self.itemClicked = pyTTkSignal(TTkWidget) self.textClicked = pyTTkSignal(str) @@ -161,6 +163,14 @@ 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 setSelectionMode(self, mode): '''setSelectionMode''' self._selectionMode = mode @@ -291,7 +301,8 @@ class TTkListWidget(TTkAbstractScrollView): self.viewMoveTo(offx, index) def mouseDragEvent(self, evt) -> bool: - TTkLog.debug("Start DnD") + if not(self._dndMode & TTkK.DragDropMode.AllowDrag): + return False if not (items:=self._selectedItems.copy()): return True drag = TTkDrag() @@ -315,6 +326,8 @@ class TTkListWidget(TTkAbstractScrollView): 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 @@ -332,7 +345,8 @@ class TTkListWidget(TTkAbstractScrollView): return True def dropEvent(self, evt) -> bool: - TTkLog.debug(f"Drop pos={evt.pos()}") + if not(self._dndMode & TTkK.DragDropMode.AllowDrop): + return False self._dragPos = None if not issubclass(type(evt.data()) ,TTkListWidget._DropListData): return False diff --git a/demo/showcase/list.py b/demo/showcase/list.py index c72b060d..f18a76a7 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, 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) + rootLayout.addWidget(win2,0,1) + rootLayout.addWidget(win3,0,2,1,3) + rootLayout.addItem(layout1,1,0,1,3) + rootLayout.addWidget(win4,1,3) + rootLayout.addWidget(win5,1,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) + + # Multi Selection List + listWidgetMulti = ttk.TTkList(parent=win2, maxWidth=40, minWidth=10, selectionMode=ttk.TTkK.MultiSelection) + + # Multi Selection List - Drag Allowed + listWidgetDrag = ttk.TTkList(parent=win4, maxWidth=40, minWidth=10, dragDropMode=ttk.TTkK.DragDropMode.AllowDrag) + listWidgetDrop = ttk.TTkList(parent=win5, maxWidth=40, minWidth=10, dragDropMode=ttk.TTkK.DragDropMode.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/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() From 663cb032fd33ffd889c2ce1ce3b5731b0b109e3c Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Tue, 10 Oct 2023 11:31:04 +0100 Subject: [PATCH 20/52] Added list support in TTkDesigner + other stuff --- TermTk/TTkUiTools/properties/list_.py | 46 ++++++++++++++++++- TermTk/TTkWidgets/list_.py | 6 ++- TermTk/TTkWidgets/listwidget.py | 6 ++- demo/showcase/list.py | 2 +- ttkDesigner/app/propertyeditor.py | 2 +- ttkDesigner/app/superobj/__init__.py | 4 +- ttkDesigner/app/superobj/superwidget.py | 5 +- .../superobj/superwidgetabstractscrollarea.py | 44 ++++++++++++++++++ ttkDesigner/app/superobj/superwidgetlist.py | 29 ++++++++++++ ttkDesigner/app/widgetbox.py | 3 +- 10 files changed, 139 insertions(+), 8 deletions(-) create mode 100644 ttkDesigner/app/superobj/superwidgetabstractscrollarea.py create mode 100644 ttkDesigner/app/superobj/superwidgetlist.py 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/TTkWidgets/list_.py b/TermTk/TTkWidgets/list_.py index b2d9e6af..b6450e30 100644 --- a/TermTk/TTkWidgets/list_.py +++ b/TermTk/TTkWidgets/list_.py @@ -31,10 +31,11 @@ class TTkList(TTkAbstractScrollArea): '_listView', 'itemClicked', 'textClicked', # Forwarded Methods 'items', + 'dragDropMode', 'setDragDropMode', 'addItem', 'addItemAt', 'addItems', 'addItemsAt', 'indexOf', 'itemAt', 'moveItem', 'removeAt', 'removeItem', 'removeItems', - 'setSelectionMode', 'selectedItems', 'selectedLabels', + 'selectionMode', 'setSelectionMode', 'selectedItems', 'selectedLabels', 'setCurrentRow', 'setCurrentItem', ) def __init__(self, *args, **kwargs): @@ -58,9 +59,12 @@ class TTkList(TTkAbstractScrollArea): 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 74025c89..4e54deb4 100644 --- a/TermTk/TTkWidgets/listwidget.py +++ b/TermTk/TTkWidgets/listwidget.py @@ -128,7 +128,7 @@ class TTkListWidget(TTkAbstractScrollView): self._dndMode = kwargs.get("dragDropMode", TTkK.DragDropMode.AllowDrag | TTkK.DragDropMode.AllowDrop ) # Signals - self.itemClicked = pyTTkSignal(TTkWidget) + self.itemClicked = pyTTkSignal(TTkAbstractListItem) self.textClicked = pyTTkSignal(str) # Init Super TTkAbstractScrollView.__init__(self, *args, **kwargs) @@ -171,6 +171,10 @@ class TTkListWidget(TTkAbstractScrollView): '''setDragDropMode''' self._dndMode = dndMode + def selectionMode(self): + '''selectionMode''' + return self._selectionMode + def setSelectionMode(self, mode): '''setSelectionMode''' self._selectionMode = mode diff --git a/demo/showcase/list.py b/demo/showcase/list.py index f18a76a7..7a703170 100755 --- a/demo/showcase/list.py +++ b/demo/showcase/list.py @@ -33,7 +33,7 @@ from showcase._showcasehelper import getUtfWord def demoList(root= None): # Define the main Layout - retFrame = ttk.TTkFrame(parent=root, layout=(rootLayout:=ttk.TTkGridLayout())) + retFrame = ttk.TTkFrame(parent=root, border=False, layout=(rootLayout:=ttk.TTkGridLayout())) # Define the main Layout win1 = ttk.TTkWindow(title="Single List", layout=ttk.TTkVBoxLayout()) 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}, From 14536dde1850c1dbabe507127d75819fae676a53 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Tue, 10 Oct 2023 11:36:30 +0100 Subject: [PATCH 21/52] changed demo list --- demo/showcase/list.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/demo/showcase/list.py b/demo/showcase/list.py index 7a703170..1015196a 100755 --- a/demo/showcase/list.py +++ b/demo/showcase/list.py @@ -44,12 +44,12 @@ def demoList(root= None): layout1 = ttk.TTkLayout() # Place the widgets in the root layout - rootLayout.addWidget(win1,0,0) - rootLayout.addWidget(win2,0,1) - rootLayout.addWidget(win3,0,2,1,3) - rootLayout.addItem(layout1,1,0,1,3) - rootLayout.addWidget(win4,1,3) - rootLayout.addWidget(win5,1,4) + 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 listWidgetSingle = ttk.TTkList(parent=win1, maxWidth=40, minWidth=10) From bbcb55b3d3322fd4e8f8daa655a26d3c467605b6 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Tue, 10 Oct 2023 11:46:34 +0100 Subject: [PATCH 22/52] Updated requirement version for ttkDesigner --- setup.ttkDesigner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.ttkDesigner.py b/setup.ttkDesigner.py index 7056376f..b192a8cd 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.34.0a', + 'pyTermTk>=0.35.0a', 'pyperclip', 'Pillow'], entry_points={ From d2ae7d1964b370569046bbb8fa5efa9723b15681 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 18 Oct 2023 09:19:08 +0100 Subject: [PATCH 23/52] Added basic Select/Copy in the TTkTerminal --- TermTk/TTkCore/string.py | 11 +- .../TTkWidgets/TTkTerminal/terminal_screen.py | 114 +++++++++++++++++- TermTk/TTkWidgets/TTkTerminal/terminalview.py | 36 ++++-- 3 files changed, 141 insertions(+), 20 deletions(-) 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/TTkWidgets/TTkTerminal/terminal_screen.py b/TermTk/TTkWidgets/TTkTerminal/terminal_screen.py index a107f8eb..f5aa3560 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', @@ -68,6 +111,7 @@ class _TTkTerminalScreen(_TTkTerminalScreen_CSI, _TTkTerminalScreen_C1): 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) @@ -94,6 +138,7 @@ 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) @@ -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,11 +245,70 @@ class _TTkTerminalScreen(_TTkTerminalScreen_CSI, _TTkTerminalScreen_C1): self._terminalCursor = (x,y) self._pushTxt(lll,irm) - def paintEvent(self, canvas: TTkCanvas, w:int, h:int, ox:int=0, oy:int=0, select:list=None) -> None: + 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): + 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"({select})") + # 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/terminalview.py b/TermTk/TTkWidgets/TTkTerminal/terminalview.py index ce8cc438..036888f8 100644 --- a/TermTk/TTkWidgets/TTkTerminal/terminalview.py +++ b/TermTk/TTkWidgets/TTkTerminal/terminalview.py @@ -93,7 +93,7 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): '_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', @@ -111,7 +111,6 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): self._mode_normal = True self._quit_pipe = None self._resize_pipe = None - self._select = None self._terminal = TTkTerminalView._Terminal() self._keyboard = TTkTerminalView._Keyboard() self._mouse = TTkTerminalView._Mouse() @@ -121,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!!! 🔔🔔🔔")) @@ -967,28 +967,42 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): def mousePressEvent(self, evt): if self._mouse.reportPress: - self._select = None + 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._select = [(x+ox,y+oy)] + self._screen_current.select(x+ox,y+oy) self.update() return True def mouseDragEvent(self, evt): if self._mouse.reportPress: - self._select = None + self._screen_current.select(0,0) return self._sendMouse(evt) - if not self._select: - return True x,y = evt.x,evt.y ox,oy = self.getViewOffsets() - self._select[1:] = [(x+ox,y+oy)] + self._screen_current.select(x+ox,y+oy,moveAnchor=False) self.update() return True - def mouseReleaseEvent(self, evt): return self._sendMouse(evt) - def wheelEvent(self, evt): return True if self._sendMouse(evt) else super().wheelEvent(evt) + def mouseReleaseEvent(self, evt): + self._selecting = False + 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): @@ -999,7 +1013,7 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): def paintEvent(self, canvas: TTkCanvas): w,h = self.size() ox,oy = self.getViewOffsets() - self._screen_current.paintEvent(canvas,w,h,ox,oy,self._select) + self._screen_current.paintEvent(canvas,w,h,ox,oy) From 3e8a55b727343e6ec983ff5845bf1fd129ec8ecf Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Thu, 19 Oct 2023 14:42:51 +0100 Subject: [PATCH 24/52] Do not push to clipboard if the Terminal selection is empty --- TermTk/TTkWidgets/TTkTerminal/terminal_screen.py | 2 ++ TermTk/TTkWidgets/TTkTerminal/terminalview.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/TermTk/TTkWidgets/TTkTerminal/terminal_screen.py b/TermTk/TTkWidgets/TTkTerminal/terminal_screen.py index f5aa3560..db956a38 100644 --- a/TermTk/TTkWidgets/TTkTerminal/terminal_screen.py +++ b/TermTk/TTkWidgets/TTkTerminal/terminal_screen.py @@ -252,6 +252,8 @@ class _TTkTerminalScreen(_TTkTerminalScreen_CSI, _TTkTerminalScreen_C1): self._selectCursor.select(x,y,moveAnchor) def getSelected(self): + if not self._selectCursor.hasSelection(): + return "" ret = [] st = self._selectCursor.selectionStart() diff --git a/TermTk/TTkWidgets/TTkTerminal/terminalview.py b/TermTk/TTkWidgets/TTkTerminal/terminalview.py index 036888f8..c07b1a28 100644 --- a/TermTk/TTkWidgets/TTkTerminal/terminalview.py +++ b/TermTk/TTkWidgets/TTkTerminal/terminalview.py @@ -988,8 +988,8 @@ class TTkTerminalView(TTkAbstractScrollView, _TTkTerminal_CSI_DEC): def mouseReleaseEvent(self, evt): self._selecting = False - selected = self._screen_current.getSelected() - self._clipboard.setText(selected) + if (selected := self._screen_current.getSelected()): + self._clipboard.setText(selected) return self._sendMouse(evt) def wheelEvent(self, evt): From 711efa3e1c4cf4bad4c0eee70c83c5088fc38a86 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Thu, 19 Oct 2023 16:50:29 +0100 Subject: [PATCH 25/52] Updated the notes for the MS-Windows Support --- docs/MDNotes/Resources.md | 12 ------------ docs/MDNotes/msWindows/Resources.md | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 docs/MDNotes/msWindows/Resources.md 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/Resources.md b/docs/MDNotes/msWindows/Resources.md new file mode 100644 index 00000000..a127c6a7 --- /dev/null +++ b/docs/MDNotes/msWindows/Resources.md @@ -0,0 +1,17 @@ +# 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 +``` + +# Competitors with MS-Win support + +## Textual -> https://github.com/Textualize/textual + +## TheVTPyProject -> https://github.com/srccircumflex/TheVTPyProject \ No newline at end of file From b60534ee4ec3a71581d2e41ddb97ed6c0dc3f4dd Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 20 Oct 2023 12:13:22 +0100 Subject: [PATCH 26/52] defining the base structure for the windows implementation --- TermTk/TTkCore/TTkTerm/input.py | 2 +- TermTk/TTkCore/TTkTerm/readinputwindows.py | 36 +++++++++++ TermTk/TTkCore/TTkTerm/term.py | 7 ++- TermTk/TTkCore/TTkTerm/term_base.py | 2 +- TermTk/TTkCore/TTkTerm/term_windows.py | 73 ++++++++++++++++++++++ 5 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 TermTk/TTkCore/TTkTerm/readinputwindows.py create mode 100644 TermTk/TTkCore/TTkTerm/term_windows.py diff --git a/TermTk/TTkCore/TTkTerm/input.py b/TermTk/TTkCore/TTkTerm/input.py index e8afcf34..ac00dbfa 100644 --- a/TermTk/TTkCore/TTkTerm/input.py +++ b/TermTk/TTkCore/TTkTerm/input.py @@ -33,7 +33,7 @@ if platform.system() == 'Linux': elif platform.system() == 'Darwin': from .readinputlinux import ReadInput elif platform.system() == 'Windows': - raise NotImplementedError('Windows OS not yet supported') + from .readinputwindows import ReadInput from TermTk.TTkCore.log import TTkLog from TermTk.TTkCore.constant import TTkK diff --git a/TermTk/TTkCore/TTkTerm/readinputwindows.py b/TermTk/TTkCore/TTkTerm/readinputwindows.py new file mode 100644 index 00000000..392d03a9 --- /dev/null +++ b/TermTk/TTkCore/TTkTerm/readinputwindows.py @@ -0,0 +1,36 @@ +# 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. + +class ReadInput(): + def __init__(self): + pass + + def close(self): + pass + + def cont(self): + pass + + def read(self): + from time import sleep + sleep(5) + yield "" diff --git a/TermTk/TTkCore/TTkTerm/term.py b/TermTk/TTkCore/TTkTerm/term.py index bed6bb1e..dc4fa866 100644 --- a/TermTk/TTkCore/TTkTerm/term.py +++ b/TermTk/TTkCore/TTkTerm/term.py @@ -23,8 +23,13 @@ __all__ = ['TTkTerm'] import importlib.util +import platform if importlib.util.find_spec('pyodideProxy'): from .term_pyodide import TTkTerm -else: +elif platform.system() == 'Linux': from .term_unix import TTkTerm +elif platform.system() == 'Darwin': + from .term_unix import TTkTerm +elif platform.system() == 'Windows': + from .term_windows import TTkTerm \ No newline at end of file diff --git a/TermTk/TTkCore/TTkTerm/term_base.py b/TermTk/TTkCore/TTkTerm/term_base.py index dd80a39a..ede911a6 100644 --- a/TermTk/TTkCore/TTkTerm/term_base.py +++ b/TermTk/TTkCore/TTkTerm/term_base.py @@ -148,5 +148,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/TTkTerm/term_windows.py b/TermTk/TTkCore/TTkTerm/term_windows.py new file mode 100644 index 00000000..74b4801b --- /dev/null +++ b/TermTk/TTkCore/TTkTerm/term_windows.py @@ -0,0 +1,73 @@ +# 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. + +import sys, os, signal +from threading import Thread, Lock + +from .term_base import TTkTermBase +from TermTk.TTkCore.log import TTkLog + +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 + + @staticmethod + 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() + + @staticmethod + def _sigWinCh(signum, frame): + Thread(target=TTkTerm._sigWinChThreaded).start() + + # @staticmethod + # def _registerResizeCb(callback): + # TTkTerm._sigWinChCb = callback + # # Dummy call to retrieve the terminal size + # TTkTerm._sigWinCh(signal.SIGWINCH, None) + # signal.signal(signal.SIGWINCH, TTkTerm._sigWinCh) + # TTkTermBase.registerResizeCb = _registerResizeCb \ No newline at end of file From d9182cd3cd4d6ae8a3e52ee55fffcd99ecaf4d07 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 20 Oct 2023 12:14:02 +0100 Subject: [PATCH 27/52] updated doc --- docs/MDNotes/msWindows/Resources.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/MDNotes/msWindows/Resources.md b/docs/MDNotes/msWindows/Resources.md index a127c6a7..f3444200 100644 --- a/docs/MDNotes/msWindows/Resources.md +++ b/docs/MDNotes/msWindows/Resources.md @@ -1,17 +1,29 @@ # 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 +# 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 ``` +# termios wrappers + - termiWin -> https://github.com/veeso/termiWin + # Competitors with MS-Win support -## Textual -> https://github.com/Textualize/textual +### Textual -> https://github.com/Textualize/textual + +### TheVTPyProject -> https://github.com/srccircumflex/TheVTPyProject -## TheVTPyProject -> https://github.com/srccircumflex/TheVTPyProject \ No newline at end of file +# Incompatible code (the one using termios): + - TermTk/TTkCore/TTkTerm/readinputlinux.py + - TermTk/TTkCore/TTkTerm/readinputlinux_thread.py + - TermTk/TTkCore/TTkTerm/term_unix.py From 29c433fe1fd67aea369ff5668c74cab0db255fec Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 20 Oct 2023 13:18:50 +0100 Subject: [PATCH 28/52] Removed unrequired imports --- TermTk/TTkCore/TTkTerm/__init__.py | 8 ++--- TermTk/TTkUiTools/properties/__init__.py | 33 --------------------- TermTk/TTkUiTools/uiproperties.py | 37 ++++++++++++++++++++++-- 3 files changed, 39 insertions(+), 39 deletions(-) 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/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/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 From d0969508a1fb4f50cd815e687affaad87b096671 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 20 Oct 2023 19:28:46 +0100 Subject: [PATCH 29/52] Enhancement: #190 allow independent closable status in the tabs --- TermTk/TTkWidgets/tabwidget.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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): From 2f9b1251d713af75f883814cf2a8f79bae296202 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Sat, 21 Oct 2023 14:57:50 +0100 Subject: [PATCH 30/52] Enhancement: #189 allow to set the starting value of the line number in the Textedit --- TermTk/TTkWidgets/texedit.py | 34 ++++++++++++++++++++++------------ demo/showcase/textedit.py | 9 ++++++--- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/TermTk/TTkWidgets/texedit.py b/TermTk/TTkWidgets/texedit.py index 014dcaba..dd8cb2a2 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) @@ -862,6 +865,13 @@ class TTkTextEdit(TTkAbstractScrollArea): '''setLineNumber''' self._lineNumberView.setVisible(ln) + def lineNumberStarting(self): + return self._lineNumberView._startingNumber + + def setLineNumberStarting(self, starting): + self._lineNumberView._startingNumber = starting + self._lineNumberView._wrapChanged() + def setDocument(self, document): '''setDocument''' self._textEditView.setDocument(document) diff --git a/demo/showcase/textedit.py b/demo/showcase/textedit.py index c37951da..2a690f09 100755 --- a/demo/showcase/textedit.py +++ b/demo/showcase/textedit.py @@ -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) @@ -127,7 +127,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 +140,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 +210,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()) From fe85372e9a7890b27d8da933d2f979f6a6c8738f Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Sat, 21 Oct 2023 15:01:10 +0100 Subject: [PATCH 31/52] fix teest error in the uiproperties import --- tools/check.import.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/check.import.sh b/tools/check.import.sh index e37b3f6a..3036362f 100755 --- a/tools/check.import.sh +++ b/tools/check.import.sh @@ -40,7 +40,7 @@ __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" \ From d42dc0bca922a9f50e35e6788826f0e7327c37d2 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Sat, 21 Oct 2023 15:13:20 +0100 Subject: [PATCH 32/52] Enhancement: #189 included the linenumber property in the ttkDesigner --- TermTk/TTkUiTools/properties/texedit.py | 8 +++++++- TermTk/TTkWidgets/texedit.py | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) 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/TTkWidgets/texedit.py b/TermTk/TTkWidgets/texedit.py index dd8cb2a2..1b3e407c 100644 --- a/TermTk/TTkWidgets/texedit.py +++ b/TermTk/TTkWidgets/texedit.py @@ -861,6 +861,7 @@ class TTkTextEdit(TTkAbstractScrollArea): '''getLineNumber''' return self._lineNumberView.isVisible() + @pyTTkSlot(bool) def setLineNumber(self, ln): '''setLineNumber''' self._lineNumberView.setVisible(ln) @@ -868,6 +869,7 @@ class TTkTextEdit(TTkAbstractScrollArea): def lineNumberStarting(self): return self._lineNumberView._startingNumber + @pyTTkSlot(int) def setLineNumberStarting(self, starting): self._lineNumberView._startingNumber = starting self._lineNumberView._wrapChanged() From 7c056e836150f17d6d50d18269aec504c5206e84 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Mon, 23 Oct 2023 14:47:58 +0100 Subject: [PATCH 33/52] Added Windows Console input test --- tests/test.input.curses.py | 68 +++++++ tests/test.input.win.py | 368 ++++++++++++++++++++++++++++++++++--- 2 files changed, 407 insertions(+), 29 deletions(-) create mode 100755 tests/test.input.curses.py 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.win.py b/tests/test.input.win.py index 5e835751..9bf67e8d 100755 --- a/tests/test.input.win.py +++ b/tests/test.input.win.py @@ -1,8 +1,6 @@ -#!/usr/bin/env python3 - # 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 +20,348 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import sys, os -import logging +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. + -sys.path.append(os.path.join(sys.path[0],'..')) -from TermTk import TTkLog, TTkK, TTkGridLayout, TTk, TTkLogViewer, TTkHelper +# 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)] -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 -def pasteCallback(txt:str): - TTkLog.info(f"PASTE:") - for s in txt.split('\n'): - TTkLog.info(f" | {s}") - return True +# 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)] -root = TTk(layout=TTkGridLayout()) -TTkLogViewer(parent=root) +# 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[?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() + +sys.stdout.write("\x1b[?1000h") +sys.stdout.write("\x1b[?1003h") +sys.stdout.write("\x1b[?1015h") +sys.stdout.write("\x1b[?1006h") +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 -TTkHelper._rootWidget._input.inputEvent.connect(keyCallback) -TTkHelper._rootWidget._input.pasteEvent.connect(pasteCallback) +hStdin = GetStdHandle(STD_INPUT_HANDLE) +if hStdin == INVALID_HANDLE_VALUE: + raise Exception("GetStdHandle") + +# // Save the current input mode, to be restored on exit. +# +# if (! GetConsoleMode(hStdin, &fdwSaveOldMode) ) +# 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 + +fdwSaveOldMode = wintypes.DWORD() +if not GetConsoleMode(hStdin, byref(fdwSaveOldMode)): + raise Exception("GetConsoleMode") + +# // 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 + +fdwMode = ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT +if not SetConsoleMode(hStdin, fdwMode): + raise Exception("SetConsoleMode") + +# 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, fdwSaveOldMode); +if not SetConsoleMode(hStdin, fdwSaveOldMode): + raise Exception("SetConsoleMode") + +# return 0; +# -root.mainloop() +reset() +print('OK') From 47c5e9c5e67b14a80064a506cc540c7dcf44b2c9 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Mon, 23 Oct 2023 15:41:23 +0100 Subject: [PATCH 34/52] Fixed Test mouse events reported in the windows console --- tests/test.input.win.py | 74 ++++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/tests/test.input.win.py b/tests/test.input.win.py index 9bf67e8d..f31b4b79 100755 --- a/tests/test.input.win.py +++ b/tests/test.input.win.py @@ -170,19 +170,20 @@ class INPUT_RECORD(Structure): def reset(): # Reset sys.stdout.write("\033[?1000l") - sys.stdout.write("\033[?1002l") + # sys.stdout.write("\033[?1002l") sys.stdout.write("\033[?1003l") - 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.write("\033[?1015l") + # sys.stdout.write("\033[?1049l") # Switch to normal screen + # sys.stdout.write("\033[?2004l") # Paste Bracketed mode sys.stdout.flush() -sys.stdout.write("\x1b[?1000h") -sys.stdout.write("\x1b[?1003h") -sys.stdout.write("\x1b[?1015h") -sys.stdout.write("\x1b[?1006h") -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]; @@ -190,8 +191,8 @@ sys.stdout.flush() # // Get the standard input handle. # -# hStdin = GetStdHandle(STD_INPUT_HANDLE); -# if (hStdin == INVALID_HANDLE_VALUE) +# hStdIn = GetStdHandle(STD_INPUT_HANDLE); +# if (hStdIn == INVALID_HANDLE_VALUE) # ErrorExit("GetStdHandle"); # # From: @@ -205,13 +206,17 @@ GetStdHandle = windll.kernel32.GetStdHandle GetStdHandle.argtypes = [wintypes.DWORD] GetStdHandle.restype = wintypes.HANDLE -hStdin = GetStdHandle(STD_INPUT_HANDLE) -if hStdin == INVALID_HANDLE_VALUE: +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, &fdwSaveOldMode) ) +# if (! GetConsoleMode(hStdIn, &fdwSaveOldModeIn) ) # ErrorExit("GetConsoleMode"); # # From: @@ -226,14 +231,21 @@ GetConsoleMode = windll.kernel32.GetConsoleMode GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD] GetConsoleMode.restype = wintypes.BOOL -fdwSaveOldMode = wintypes.DWORD() -if not GetConsoleMode(hStdin, byref(fdwSaveOldMode)): +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) ) +# if (! SetConsoleMode(hStdIn, fdwMode) ) # ErrorExit("SetConsoleMode"); # # From: @@ -248,10 +260,21 @@ SetConsoleMode = windll.kernel32.SetConsoleMode SetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.DWORD] SetConsoleMode.restype = wintypes.BOOL -fdwMode = ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT -if not SetConsoleMode(hStdin, fdwMode): +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 # @@ -284,19 +307,19 @@ for _ in range(50): # // Wait for the events. # # if (! ReadConsoleInput( -# hStdin, // input buffer handle +# 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 + 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"{hStdIn=} {irInBuf=} {cNumRead=}") print(f"{cNumRead=}") # // Dispatch the events to the appropriate handler. @@ -356,8 +379,11 @@ for _ in range(50): # // Restore input mode on exit. # -# SetConsoleMode(hStdin, fdwSaveOldMode); -if not SetConsoleMode(hStdin, fdwSaveOldMode): +# SetConsoleMode(hStdIn, fdwSaveOldModeIn); +if not SetConsoleMode(hStdIn, fdwSaveOldModeIn): + raise Exception("SetConsoleMode") + +if not SetConsoleMode(hStdOut, fdwSaveOldModeOut): raise Exception("SetConsoleMode") # return 0; From 38ee81f855b1ebaf9c117c34f155647e73c1c35d Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Tue, 24 Oct 2023 17:45:49 +0100 Subject: [PATCH 35/52] Initial drop of the platform specific routines, TTkInput is now a static class --- TermTk/TTkCore/TTkTerm/input.py | 141 +++--- TermTk/TTkCore/TTkTerm/readinputwindows.py | 36 -- TermTk/TTkCore/TTkTerm/term_base.py | 28 +- TermTk/TTkCore/drivers/__init__.py | 0 TermTk/TTkCore/drivers/pyodide.py | 0 .../readinputlinux.py => drivers/unix.py} | 4 +- .../unix_thread.py} | 0 TermTk/TTkCore/drivers/windows.py | 344 ++++++++++++++ TermTk/TTkCore/ttk.py | 30 +- TermTk/TTkTestWidgets/keypressview.py | 3 +- docs/MDNotes/msWindows/Resources.md | 2 + tests/test.input.py | 17 +- tests/test.input.raw.py | 8 +- ...test.input.win.py => test.input.win.01.py} | 0 tests/test.input.win.02.py | 427 ++++++++++++++++++ 15 files changed, 884 insertions(+), 156 deletions(-) delete mode 100644 TermTk/TTkCore/TTkTerm/readinputwindows.py create mode 100644 TermTk/TTkCore/drivers/__init__.py create mode 100644 TermTk/TTkCore/drivers/pyodide.py rename TermTk/TTkCore/{TTkTerm/readinputlinux.py => drivers/unix.py} (98%) rename TermTk/TTkCore/{TTkTerm/readinputlinux_thread.py => drivers/unix_thread.py} (100%) create mode 100644 TermTk/TTkCore/drivers/windows.py rename tests/{test.input.win.py => test.input.win.01.py} (100%) create mode 100755 tests/test.input.win.02.py diff --git a/TermTk/TTkCore/TTkTerm/input.py b/TermTk/TTkCore/TTkTerm/input.py index ac00dbfa..acf8a8e1 100644 --- a/TermTk/TTkCore/TTkTerm/input.py +++ b/TermTk/TTkCore/TTkTerm/input.py @@ -28,71 +28,76 @@ from time import time import platform if platform.system() == 'Linux': - from .readinputlinux import ReadInput - # from .readinputlinux_thread import ReadInput + from ..drivers.unix import TTkInputDriver elif platform.system() == 'Darwin': - from .readinputlinux import ReadInput + from ..drivers.unix import TTkInputDriver elif platform.system() == 'Windows': - from .readinputwindows import ReadInput + from ..drivers.windows 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 +107,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 +139,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 +176,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/readinputwindows.py b/TermTk/TTkCore/TTkTerm/readinputwindows.py deleted file mode 100644 index 392d03a9..00000000 --- a/TermTk/TTkCore/TTkTerm/readinputwindows.py +++ /dev/null @@ -1,36 +0,0 @@ -# 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. - -class ReadInput(): - def __init__(self): - pass - - def close(self): - pass - - def cont(self): - pass - - def read(self): - from time import sleep - sleep(5) - yield "" diff --git a/TermTk/TTkCore/TTkTerm/term_base.py b/TermTk/TTkCore/TTkTerm/term_base.py index ede911a6..ff944a14 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,22 +91,29 @@ 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): 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) + TTkTermBase.setEcho(False) + TTkTermBase.CRNL(False) + TTkTermBase.setSigmask(sigmask, False) + + @staticmethod + def setMouse(mouse:bool=False, directMouse:bool=False) -> None: + TTkTermBase.mouse = mouse | directMouse + TTkTermBase.directMouse = directMouse if TTkTermBase.mouse: TTkTermBase.push(TTkTermBase.Mouse.ON) + else: + TTkTermBase.push(TTkTermBase.Mouse.OFF) if TTkTermBase.directMouse: TTkTermBase.push(TTkTermBase.Mouse.DIRECT_ON) - TTkTermBase.setEcho(False) - TTkTermBase.CRNL(False) - TTkTermBase.setSigmask(sigmask, False) + else: + TTkTermBase.push(TTkTermBase.Mouse.DIRECT_OFF) @staticmethod def exit(): @@ -125,10 +132,7 @@ class TTkTermBase(): @staticmethod def cont(): 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) diff --git a/TermTk/TTkCore/drivers/__init__.py b/TermTk/TTkCore/drivers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/TermTk/TTkCore/drivers/pyodide.py b/TermTk/TTkCore/drivers/pyodide.py new file mode 100644 index 00000000..e69de29b diff --git a/TermTk/TTkCore/TTkTerm/readinputlinux.py b/TermTk/TTkCore/drivers/unix.py similarity index 98% rename from TermTk/TTkCore/TTkTerm/readinputlinux.py rename to TermTk/TTkCore/drivers/unix.py index 0ba4b635..4f2f81ca 100644 --- a/TermTk/TTkCore/TTkTerm/readinputlinux.py +++ b/TermTk/TTkCore/drivers/unix.py @@ -20,6 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +__all__ = [''] + import sys, os, re from select import select @@ -29,7 +31,7 @@ except Exception as e: exit(1) -class ReadInput(): +class TTkInputDriver(): __slots__ = ('_readPipe','_attr') def __init__(self): 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..d3ef3de0 --- /dev/null +++ b/TermTk/TTkCore/drivers/windows.py @@ -0,0 +1,344 @@ +# 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__ = [''] + +import sys + +from ctypes import Structure, Union, byref, wintypes, windll + +from TermTk.TTkCore.constant import TTkK +from TermTk.TTkCore.log import TTkLog +from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent + +# 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(): + 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. + saveKey = "" + for bb in irInBuf[:cNumRead.value]: + # if not bb.EventType: continue + # TTkLog.debug(f"{bb=} {bb.EventType=} {cNumRead.value=}") + + if bb.EventType == KEY_EVENT: + if not bb.Event.KeyEvent.bKeyDown: + continue + saveKey += bb.Event.KeyEvent.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=}") + + yield saveKey diff --git a/TermTk/TTkCore/ttk.py b/TermTk/TTkCore/ttk.py index fe0347cf..3c584f17 100644 --- a/TermTk/TTkCore/ttk.py +++ b/TermTk/TTkCore/ttk.py @@ -40,19 +40,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 +98,8 @@ 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) 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,6 +143,7 @@ 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 @@ -162,15 +161,16 @@ 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() @@ -183,7 +183,7 @@ class TTk(TTkContainer): def _mainLoop(self): if platform.system() == 'Emscripten': return - self._input.start() + TTkInput.start() @pyTTkSlot(str) def _processPaste(self, txt:str): @@ -325,15 +325,15 @@ 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): """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) @@ -341,7 +341,7 @@ class TTk(TTkContainer): """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 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/docs/MDNotes/msWindows/Resources.md b/docs/MDNotes/msWindows/Resources.md index f3444200..f8e34eeb 100644 --- a/docs/MDNotes/msWindows/Resources.md +++ b/docs/MDNotes/msWindows/Resources.md @@ -12,6 +12,7 @@ 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 @@ -20,6 +21,7 @@ python.exe demo/demo.py # 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 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 e74f4cf7..cf65b5a5 100755 --- a/tests/test.input.raw.py +++ b/tests/test.input.raw.py @@ -40,6 +40,7 @@ def reset(): # Reset TTkTerm.push("\033[?1000l") TTkTerm.push("\033[?1002l") + TTkTerm.push("\033[?1003l") TTkTerm.push("\033[?1015l") TTkTerm.push("\033[?1006l") TTkTerm.push("\033[?1049l") # Switch to normal screen @@ -49,10 +50,11 @@ reset() TTkTerm.push("\033[?2004h") # Paste Bracketed mode # TTkTerm.push("\033[?1000h") -# TTkTerm.push("\033[?1002h") -# TTkTerm.push("\033[?1006h") +TTkTerm.push("\033[?1002h") +# TTkTerm.push("\033[?1003h") +TTkTerm.push("\033[?1006h") # TTkTerm.push("\033[?1015h") -TTkTerm.push("\033[?1049h") # Switch to alternate screen +# 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.py b/tests/test.input.win.01.py similarity index 100% rename from tests/test.input.win.py rename to tests/test.input.win.01.py diff --git a/tests/test.input.win.02.py b/tests/test.input.win.02.py new file mode 100755 index 00000000..62260557 --- /dev/null +++ b/tests/test.input.win.02.py @@ -0,0 +1,427 @@ +# 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 + saveKey = "" + for bb in irInBuf[:cNumRead.value]: + # if not bb.EventType: continue + print(f"{bb=} {bb.EventType=} {cNumRead.value=}") + + if bb.EventType == KEY_EVENT: + saveKey += bb.Event.KeyEvent.uChar.UnicodeChar + #continue + print( + # f" evt:{bb.Event.KeyEvent}" + + f" kd:{bb.Event.KeyEvent.bKeyDown}" + + f" rc:{bb.Event.KeyEvent.wRepeatCount}" + + f" VKC:{bb.Event.KeyEvent.wVirtualKeyCode}" + + f" VSC:{bb.Event.KeyEvent.wVirtualScanCode}" + + f" UC:{bb.Event.KeyEvent.uChar.UnicodeChar}" + + f" AC:{bb.Event.KeyEvent.uChar.AsciiChar}" + + f" CKS:{bb.Event.KeyEvent.dwControlKeyState} -> {saveKey=}" + ) + # 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=}") + + elif 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=}") + + print(f"{saveKey=}") + +# 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') From eff23e496847647f3ef887531abf6f64275d8cc7 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Tue, 24 Oct 2023 23:53:19 +0100 Subject: [PATCH 36/52] Handled terminal resize event, moved all the plaform specific routines into the driver folder --- TermTk/TTkCore/TTkTerm/term.py | 12 +----- TermTk/TTkCore/TTkTerm/term_base.py | 8 ++-- TermTk/TTkCore/drivers/__init__.py | 15 +++++++ TermTk/TTkCore/drivers/pyodide.py | 40 +++++++++++++++++ .../{TTkTerm => drivers}/term_pyodide.py | 4 +- .../TTkCore/{TTkTerm => drivers}/term_unix.py | 4 +- .../{TTkTerm => drivers}/term_windows.py | 43 +++++++++++-------- TermTk/TTkCore/drivers/unix.py | 26 ++++++++++- TermTk/TTkCore/drivers/windows.py | 35 ++++++++++++--- TermTk/TTkCore/ttk.py | 19 +++++--- 10 files changed, 156 insertions(+), 50 deletions(-) rename TermTk/TTkCore/{TTkTerm => drivers}/term_pyodide.py (96%) rename TermTk/TTkCore/{TTkTerm => drivers}/term_unix.py (98%) rename TermTk/TTkCore/{TTkTerm => drivers}/term_windows.py (65%) diff --git a/TermTk/TTkCore/TTkTerm/term.py b/TermTk/TTkCore/TTkTerm/term.py index dc4fa866..8f141038 100644 --- a/TermTk/TTkCore/TTkTerm/term.py +++ b/TermTk/TTkCore/TTkTerm/term.py @@ -22,14 +22,4 @@ __all__ = ['TTkTerm'] -import importlib.util -import platform - -if importlib.util.find_spec('pyodideProxy'): - from .term_pyodide import TTkTerm -elif platform.system() == 'Linux': - from .term_unix import TTkTerm -elif platform.system() == 'Darwin': - from .term_unix import TTkTerm -elif platform.system() == 'Windows': - from .term_windows import TTkTerm \ No newline at end of file +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 ff944a14..a65b5c69 100644 --- a/TermTk/TTkCore/TTkTerm/term_base.py +++ b/TermTk/TTkCore/TTkTerm/term_base.py @@ -91,7 +91,7 @@ class TTkTermBase(): _sigWinChCb = None @staticmethod - def init(title: str = "TermTk", sigmask=0): + def init(title: str = "TermTk", sigmask=0) -> None: TTkTermBase.title = title TTkTermBase.Cursor.hide() TTkTermBase.push(TTkTermBase.escTitle(TTkTermBase.title)) @@ -116,21 +116,21 @@ class TTkTermBase(): TTkTermBase.push(TTkTermBase.Mouse.DIRECT_OFF) @staticmethod - def exit(): + 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)) TTkTermBase.setMouse(TTkTermBase.mouse, TTkTermBase.directMouse) TTkTermBase.setEcho(False) diff --git a/TermTk/TTkCore/drivers/__init__.py b/TermTk/TTkCore/drivers/__init__.py index e69de29b..c2a77f6f 100644 --- a/TermTk/TTkCore/drivers/__init__.py +++ 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 index e69de29b..d81af6d7 100644 --- a/TermTk/TTkCore/drivers/pyodide.py +++ b/TermTk/TTkCore/drivers/pyodide.py @@ -0,0 +1,40 @@ +# 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 TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot + +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(): pass + 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/TTkTerm/term_windows.py b/TermTk/TTkCore/drivers/term_windows.py similarity index 65% rename from TermTk/TTkCore/TTkTerm/term_windows.py rename to TermTk/TTkCore/drivers/term_windows.py index 74b4801b..d42da0b2 100644 --- a/TermTk/TTkCore/TTkTerm/term_windows.py +++ b/TermTk/TTkCore/drivers/term_windows.py @@ -20,13 +20,22 @@ # 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 -from .term_base import TTkTermBase +from ..TTkTerm.term_base import TTkTermBase from TermTk.TTkCore.log import TTkLog +from .windows import * class TTkTerm(TTkTermBase): + # force directMouse onn Windows + # otherwise the mouse events are not received + @staticmethod + def setMouse(mouse:bool=False, directMouse:bool=False) -> None: + TTkTermBase.setMouse(mouse|directMouse, mouse|directMouse) + @staticmethod def _push(*args): try: @@ -51,23 +60,21 @@ class TTkTerm(TTkTermBase): print(f'ERROR: {e}') TTkTermBase.getTerminalSize = _getTerminalSize - @staticmethod - 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() + _sigWinChMutex = Lock() @staticmethod - def _sigWinCh(signum, frame): - Thread(target=TTkTerm._sigWinChThreaded).start() + 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 - # # Dummy call to retrieve the terminal size - # TTkTerm._sigWinCh(signal.SIGWINCH, None) - # signal.signal(signal.SIGWINCH, TTkTerm._sigWinCh) - # TTkTermBase.registerResizeCb = _registerResizeCb \ No newline at end of file + @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/drivers/unix.py b/TermTk/TTkCore/drivers/unix.py index 4f2f81ca..66b3ab64 100644 --- a/TermTk/TTkCore/drivers/unix.py +++ b/TermTk/TTkCore/drivers/unix.py @@ -20,9 +20,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -__all__ = [''] +__all__ = ['TTkSignalDriver','TTkInputDriver'] import sys, os, re +import signal from select import select try: import fcntl, termios, tty @@ -30,6 +31,8 @@ except Exception as e: print(f'ERROR: {e}') exit(1) +from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot + class TTkInputDriver(): __slots__ = ('_readPipe','_attr') @@ -66,3 +69,24 @@ class TTkInputDriver(): 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/drivers/windows.py b/TermTk/TTkCore/drivers/windows.py index d3ef3de0..51858363 100644 --- a/TermTk/TTkCore/drivers/windows.py +++ b/TermTk/TTkCore/drivers/windows.py @@ -20,15 +20,15 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -__all__ = [''] +__all__ = ['TTkSignalDriver','TTkInputDriver'] -import sys +import signal from ctypes import Structure, Union, byref, wintypes, windll -from TermTk.TTkCore.constant import TTkK +from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot + from TermTk.TTkCore.log import TTkLog -from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent # Based on the example ported from: # https://learn.microsoft.com/en-us/windows/console/reading-input-buffer-events @@ -199,6 +199,7 @@ class INPUT_RECORD(Structure): class TTkInputDriver(): + windowResized = pyTTkSignal(int,int) def __init__(self): self._run = True self._initTerminal() @@ -337,8 +338,28 @@ class TTkInputDriver(): # 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=}") + # 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) yield saveKey + +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/ttk.py b/TermTk/TTkCore/ttk.py index 3c584f17..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 @@ -100,6 +101,9 @@ class TTk(TTkContainer): self._termDirectMouse = kwargs.get('mouseTrack',False) 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)) @@ -146,9 +150,7 @@ class TTk(TTkContainer): 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") @@ -176,7 +178,7 @@ class TTk(TTkContainer): self._mainLoop() finally: if platform.system() != 'Emscripten': - signal.signal(signal.SIGINT, signal.SIG_DFL) + TTkSignalDriver.exit() self.quit() TTkTerm.exit() @@ -329,7 +331,8 @@ class TTk(TTkContainer): self._paintEvent.set() 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() @@ -337,7 +340,8 @@ class TTk(TTkContainer): # 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() @@ -346,7 +350,8 @@ class TTk(TTkContainer): # 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. From f92a81af52eeb195d8cb2315d4cf4c9b3171cfb8 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Tue, 24 Oct 2023 23:54:23 +0100 Subject: [PATCH 37/52] Removed zero sized chars in the demo if running under windows --- demo/showcase/_showcasehelper.py | 13 ++++++++++--- demo/showcase/formwidgets02.py | 4 ++-- demo/showcase/textedit.py | 6 +----- 3 files changed, 13 insertions(+), 10 deletions(-) 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/textedit.py b/demo/showcase/textedit.py index 2a690f09..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): @@ -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( " - | | | | | -") From f3fe9a6e6606cdf6d03f4e999dafaab4f7f5b267 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 25 Oct 2023 14:31:47 +0100 Subject: [PATCH 38/52] Fixed Copy/Paste in the windows driver --- TermTk/TTkCore/drivers/windows.py | 11 ++++-- tests/test.input.win.02.py | 61 ++++++++++++++++++------------- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/TermTk/TTkCore/drivers/windows.py b/TermTk/TTkCore/drivers/windows.py index 51858363..b438b5d5 100644 --- a/TermTk/TTkCore/drivers/windows.py +++ b/TermTk/TTkCore/drivers/windows.py @@ -323,15 +323,18 @@ class TTkInputDriver(): # TTkLog.debug(f"{cNumRead=}") # Dispatch the events to the appropriate handler. - saveKey = "" + 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: - if not bb.Event.KeyEvent.bKeyDown: + ke = bb.Event.KeyEvent + if ( not ke.bKeyDown or + ke.dwControlKeyState or + ke.wVirtualKeyCode ): continue - saveKey += bb.Event.KeyEvent.uChar.UnicodeChar + saveKeys.append(ke.uChar.UnicodeChar) elif bb.EventType == MOUSE_EVENT: # It is not supposed to receive Mouse Events # due to ENABLE_VIRTUAL_TERMINAL_PROCESSING @@ -343,7 +346,7 @@ class TTkInputDriver(): # TTkLog.debug(f"{bb.Event.WindowBufferSizeEvent.dwSize.Y=}") TTkInputDriver.windowResized.emit(bb.Event.WindowBufferSizeEvent.dwSize.X, bb.Event.WindowBufferSizeEvent.dwSize.Y) - yield saveKey + yield "".join(saveKeys).encode("utf-16", "surrogatepass").decode("utf-16") class TTkSignalDriver(): sigStop = pyTTkSignal() diff --git a/tests/test.input.win.02.py b/tests/test.input.win.02.py index 62260557..e625ee5c 100755 --- a/tests/test.input.win.02.py +++ b/tests/test.input.win.02.py @@ -327,34 +327,14 @@ for _ in range(50): # Dispatch the events to the appropriate handler. lastMousePress = 0 - saveKey = "" + saveKeyb = b"" + saveKeys = b"" + listKeys = [] for bb in irInBuf[:cNumRead.value]: # if not bb.EventType: continue - print(f"{bb=} {bb.EventType=} {cNumRead.value=}") + # print(f"{bb=} {bb.EventType=} {cNumRead.value=}") - if bb.EventType == KEY_EVENT: - saveKey += bb.Event.KeyEvent.uChar.UnicodeChar - #continue - print( - # f" evt:{bb.Event.KeyEvent}" + - f" kd:{bb.Event.KeyEvent.bKeyDown}" + - f" rc:{bb.Event.KeyEvent.wRepeatCount}" + - f" VKC:{bb.Event.KeyEvent.wVirtualKeyCode}" + - f" VSC:{bb.Event.KeyEvent.wVirtualScanCode}" + - f" UC:{bb.Event.KeyEvent.uChar.UnicodeChar}" + - f" AC:{bb.Event.KeyEvent.uChar.AsciiChar}" + - f" CKS:{bb.Event.KeyEvent.dwControlKeyState} -> {saveKey=}" - ) - # 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=}") - - elif bb.EventType == MOUSE_EVENT: + if bb.EventType == MOUSE_EVENT: x = bb.Event.MouseEvent.dwMousePosition.X y = bb.Event.MouseEvent.dwMousePosition.Y print(f"{bb.Event.MouseEvent.dwControlKeyState=}") @@ -410,8 +390,37 @@ for _ in range(50): 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=}") - print(f"{saveKey=}") # Restore input mode on exit. if not SetConsoleMode(hStdIn, fdwSaveOldModeIn): From 1dc8dbf58c5f881ee40c0827a78210eb6da4b54f Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 25 Oct 2023 14:53:04 +0100 Subject: [PATCH 39/52] Windows Driver, drop empty inputs --- TermTk/TTkCore/drivers/windows.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TermTk/TTkCore/drivers/windows.py b/TermTk/TTkCore/drivers/windows.py index b438b5d5..007bea5c 100644 --- a/TermTk/TTkCore/drivers/windows.py +++ b/TermTk/TTkCore/drivers/windows.py @@ -345,8 +345,8 @@ class TTkInputDriver(): # 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) - - yield "".join(saveKeys).encode("utf-16", "surrogatepass").decode("utf-16") + if saveKeys: + yield "".join(saveKeys).encode("utf-16", "surrogatepass").decode("utf-16") class TTkSignalDriver(): sigStop = pyTTkSignal() From 1b9e5aa9a5614ff9da592d75a866b60d9c99baf3 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 25 Oct 2023 15:13:49 +0100 Subject: [PATCH 40/52] Tweaked the mouse initialization --- TermTk/TTkCore/TTkTerm/term_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TermTk/TTkCore/TTkTerm/term_base.py b/TermTk/TTkCore/TTkTerm/term_base.py index a65b5c69..2585c434 100644 --- a/TermTk/TTkCore/TTkTerm/term_base.py +++ b/TermTk/TTkCore/TTkTerm/term_base.py @@ -107,12 +107,12 @@ class TTkTermBase(): 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) - if TTkTermBase.directMouse: - TTkTermBase.push(TTkTermBase.Mouse.DIRECT_ON) - else: TTkTermBase.push(TTkTermBase.Mouse.DIRECT_OFF) @staticmethod From 3ccc69b00db2519a72736447c1bde5fa9772f39a Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 25 Oct 2023 16:30:22 +0100 Subject: [PATCH 41/52] Updated windows porting doc --- docs/MDNotes/msWindows/Init Sequence.md | 36 +++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 docs/MDNotes/msWindows/Init Sequence.md 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 From ff873afc0be7bb090796e03a176efda3c556255d Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 25 Oct 2023 16:31:09 +0100 Subject: [PATCH 42/52] Adapted pyodide to the new structure --- TermTk/TTkCore/TTkTerm/input.py | 7 +------ tests/sandbox/sandbox.html | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/TermTk/TTkCore/TTkTerm/input.py b/TermTk/TTkCore/TTkTerm/input.py index acf8a8e1..27ab9141 100644 --- a/TermTk/TTkCore/TTkTerm/input.py +++ b/TermTk/TTkCore/TTkTerm/input.py @@ -27,12 +27,7 @@ from time import time import platform -if platform.system() == 'Linux': - from ..drivers.unix import TTkInputDriver -elif platform.system() == 'Darwin': - from ..drivers.unix import TTkInputDriver -elif platform.system() == 'Windows': - from ..drivers.windows import TTkInputDriver +from ..drivers import TTkInputDriver from TermTk.TTkCore.log import TTkLog from TermTk.TTkCore.constant import TTkK 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=}") From 01641cdc3d51a20d1304d95babeca4fb21522e14 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 25 Oct 2023 16:38:48 +0100 Subject: [PATCH 43/52] Removed force direct mouse in the windows driver --- TermTk/TTkCore/drivers/term_windows.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/TermTk/TTkCore/drivers/term_windows.py b/TermTk/TTkCore/drivers/term_windows.py index d42da0b2..cd9ee35e 100644 --- a/TermTk/TTkCore/drivers/term_windows.py +++ b/TermTk/TTkCore/drivers/term_windows.py @@ -30,12 +30,6 @@ from TermTk.TTkCore.log import TTkLog from .windows import * class TTkTerm(TTkTermBase): - # force directMouse onn Windows - # otherwise the mouse events are not received - @staticmethod - def setMouse(mouse:bool=False, directMouse:bool=False) -> None: - TTkTermBase.setMouse(mouse|directMouse, mouse|directMouse) - @staticmethod def _push(*args): try: From a95b0279d2b5dbc991b0060877891493dec8915e Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 25 Oct 2023 16:56:03 +0100 Subject: [PATCH 44/52] Fix test --- tools/check.import.sh | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/tools/check.import.sh b/tools/check.import.sh index 3036362f..f1d8d05b 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" \ @@ -44,7 +37,29 @@ __check(){ -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 "propertyanimation.py:import time, math" | + 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 From 4a70ff6090066505d55f1d44fdd60d38594fe69e Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 25 Oct 2023 17:37:48 +0100 Subject: [PATCH 45/52] Adapted mock test objects to the new driver --- TermTk/TTkCore/drivers/pyodide.py | 2 +- TermTk/TTkCore/drivers/term_windows.py | 2 +- tests/pytest/conftest.py | 4 ++-- tests/pytest/mock_input.py | 20 ++++++++++++++------ tests/pytest/mock_term.py | 2 +- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/TermTk/TTkCore/drivers/pyodide.py b/TermTk/TTkCore/drivers/pyodide.py index d81af6d7..7e383849 100644 --- a/TermTk/TTkCore/drivers/pyodide.py +++ b/TermTk/TTkCore/drivers/pyodide.py @@ -22,7 +22,7 @@ __all__ = ['TTkSignalDriver','TTkInputDriver'] -from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot +from TermTk.TTkCore.signal import pyTTkSignal class TTkInputDriver(): def close(self): pass diff --git a/TermTk/TTkCore/drivers/term_windows.py b/TermTk/TTkCore/drivers/term_windows.py index cd9ee35e..f0506724 100644 --- a/TermTk/TTkCore/drivers/term_windows.py +++ b/TermTk/TTkCore/drivers/term_windows.py @@ -22,7 +22,7 @@ __all__ = ['TTkTerm'] -import sys, os, signal +import sys, os from threading import Thread, Lock from ..TTkTerm.term_base import TTkTermBase 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 From 5fe92d6af889f1fe94831c996b1b37ae3d0bf22a Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 25 Oct 2023 17:47:11 +0100 Subject: [PATCH 46/52] TTkList Widget: dnd is disable by default to avoid unexpected dragging. --- TermTk/TTkCore/constant.py | 9 ++++++--- TermTk/TTkWidgets/listwidget.py | 3 +-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/TermTk/TTkCore/constant.py b/TermTk/TTkCore/constant.py index 833200e4..4d0fde66 100644 --- a/TermTk/TTkCore/constant.py +++ b/TermTk/TTkCore/constant.py @@ -136,9 +136,12 @@ class TTkConstant: '''Drag allowed''' AllowDrop = 0x02 '''Drop allowed''' - NoDragDrop = DragDropMode.NoDragDrop - AllowDrag = DragDropMode.AllowDrag - AllowDrop = DragDropMode.AllowDrop + 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. diff --git a/TermTk/TTkWidgets/listwidget.py b/TermTk/TTkWidgets/listwidget.py index 4e54deb4..8a43e667 100644 --- a/TermTk/TTkWidgets/listwidget.py +++ b/TermTk/TTkWidgets/listwidget.py @@ -125,8 +125,7 @@ class TTkListWidget(TTkAbstractScrollView): self._items = [] self._highlighted = None self._dragPos = None - self._dndMode = kwargs.get("dragDropMode", - TTkK.DragDropMode.AllowDrag | TTkK.DragDropMode.AllowDrop ) + self._dndMode = kwargs.get("dragDropMode", TTkK.DragDropMode.NoDragDrop) # Signals self.itemClicked = pyTTkSignal(TTkAbstractListItem) self.textClicked = pyTTkSignal(str) From 1b19bfe9774040d9f97793fb5bc742206925c31c Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Wed, 25 Oct 2023 17:52:26 +0100 Subject: [PATCH 47/52] Updated README --- README.md | 2 +- demo/showcase/list.py | 8 ++++---- setup.ttkDesigner.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6038dc7e..b08397bb 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ 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 +- Cross compatible: Linux, Mac, MS Windows, HTML5 - 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 @@ -26,7 +27,6 @@ and inspired by a mix of [Qt5](https://www.riverbankcomputing.com/static/Docs/Py - Ful/Half/Zero sized Unicode characters 😎 ## 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) --- diff --git a/demo/showcase/list.py b/demo/showcase/list.py index 1015196a..f5eddea3 100755 --- a/demo/showcase/list.py +++ b/demo/showcase/list.py @@ -52,14 +52,14 @@ def demoList(root= None): rootLayout.addWidget(win5,2,4) # Single Selection List - listWidgetSingle = ttk.TTkList(parent=win1, 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, selectionMode=ttk.TTkK.MultiSelection) + 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.DragDropMode.AllowDrag) - listWidgetDrop = ttk.TTkList(parent=win5, maxWidth=40, minWidth=10, dragDropMode=ttk.TTkK.DragDropMode.AllowDrop) + 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(pos=(10,0), text="[ list1 ]",maxHeight=2) diff --git a/setup.ttkDesigner.py b/setup.ttkDesigner.py index b192a8cd..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.35.0a', + 'pyTermTk>=0.36.0a', 'pyperclip', 'Pillow'], entry_points={ From 8e04d3ace9575a4754dbe8edddd8dcdc97992637 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Thu, 26 Oct 2023 15:12:55 +0100 Subject: [PATCH 48/52] Added more info in the README --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b08397bb..f6aacd7f 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,14 @@ 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 -- Cross compatible: Linux, Mac, MS Windows, HTML5 +- 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) - 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 - 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 From bd4df2b090de9076b64e03a63a5940a7510fcc12 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Thu, 26 Oct 2023 15:22:05 +0100 Subject: [PATCH 49/52] Added stupid emoji in the readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f6aacd7f..cefea4bd 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ and inspired by a mix of [Qt5](https://www.riverbankcomputing.com/static/Docs/Py ## 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) +- 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 From e5fc190b0d30adb19880d985c7ecdf55e69bae6c Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Thu, 26 Oct 2023 22:58:09 +0100 Subject: [PATCH 50/52] Fix deadloop in windows when file picker is opened --- TermTk/TTkWidgets/TTkPickers/filepicker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From 2e667eb67fc4a60bd93892901bfc9b15c71bbb90 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Thu, 26 Oct 2023 23:02:30 +0100 Subject: [PATCH 51/52] Added pyodide version --- TermTk/TTkCore/drivers/pyodide.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/TermTk/TTkCore/drivers/pyodide.py b/TermTk/TTkCore/drivers/pyodide.py index 7e383849..de482624 100644 --- a/TermTk/TTkCore/drivers/pyodide.py +++ b/TermTk/TTkCore/drivers/pyodide.py @@ -22,7 +22,10 @@ __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 @@ -36,5 +39,6 @@ class TTkSignalDriver(): sigInt = pyTTkSignal() @staticmethod - def init(): pass + def init(): + TTkLog.info(f"Pyodide Version:\033[38;5;11m{pyodideVersion}") def exit(): pass \ No newline at end of file From f43934ce3f9f84c1fd4ef4f8d90481ff477cd422 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Thu, 26 Oct 2023 23:03:02 +0100 Subject: [PATCH 52/52] Removed regex warnings --- TermTk/TTkGui/textcursor.py | 2 +- TermTk/TTkWidgets/TTkModelView/filetreewidgetitem.py | 2 +- TermTk/TTkWidgets/lineedit.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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/lineedit.py b/TermTk/TTkWidgets/lineedit.py index 5d039a36..e6aedcb4 100644 --- a/TermTk/TTkWidgets/lineedit.py +++ b/TermTk/TTkWidgets/lineedit.py @@ -157,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))