From 9a5984c1badfed917edd11a1b60dd5f2305a63ce Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Mon, 20 Mar 2023 10:01:25 +0000 Subject: [PATCH] Finalised signal/slot editor in ttkDesigner --- TermTk/TTkUiTools/properties/checkbox.py | 6 +- TermTk/TTkUiTools/properties/widget.py | 18 +-- TermTk/TTkUiTools/uiloader.py | 39 ++++++- TermTk/TTkWidgets/widget.py | 5 + ...ignals.slots.py => 11.signals.slots.01.py} | 0 tests/timeit/11.signals.slots.02.py | 81 ++++++++++++++ tests/timeit/11.signals.slots.03.py | 103 ++++++++++++++++++ ttkDesigner/app/designer.py | 17 ++- ttkDesigner/app/signalsloteditor.py | 35 ++++++ ttkDesigner/app/windoweditor.py | 12 +- 10 files changed, 287 insertions(+), 29 deletions(-) rename tests/timeit/{11.signals.slots.py => 11.signals.slots.01.py} (100%) create mode 100644 tests/timeit/11.signals.slots.02.py create mode 100644 tests/timeit/11.signals.slots.03.py diff --git a/TermTk/TTkUiTools/properties/checkbox.py b/TermTk/TTkUiTools/properties/checkbox.py index 720c5b85..6eef7528 100644 --- a/TermTk/TTkUiTools/properties/checkbox.py +++ b/TermTk/TTkUiTools/properties/checkbox.py @@ -61,8 +61,8 @@ TTkCheckboxProperties = { 'stateChanged(CheckState)' : {'name' : 'stateChanged', 'type' : TTkK.CheckState}, }, 'slots' : { - 'setChecked(bool)' : {'cb' : TTkCheckbox.setChecked , 'type' : bool}, - 'setCheckState(CheckState)' : {'cb' : TTkCheckbox.setCheckState , 'type' : TTkK.CheckState}, - 'setText(str)' : {'cb' : TTkCheckbox.setText , 'type' : str} + 'setChecked(bool)' : {'name' : 'setChecked' , 'type' : bool}, + 'setCheckState(CheckState)' : {'name' : 'setCheckState' , 'type' : TTkK.CheckState}, + 'setText(str)' : {'name' : 'setText' , 'type' : str} } } diff --git a/TermTk/TTkUiTools/properties/widget.py b/TermTk/TTkUiTools/properties/widget.py index b80dea32..a90900b5 100644 --- a/TermTk/TTkUiTools/properties/widget.py +++ b/TermTk/TTkUiTools/properties/widget.py @@ -108,14 +108,14 @@ TTkWidgetProperties = { 'focusChanged(bool)' : {'name' : 'focusChanged', 'type':bool}, 'sizeChanged(int,int)' : {'name' : 'sizeChanged', 'type':(int, int)} },'slots' : { - 'show()' : {'cb': TTkWidget.show, 'type':None}, - 'hide()' : {'cb': TTkWidget.hide, 'type':None}, - 'close()' : {'cb': TTkWidget.close, 'type':None}, - 'setFocus()' : {'cb': TTkWidget.setFocus, 'type':None}, - 'setVisible(bool)' : {'cb': TTkWidget.setVisible, 'type':bool}, - 'setEnabled(bool)': {'cb': TTkWidget.setEnabled, 'type':bool}, - 'setDisabled(bool)': {'cb': TTkWidget.setDisabled,'type':bool}, - 'raiseWidget()' : {'cb': TTkWidget.raiseWidget,'type':None}, - 'lowerWidget()' : {'cb': TTkWidget.lowerWidget,'type':None}, + 'show()' : {'name': 'show', 'type':None}, + 'hide()' : {'name': 'hide', 'type':None}, + 'close()' : {'name': 'close', 'type':None}, + 'setFocus()' : {'name': 'setFocus', 'type':None}, + 'setVisible(bool)' : {'name': 'setVisible', 'type':bool}, + 'setEnabled(bool)': {'name': 'setEnabled', 'type':bool}, + 'setDisabled(bool)': {'name': 'setDisabled', 'type':bool}, + 'raiseWidget()' : {'name': 'raiseWidget', 'type':None}, + 'lowerWidget()' : {'name': 'lowerWidget', 'type':None}, } } diff --git a/TermTk/TTkUiTools/uiloader.py b/TermTk/TTkUiTools/uiloader.py index 355e7e7f..c00003bb 100644 --- a/TermTk/TTkUiTools/uiloader.py +++ b/TermTk/TTkUiTools/uiloader.py @@ -33,6 +33,10 @@ from TermTk.TTkUiTools.uiproperties import TTkUiProperties class TTkUiLoader(): @staticmethod def loadJson(text): + return TTkUiLoader.loadDict(json.loads(text)) + + @staticmethod + def loadDict(ui): def _getWidget(widProp): properties = {} ttkClass = globals()[widProp['class']] @@ -137,10 +141,39 @@ class TTkUiLoader(): layout.addWidget(w) return layout - widgetProperty = json.loads(text) - TTkLog.debug(widgetProperty) + TTkLog.debug(ui) + + widget = _getWidget(ui['tui']) + + def _getSignal(sender, name): + for cc in reversed(type(sender).__mro__): + if cc.__name__ in TTkUiProperties: + if not name in TTkUiProperties[cc.__name__]['signals']: + continue + signame = TTkUiProperties[cc.__name__]['signals'][name]['name'] + return getattr(sender,signame) + return None + + def _getSlot(receiver, name): + for cc in reversed(type(receiver).__mro__): + if cc.__name__ in TTkUiProperties: + if not name in TTkUiProperties[cc.__name__]['slots']: + continue + slotname = TTkUiProperties[cc.__name__]['slots'][name]['name'] + return getattr(receiver,slotname) + return None + + + for conn in ui['connections']: + sender = widget.getWidgetByName(conn['sender']) + receiver = widget.getWidgetByName(conn['receiver']) + signal = conn['signal'] + slot = conn['slot'] + if None in (sender,receiver): continue + _getSignal(sender,signal).connect(_getSlot(receiver,slot)) + + return widget - return _getWidget(widgetProperty) diff --git a/TermTk/TTkWidgets/widget.py b/TermTk/TTkWidgets/widget.py index e3ca8518..89f6ec98 100644 --- a/TermTk/TTkWidgets/widget.py +++ b/TermTk/TTkWidgets/widget.py @@ -712,3 +712,8 @@ class TTkWidget(TMouseEvents,TKeyEvents, TDragEvents): def setToolTip(self, toolTip): self._toolTip = toolTip + def getWidgetByName(self, name): + for w in self.rootLayout().iterWidgets(onlyVisible=False, recurse=True): + if w._name == name: + return w + return None diff --git a/tests/timeit/11.signals.slots.py b/tests/timeit/11.signals.slots.01.py similarity index 100% rename from tests/timeit/11.signals.slots.py rename to tests/timeit/11.signals.slots.01.py diff --git a/tests/timeit/11.signals.slots.02.py b/tests/timeit/11.signals.slots.02.py new file mode 100644 index 00000000..fd737a25 --- /dev/null +++ b/tests/timeit/11.signals.slots.02.py @@ -0,0 +1,81 @@ +#!/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 timeit +import random +import unicodedata + +sys.path.append(os.path.join(sys.path[0],'../..')) +sys.path.append(os.path.join(sys.path[0],'.')) +import TermTk as ttk + + +def f1(a): return a +def f2(a,b): return a+b +def f3(a,b,c): return a+b+c +def f4(a,b,c,d): return a+b+c+d + +def c1(*args,**argv): return f1(*args[:1]) +def c2(*args,**argv): return f2(*args[:2]) +def c3(*args,**argv): return f3(*args[:3]) +def c4(*args,**argv): return f4(*args[:4]) + +def d1(*args,**argv): return f1(*args) +def d2(*args,**argv): return f2(*args) +def d3(*args,**argv): return f3(*args) +def d4(*args,**argv): return f4(*args) + + +def test1(v): return d1(1) +def test2(v): return d2(1,2) +def test3(v): return d3(1,2,3) +def test4(v): return d4(1,2,3,4) +def test5(v): return c1(1,2,3,4) +def test6(v): return c2(1,2,3,4) +def test7(v): return c3(1,2,3,4) +def test8(v): return c4(1,2,3,4) + + +loop = 200000 + + +a=1 +result = timeit.timeit('test1(a)', globals=globals(), number=loop) +print(f"1a s {result / loop:.10f} - {result / loop} {test1(a)}") +result = timeit.timeit('test2(a)', globals=globals(), number=loop) +print(f"2a {result / loop:.10f} - {result / loop} {test2(a)}") +result = timeit.timeit('test3(a)', globals=globals(), number=loop) +print(f"3a s {result / loop:.10f} - {result / loop} {test3(a)}") +result = timeit.timeit('test4(a)', globals=globals(), number=loop) +print(f"4a {result / loop:.10f} - {result / loop} {test4(a)}") +result = timeit.timeit('test5(a)', globals=globals(), number=loop) +print(f"5a s {result / loop:.10f} - {result / loop} {test5(a)}") +result = timeit.timeit('test6(a)', globals=globals(), number=loop) +print(f"6a {result / loop:.10f} - {result / loop} {test6(a)}") +result = timeit.timeit('test7(a)', globals=globals(), number=loop) +print(f"7a s {result / loop:.10f} - {result / loop} {test7(a)}") +result = timeit.timeit('test8(a)', globals=globals(), number=loop) +print(f"8a {result / loop:.10f} - {result / loop} {test8(a)}") diff --git a/tests/timeit/11.signals.slots.03.py b/tests/timeit/11.signals.slots.03.py new file mode 100644 index 00000000..408ab28b --- /dev/null +++ b/tests/timeit/11.signals.slots.03.py @@ -0,0 +1,103 @@ +#!/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 timeit +import random +import unicodedata + +sys.path.append(os.path.join(sys.path[0],'../..')) +sys.path.append(os.path.join(sys.path[0],'.')) +import TermTk as ttk + + +def f1(a): return a +def f2(a,b): return a+b +def f3(a,b,c): return a+b+c +def f4(a,b,c,d): return a+b+c+d + +ccb = {f1:1, f2:2, f3:3, f4:4} + +def c1(*args,**argv): return f1(*args[:1]) +def c2(*args,**argv): return f2(*args[:2]) +def c3(*args,**argv): return f3(*args[:3]) +def c4(*args,**argv): return f4(*args[:4]) + +def d1(*args,**argv): return f1(*args) +def d2(*args,**argv): return f2(*args) +def d3(*args,**argv): return f3(*args) +def d4(*args,**argv): return f4(*args) + +def e1(*args,**argv): + f1(1) + f2(1,2) + f3(1,2,3) + f4(1,2,3,4) + +def e2(*args,**argv): + for cb in ccb: + nargs = ccb[cb] + cb(*args[:nargs]) + +def e3(*args,**argv): + for cb in ccb.copy(): + nargs = ccb[cb] + cb(*args[:nargs]) + +def e4(*args,**argv): + for cb,n in ccb.copy().items(): + cb(*args[:n]) + + +def test1(v): return d1(1) +def test2(v): return d2(1,2) +def test3(v): return d3(1,2,3) +def test4(v): return d4(1,2,3,4) +def test5(v): return e1(1,2,3,4) +def test6(v): return e2(1,2,3,4) +def test7(v): return e3(1,2,3,4) +def test8(v): return e4(1,2,3,4) + + +loop = 200000 + + +a=1 +result = timeit.timeit('test1(a)', globals=globals(), number=loop) +print(f"1a s {result / loop:.10f} - {result / loop} {test1(a)}") +result = timeit.timeit('test2(a)', globals=globals(), number=loop) +print(f"2a {result / loop:.10f} - {result / loop} {test2(a)}") +result = timeit.timeit('test3(a)', globals=globals(), number=loop) +print(f"3a s {result / loop:.10f} - {result / loop} {test3(a)}") +result = timeit.timeit('test4(a)', globals=globals(), number=loop) +print(f"4a {result / loop:.10f} - {result / loop} {test4(a)}") +result = timeit.timeit('test5(a)', globals=globals(), number=loop) +print(f"5a s {result / loop:.10f} - {result / loop} {test5(a)}") +result = timeit.timeit('test6(a)', globals=globals(), number=loop) +print(f"6a {result / loop:.10f} - {result / loop} {test6(a)}") +result = timeit.timeit('test7(a)', globals=globals(), number=loop) +print(f"7a s {result / loop:.10f} - {result / loop} {test7(a)}") +result = timeit.timeit('test8(a)', globals=globals(), number=loop) +print(f"8a {result / loop:.10f} - {result / loop} {test8(a)}") diff --git a/ttkDesigner/app/designer.py b/ttkDesigner/app/designer.py index 31468d4b..1b981542 100644 --- a/ttkDesigner/app/designer.py +++ b/ttkDesigner/app/designer.py @@ -20,6 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import json + from TermTk import TTk, TTkK, TTkLog, TTkCfg, TTkColor, TTkTheme, TTkTerm, TTkHelper from TermTk import TTkString from TermTk import TTkColorGradient @@ -72,7 +74,7 @@ from .signalsloteditor import SignalSlotEditor # class TTkDesigner(TTkGridLayout): - __slots__ = ('_pippo', '_main', '_windowEditor', '_toolBar') + __slots__ = ('_pippo', '_main', '_windowEditor', '_toolBar', '_sigslotEditor') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -88,10 +90,9 @@ class TTkDesigner(TTkGridLayout): # sa.viewport().layout().addWidget(WindowEditor()) self._main = TTkVBoxLayout() - self._toolBar = TTkHBoxLayout() - self._windowEditor = WindowEditor() + self._sigslotEditor = SignalSlotEditor(self._windowEditor.viewport()) self._main.addItem(self._toolBar) self._main.addWidget(self._windowEditor) @@ -100,14 +101,14 @@ class TTkDesigner(TTkGridLayout): centralSplit.addWidget(self._main) centralSplit.addWidget(bottonTabWidget := TTkTabWidget(border=False)) # centralSplit.addWidget(TTkLogViewer()) - bottonTabWidget.addTab(sigslotEditor := SignalSlotEditor(self._windowEditor.viewport()),'Signal/Slot Editor') + bottonTabWidget.addTab(self._sigslotEditor,'Signal/Slot Editor') bottonTabWidget.addTab(TTkLogViewer(),'Logs') mainSplit.addWidget(rightSplit := TTkSplitter(orientation=TTkK.VERTICAL)) rightSplit.addItem(treeInspector := TreeInspector(self._windowEditor.viewport())) rightSplit.addItem(propertyEditor := PropertyEditor()) - # rightSplit.addItem(sigslotEditor := SignalSlotEditor(self._windowEditor.viewport())) + # rightSplit.addItem(self._sigslotEditor) treeInspector.thingSelected.connect(lambda _,s : s.pushSuperControlWidget()) treeInspector.thingSelected.connect(propertyEditor.setDetail) @@ -160,9 +161,13 @@ class TTkDesigner(TTkGridLayout): SuperWidget.toggleHighlightLayout.emit(state) def preview(self, btn=None): - jj = self._windowEditor.getJson() + tui = self._windowEditor.dumpDict() + connections = self._sigslotEditor.dumpDict() # for line in jj.split('\n'): # TTkLog.debug(f"{line}") + newUI = {'tui':tui,'connections':connections} + jj = json.dumps(newUI, indent=1) + widget = TTkUiLoader.loadJson(jj) win = TTkWindow( title="Mr Terminal", diff --git a/ttkDesigner/app/signalsloteditor.py b/ttkDesigner/app/signalsloteditor.py index 7e2b5b6c..e12ba671 100644 --- a/ttkDesigner/app/signalsloteditor.py +++ b/ttkDesigner/app/signalsloteditor.py @@ -43,6 +43,35 @@ class _SignalSlotItem(ttk.TTkTreeWidgetItem): self.updateWidgets() super().__init__([self._sender,self._signal,self._receiver,self._slot], *args, **kwargs) + def isValid(self): + curSender = str(self._sender.currentText()) + curReceiver = str(self._receiver.currentText()) + curSignal = str(self._signal.currentText()) + curSlot = str(self._slot.currentText()) + if curSender=='' or curReceiver == '': + return False + ret = False + for ccName in self._signalData: + if curSignal in self._signalData[ccName]: + ret = True + break + for ccName in self._slotData: + if curSlot in self._slotData[ccName]: + ret &= True + break + return ret + + def dumpDict(self): + curSender = str(self._sender.currentText()) + curReceiver = str(self._receiver.currentText()) + curSignal = str(self._signal.currentText()) + curSlot = str(self._slot.currentText()) + return { + 'sender': curSender, + 'receiver': curReceiver, + 'signal': curSignal, + 'slot': curSlot } + def updateWidgets(self): names = [w.name() for w in self._getWidgets()] self._sender.addItems(names) @@ -173,3 +202,9 @@ class SignalSlotEditor(ttk.TTkWidget): self._items.append(item) self._detail.addTopLevelItem(item) + def dumpDict(self): + ret = [] + for i in self._items: + if i.isValid(): + ret.append(i.dumpDict()) + return ret diff --git a/ttkDesigner/app/windoweditor.py b/ttkDesigner/app/windoweditor.py index 32fd7200..7cbff6e7 100644 --- a/ttkDesigner/app/windoweditor.py +++ b/ttkDesigner/app/windoweditor.py @@ -20,10 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -# Yaml is not included by default -# import yaml -import json - from .superobj import SuperWidget import TermTk as ttk @@ -41,8 +37,8 @@ class WindowEditorView(ttk.TTkAbstractScrollView): def getTTk(self): return self._ttk - def getJson(self): - return json.dumps(self._ttk.dumpDict(), indent=1) + def dumpDict(self): + return self._ttk.dumpDict() def resizeEvent(self, w, h): self._ttk.resize(w-8,h-4) @@ -68,5 +64,5 @@ class WindowEditor(ttk.TTkAbstractScrollArea): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setViewport(wev := WindowEditorView()) - self.getTTk = wev.getTTk - self.getJson = wev.getJson + self.getTTk = wev.getTTk + self.dumpDict = wev.dumpDict