From c9491dd8ab8a9311e116dc1e917b6740f0dfca63 Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Sun, 6 Nov 2022 21:41:58 +0000 Subject: [PATCH] Added TTkAbstractScrollViewGridLayout --- TermTk/TTkAbstract/__init__.py | 5 +- TermTk/TTkAbstract/abstractscrollarea.py | 14 ++- TermTk/TTkAbstract/abstractscrollview.py | 109 ++++++++++++++++--- TermTk/TTkLayouts/layout.py | 8 +- TermTk/TTkTestWidgets/__init__.py | 7 +- TermTk/TTkTestWidgets/testabstractscroll.py | 66 +++++++++++ TermTk/TTkWidgets/TTkModelView/treewidget.py | 2 +- tests/test.metaclass.001.py | 71 ++++++++++++ tests/test.ui.021.abstractscroll.01.py | 59 ++++++++++ tests/test.ui.021.abstractscroll.02.py | 66 +++++++++++ 10 files changed, 377 insertions(+), 30 deletions(-) create mode 100644 TermTk/TTkTestWidgets/testabstractscroll.py create mode 100755 tests/test.metaclass.001.py create mode 100755 tests/test.ui.021.abstractscroll.01.py create mode 100755 tests/test.ui.021.abstractscroll.02.py diff --git a/TermTk/TTkAbstract/__init__.py b/TermTk/TTkAbstract/__init__.py index efa544d5..3137a2e9 100644 --- a/TermTk/TTkAbstract/__init__.py +++ b/TermTk/TTkAbstract/__init__.py @@ -1,2 +1,3 @@ -from .abstractscrollarea import * -from .abstractitemmodel import * +from .abstractscrollview import TTkAbstractScrollViewInterface, TTkAbstractScrollView, TTkAbstractScrollViewGridLayout +from .abstractscrollarea import TTkAbstractScrollArea +from .abstractitemmodel import TTkAbstractItemModel diff --git a/TermTk/TTkAbstract/abstractscrollarea.py b/TermTk/TTkAbstract/abstractscrollarea.py index 0be62e69..693d15d2 100644 --- a/TermTk/TTkAbstract/abstractscrollarea.py +++ b/TermTk/TTkAbstract/abstractscrollarea.py @@ -28,7 +28,7 @@ from TermTk.TTkCore.signal import pyTTkSlot from TermTk.TTkWidgets.widget import TTkWidget from TermTk.TTkWidgets.scrollbar import TTkScrollBar from TermTk.TTkLayouts.gridlayout import TTkGridLayout -from TermTk.TTkAbstract.abstractscrollview import TTkAbstractScrollView +from TermTk.TTkAbstract.abstractscrollview import TTkAbstractScrollViewInterface class TTkAbstractScrollArea(TTkWidget): __slots__ = ( @@ -39,7 +39,6 @@ class TTkAbstractScrollArea(TTkWidget): def __init__(self, *args, **kwargs): self._viewport = None super().__init__(*args, **kwargs) - self._name = kwargs.get('name' , 'TTkAbstractScrollArea') self.setLayout(TTkGridLayout()) self._verticalScrollBar = TTkScrollBar(orientation=TTkK.VERTICAL) self._horizontalScrollBar = TTkScrollBar(orientation=TTkK.HORIZONTAL) @@ -99,13 +98,18 @@ class TTkAbstractScrollArea(TTkWidget): self._viewport.viewMoveTo(val, oy) def setViewport(self, viewport): - if not isinstance(viewport, TTkAbstractScrollView): - raise TypeError("TTkAbstractScrollView is required in TTkAbstractScrollArea.setVewport(viewport)") + if not isinstance(viewport, TTkAbstractScrollViewInterface): + raise TypeError("TTkAbstractScrollViewInterface is required in TTkAbstractScrollArea.setVewport(viewport)") self._viewport = viewport self._viewport.viewChanged.connect(self._viewportChanged) self._verticalScrollBar.sliderMoved.connect(self._vscrollMoved) self._horizontalScrollBar.sliderMoved.connect(self._hscrollMoved) - self.layout().addWidget(viewport,0,0) + # TODO: Remove this check once + # unified "addWidget" and "addItem" in the TTKGridLayout + if isinstance(viewport, TTkWidget): + self.layout().addWidget(viewport,0,0) + else: + self.layout().addItem(viewport,0,0) self.layout().addWidget(self._verticalScrollBar,0,1) self.layout().addWidget(self._horizontalScrollBar,1,0) diff --git a/TermTk/TTkAbstract/abstractscrollview.py b/TermTk/TTkAbstract/abstractscrollview.py index ea04e513..2ff6da8c 100644 --- a/TermTk/TTkAbstract/abstractscrollview.py +++ b/TermTk/TTkAbstract/abstractscrollview.py @@ -23,11 +23,28 @@ # SOFTWARE. from TermTk.TTkCore.constant import TTkK -from TermTk.TTkCore.cfg import * +from TermTk.TTkCore.cfg import TTkCfg from TermTk.TTkCore.signal import pyTTkSlot, pyTTkSignal from TermTk.TTkWidgets.widget import TTkWidget +from TermTk.TTkLayouts.gridlayout import TTkGridLayout -class TTkAbstractScrollView(TTkWidget): +class TTkAbstractScrollViewInterface(): + # Override this function + def viewFullAreaSize(self) -> (int, int): + raise NotImplementedError() + + # Override this function + def viewDisplayedSize(self) -> (int, int): + raise NotImplementedError() + + @pyTTkSlot(int, int) + def viewMoveTo(self, x, y): + raise NotImplementedError() + + def getViewOffsets(self): + return self._viewOffsetX, self._viewOffsetY + +class TTkAbstractScrollView(TTkWidget, TTkAbstractScrollViewInterface): __slots__ = ( '_viewOffsetX', '_viewOffsetY', # Signals @@ -38,23 +55,13 @@ class TTkAbstractScrollView(TTkWidget): self.viewMovedTo = pyTTkSignal(int, int) # x, y self.viewSizeChanged = pyTTkSignal(int, int) # w, h self.viewChanged = pyTTkSignal() - super().__init__(*args, **kwargs) - self._name = kwargs.get('name' , 'TTkAbstractScrollView') - + TTkWidget.__init__(self, *args, **kwargs) self._viewOffsetX = 0 self._viewOffsetY = 0 - # Override this function - def viewFullAreaSize(self) -> (int, int): - raise NotImplementedError() - - # Override this function - def viewDisplayedSize(self) -> (int, int): - raise NotImplementedError() - @pyTTkSlot(int, int) def viewMoveTo(self, x, y): - fw, fh = self.viewFullAreaSize() + fw, fh = self.viewFullAreaSize() dw, dh = self.viewDisplayedSize() rangex = fw - dw rangey = fh - dh @@ -84,6 +91,76 @@ class TTkAbstractScrollView(TTkWidget): self.viewSizeChanged.emit(w,h) self.viewChanged.emit() - def getViewOffsets(self): - return self._viewOffsetX, self._viewOffsetY +class TTkAbstractScrollViewGridLayout(TTkGridLayout, TTkAbstractScrollViewInterface): + __slots__ = ( + '_viewOffsetX', '_viewOffsetY', + # Signals + 'viewMovedTo', 'viewSizeChanged', 'viewChanged') + + def __init__(self, *args, **kwargs): + # Signals + self.viewMovedTo = pyTTkSignal(int, int) # x, y + self.viewSizeChanged = pyTTkSignal(int, int) # w, h + self.viewChanged = pyTTkSignal() + TTkGridLayout.__init__(self, *args, **kwargs) + self._viewOffsetX = 0 + self._viewOffsetY = 0 + + @pyTTkSlot(int, int) + def viewMoveTo(self, x, y): + fw, fh = self.viewFullAreaSize() + dw, dh = self.viewDisplayedSize() + rangex = fw - dw + rangey = fh - dh + # TTkLog.debug(f"x:{x},y:{y}, full:{fw,fh}, display:{dw,dh}, range:{rangex,rangey}") + x = max(0,min(rangex,x)) + y = max(0,min(rangey,y)) + # TTkLog.debug(f"x:{x},y:{y}, wo:{self._viewOffsetX,self._viewOffsetY}") + if self._viewOffsetX == x and \ + self._viewOffsetY == y: # Nothong to do + return + for widget in self.iterWidgets(): + widget.viewMoveTo(x,y) + self._viewOffsetX = x + self._viewOffsetY = y + self.viewMovedTo.emit(x,y) + self.viewChanged.emit() + self.update() + + def setGeometry(self, x, y, w, h): + TTkGridLayout.setGeometry(self, x, y, w, h) + self.viewChanged.emit() + + def _viewChanged(self): + pass + #self.viewChanged.emit() + + def addWidget(self, widget, row=None, col=None, rowspan=1, colspan=1): + if not issubclass(type(widget),TTkAbstractScrollViewInterface): + raise TypeError("TTkAbstractScrollViewInterface is required in TTkAbstractScrollViewGridLayout.addWidget(...)") + widget.viewChanged.connect(self._viewChanged) + return TTkGridLayout.addWidget(self, widget, row, col, rowspan, colspan) + + def addItem(self, item, row=None, col=None, rowspan=1, colspan=1): + if not issubclass(type(item),TTkAbstractScrollViewInterface): + raise TypeError("TTkAbstractScrollViewInterface is required in TTkAbstractScrollViewGridLayout.addItem(...)") + return TTkGridLayout.addItem(self, item, row, col, rowspan, colspan) + + # Override this function + def viewFullAreaSize(self) -> (int, int): + w,h=0,0 + for widget in self.iterWidgets(): + ww,wh = widget.viewFullAreaSize() + w = max(w,ww) + h = max(h,wh) + return w,h + + # Override this function + def viewDisplayedSize(self) -> (int, int): + w,h=0,0 + for widget in self.iterWidgets(): + ww,wh = widget.viewDisplayedSize() + w = max(w,ww) + h = max(h,wh) + return w,h diff --git a/TermTk/TTkLayouts/layout.py b/TermTk/TTkLayouts/layout.py index a31e9a0b..4bbb8682 100644 --- a/TermTk/TTkLayouts/layout.py +++ b/TermTk/TTkLayouts/layout.py @@ -220,6 +220,10 @@ class TTkLayout(TTkLayoutItem): self.insertItem(len(self._items),item) def insertItem(self, index, item): + if not issubclass(type(widget := item), TTkLayoutItem): + if widget.parentWidget() and widget.parentWidget().layout(): + widget.parentWidget().layout().removeWidget(self) + item = widget.widgetItem() self._items.insert(index, item) self._zSortItems() self.update() @@ -238,9 +242,7 @@ class TTkLayout(TTkLayoutItem): self.insertWidget(len(self._items),widget) def insertWidget(self, index, widget): - if widget.parentWidget() and widget.parentWidget().layout(): - widget.parentWidget().layout().removeWidget(self) - self.insertItem(index, widget.widgetItem()) + self.insertItem(index, widget) def removeItem(self, item): if item in self._items: diff --git a/TermTk/TTkTestWidgets/__init__.py b/TermTk/TTkTestWidgets/__init__.py index c32c6a65..dd897322 100644 --- a/TermTk/TTkTestWidgets/__init__.py +++ b/TermTk/TTkTestWidgets/__init__.py @@ -1,4 +1,5 @@ -from .logviewer import * -from .testwidget import * -from .testwidgetsizes import * +from .logviewer import TTkLogViewer +from .testwidget import TTkTestWidget +from .testwidgetsizes import TTkTestWidgetSizes +from .testabstractscroll import TTkTestAbstractScrollWidget from .keypressview import TTkKeyPressView diff --git a/TermTk/TTkTestWidgets/testabstractscroll.py b/TermTk/TTkTestWidgets/testabstractscroll.py new file mode 100644 index 00000000..a94b4c54 --- /dev/null +++ b/TermTk/TTkTestWidgets/testabstractscroll.py @@ -0,0 +1,66 @@ +#!/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. + +from TermTk.TTkCore.signal import pyTTkSlot +from TermTk.TTkWidgets.frame import TTkFrame +from TermTk.TTkAbstract.abstractscrollview import TTkAbstractScrollView + +class TTkTestAbstractScrollWidget(TTkAbstractScrollView): + ID = 1 + __slots__ = ('_areaPos','_areaSize') + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._name = kwargs.get('name' , f"TTkTestAbstractScrollWidget-{TTkTestAbstractScrollWidget.ID}" ) + self._areaSize = kwargs.get('areaSize',(10,10)) + self._areaPos = kwargs.get('areaPos',(0,0)) + TTkTestAbstractScrollWidget.ID+=1 + self.viewChanged.connect(self._viewChangedHandler) + + @pyTTkSlot() + def _viewChangedHandler(self): + self._areaPos = self.getViewOffsets() + self.update() + + def paintEvent(self): + self._canvas.drawBox(pos=(0,0),size=(self._width,self._height)) + t,_,l,_ = self.getPadding() + self._canvas.drawText(pos=(l+1,t+1+0), text=f"Test Widget [{self._name}]") + self._canvas.drawText(pos=(l+1,t+1+1), text=f"x,y ({self._x},{self._y})") + self._canvas.drawText(pos=(l+1,t+1+2), text=f"w,h ({self._width},{self._height})") + self._canvas.drawText(pos=(l+1,t+1+3), text=f"max w,h ({self._maxw},{self._maxh})") + self._canvas.drawText(pos=(l+1,t+1+4), text=f"min w,h ({self._minw},{self._minh})") + self._canvas.drawText(pos=(l+1,t+1+5), text=f"areaSize {self._areaSize}") + self._canvas.drawText(pos=(l+1,t+1+6), text=f"areaPos1 {self._areaPos}") + self._canvas.drawText(pos=(l+1,t+1+7), text=f"areaPos2 ({self._areaPos[0]+self._width},{self._areaPos[1]+self._height})") + + def mousePressEvent(self, evt): return True + def mouseReleaseEvent(self, evt): return True + + def viewFullAreaSize(self) -> (int, int): + return self._areaSize + + def viewDisplayedSize(self) -> (int, int): + return self.size() + + diff --git a/TermTk/TTkWidgets/TTkModelView/treewidget.py b/TermTk/TTkWidgets/TTkModelView/treewidget.py index 9eae056c..51ce5336 100644 --- a/TermTk/TTkWidgets/TTkModelView/treewidget.py +++ b/TermTk/TTkWidgets/TTkModelView/treewidget.py @@ -25,7 +25,7 @@ from TermTk.TTkCore.cfg import TTkCfg from TermTk.TTkCore.constant import TTkK from TermTk.TTkWidgets.TTkModelView.treewidgetitem import TTkTreeWidgetItem -from TermTk.TTkAbstract.abstractscrollarea import TTkAbstractScrollView +from TermTk.TTkAbstract.abstractscrollview import TTkAbstractScrollView from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot from dataclasses import dataclass diff --git a/tests/test.metaclass.001.py b/tests/test.metaclass.001.py new file mode 100755 index 00000000..09137aa8 --- /dev/null +++ b/tests/test.metaclass.001.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 + +# 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 A_Meta(type): + def __new__(mcs, name, bases, d): + print(f"{mcs=}") + print(f"{name=}") + print(f"{bases=}") + print(f"{d=}") + return type.__new__(mcs, name, bases, d) + +class A(metaclass=A_Meta): + def __init__(self, *args, **kwargs): + pass + def test(self): + pass + +print(f"{A=}") + +a = A(1,2,3,4) + +print(f"{a=}\n") + +class B(A_Meta): + def __init__(self, *args, **kwargs): + pass + def test(self): + pass + +b = B("NB",(),{}) + +print(f"{b=}\n") + +class C(): + def __init__(self) -> None: + print(f"C {type(self)=}") +class D(): + def __init__(self) -> None: + print(f"D {type(self)=}") +class E(C,D): + def __init__(self) -> None: + print(f"{super()=}") + super().__init__() + def pippo(self): + print(f"{super()=}") + +e = E() +e.pippo() + +print(f"{issubclass(E,D)=} {issubclass(E,C)=}") \ No newline at end of file diff --git a/tests/test.ui.021.abstractscroll.01.py b/tests/test.ui.021.abstractscroll.01.py new file mode 100755 index 00000000..6ac08a55 --- /dev/null +++ b/tests/test.ui.021.abstractscroll.01.py @@ -0,0 +1,59 @@ +#!/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 + +class ScrollAreaTest(ttk.TTkAbstractScrollArea): + __slots__ = ('_areaView') + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if 'parent' in kwargs: kwargs.pop('parent') + self.setFocusPolicy(ttk.TTkK.ClickFocus) + self.setViewport(ttk.TTkTestAbstractScrollWidget(areaSize=(100,40), areaPos=(10,5))) + +def demoScrollArea(root= None): + scrollArea = ScrollAreaTest(parent=root) + return scrollArea + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-f', help='Full Screen', action='store_true') + args = parser.parse_args() + + ttk.TTkLog.use_default_file_logging() + + root = ttk.TTk() + if args.f: + rootGraph = root + root.setLayout(ttk.TTkGridLayout()) + else: + rootGraph = ttk.TTkWindow(parent=root,pos=(1,1), size=(50,20), title="Test Graph", border=True, layout=ttk.TTkGridLayout()) + demoScrollArea(rootGraph) + root.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/test.ui.021.abstractscroll.02.py b/tests/test.ui.021.abstractscroll.02.py new file mode 100755 index 00000000..5cd51654 --- /dev/null +++ b/tests/test.ui.021.abstractscroll.02.py @@ -0,0 +1,66 @@ +#!/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 + +class ScrollAreaTest(ttk.TTkAbstractScrollArea): + __slots__ = ('_areaView') + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if 'parent' in kwargs: kwargs.pop('parent') + self.setFocusPolicy(ttk.TTkK.ClickFocus) + scrollLayout = ttk.TTkAbstractScrollViewGridLayout() + w1 = ttk.TTkTestAbstractScrollWidget(areaSize=(100,40), areaPos=(10,5)) + w2 = ttk.TTkTestAbstractScrollWidget(areaSize=(100,40), areaPos=(10,5), maxWidth=30) + w3 = ttk.TTkTestAbstractScrollWidget(areaSize=( 50,20), areaPos=(10,5)) + scrollLayout.addWidget(w1,0,0,2,1) + scrollLayout.addWidget(w2,0,1) + scrollLayout.addWidget(w3,1,1) + self.setViewport(scrollLayout) + +def demoScrollArea(root= None): + scrollArea = ScrollAreaTest(parent=root) + return scrollArea + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-f', help='Full Screen', action='store_true') + args = parser.parse_args() + + ttk.TTkLog.use_default_file_logging() + + root = ttk.TTk() + if args.f: + rootGraph = root + root.setLayout(ttk.TTkGridLayout()) + else: + rootGraph = ttk.TTkWindow(parent=root,pos=(1,1), size=(55,20), title="Test Graph", border=True, layout=ttk.TTkGridLayout()) + demoScrollArea(rootGraph) + root.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file