diff --git a/TermTk/TTkCore/constant.py b/TermTk/TTkCore/constant.py index ac69221e..0b41ea23 100644 --- a/TermTk/TTkCore/constant.py +++ b/TermTk/TTkCore/constant.py @@ -56,6 +56,8 @@ class TTkConstant: LEFT = 0x0004 RIGHT = 0x0008 CENTER = 0x0010 + HEADER = 0x0020 + FOOTER = 0x0040 # SelectionMode NoSelection = 0x00 diff --git a/TermTk/TTkTestWidgets/testwidgetsizes.py b/TermTk/TTkTestWidgets/testwidgetsizes.py index 73dec385..c12710d2 100644 --- a/TermTk/TTkTestWidgets/testwidgetsizes.py +++ b/TermTk/TTkTestWidgets/testwidgetsizes.py @@ -32,13 +32,19 @@ class TTkTestWidgetSizes(TTkFrame): TTkTestWidgetSizes.ID+=1 def paintEvent(self, canvas): - TTkFrame.paintEvent(self, canvas) t,_,l,_ = self.getPadding() - canvas.drawText(pos=(l,t+0), text=f"Test Widget [{self._name}]") - canvas.drawText(pos=(l,t+1), text=f"x,y ({self._x},{self._y})") - canvas.drawText(pos=(l,t+2), text=f"w,h ({self._width},{self._height})") - canvas.drawText(pos=(l,t+3), text=f"max w,h ({self._maxw},{self._maxh})") - canvas.drawText(pos=(l,t+4), text=f"min w,h ({self._minw},{self._minh})") + w,h = self.size() + style = self.currentStyle() + color = style['color'] + if color.background(): + canvas.fill(pos=(0,0), size=(w,h), color=color) + borderColor = style['borderColor'] + canvas.drawText(pos=(l,t+0), color=color, text=f"Test Widget [{self._name}]") + canvas.drawText(pos=(l,t+1), color=color, text=f"x,y ({self._x},{self._y})") + canvas.drawText(pos=(l,t+2), color=color, text=f"w,h ({self._width},{self._height})") + canvas.drawText(pos=(l,t+3), color=color, text=f"max w,h ({self._maxw},{self._maxh})") + canvas.drawText(pos=(l,t+4), color=color, text=f"min w,h ({self._minw},{self._minh})") + TTkFrame.paintEvent(self, canvas) def mousePressEvent(self, evt): return True def mouseReleaseEvent(self, evt): return True diff --git a/TermTk/TTkWidgets/__init__.py b/TermTk/TTkWidgets/__init__.py index 375f50cb..59070448 100644 --- a/TermTk/TTkWidgets/__init__.py +++ b/TermTk/TTkWidgets/__init__.py @@ -7,6 +7,7 @@ from .frame import * from .resizableframe import * from .window import * from .splitter import * +from .apptemplate import * # Everything else from .about import * diff --git a/TermTk/TTkWidgets/apptemplate.py b/TermTk/TTkWidgets/apptemplate.py index fab1cb20..89bf1d49 100644 --- a/TermTk/TTkWidgets/apptemplate.py +++ b/TermTk/TTkWidgets/apptemplate.py @@ -2,10 +2,11 @@ __all__ = ['TTkAppTemplate'] from dataclasses import dataclass +from TermTk.TTkCore.canvas import TTkCanvas from TermTk.TTkCore.constant import TTkK from TermTk.TTkLayouts import TTkLayout, TTkGridLayout -from TermTk.TTkWidgets.container import TTkContainer +from TermTk.TTkWidgets.container import TTkWidget, TTkContainer class TTkAppTemplate(TTkContainer): ''' TTkAppTemplate Layout sizes: @@ -15,19 +16,19 @@ class TTkAppTemplate(TTkContainer): App Template Layout ┌─────────────────────────────────┐ │ Header │ - ├─────────┬──────────────┬────────┤ A (1,2,3) + ├─────────┬──────────────┬────────┤ H (1,2,3) │ │ Top │ │ - │ ├──────────────┤ │ B + │ ├──────────────┤ │ T │ │ │ │ │ Right │ Main │ Left │ │ │ Center │ │ │ │ │ │ - │ ├──────────────┤ │ C + │ ├──────────────┤ │ B │ │ Bottom │ │ - ├─────────┴──────────────┴────────┤ D (1,2,3) + ├─────────┴──────────────┴────────┤ F (1,2,3) │ Footer │ └─────────────────────────────────┘ - E F + R L ''' MAIN = TTkK.CENTER @@ -42,17 +43,30 @@ class TTkAppTemplate(TTkContainer): @dataclass(frozen=False) class _Panel: # It's either item or widget - item = None - widget = None + item: TTkLayout = None + widget: TTkWidget = None size = 0 border = True fixed = False + def setGeometry(self,x,y,w,h): + if it := self.item: + it.setGeometry(x,y,w,h) + elif wid := self.widget: + wid.setGeometry(x,y,w,h) + def isVisible(self): if self.widget: return self.widget.isVisible() return True + def getSize(self): + if it := self.item: + return it.size() + if wid := self.widget: + return wid.size() + return (0,0) + def minimumWidth(self): if it := self.item: return it.minimumWidth() @@ -76,9 +90,9 @@ class TTkAppTemplate(TTkContainer): def maximumHeight(self): if it := self.item: - return it.maximumWidth() + return it.maximumHeight() if wid := self.widget: - return wid.maximumWidth() + return wid.maximumHeight() return 0x10000 __slots__ = ('_panels', @@ -98,15 +112,20 @@ class TTkAppTemplate(TTkContainer): self._updateGeometries() def setWidget(self, widget, location): + if not self._panels[location]: + self._panels[location] = TTkAppTemplate._Panel() self._panels[location].widget = widget if it:=self._panels[location].item: self.layout().removeItem(it) self._panels[location].item = None if widget: self.layout().addWidget(widget) + self._panels[location].size = widget.minimumWidth() if location in (TTkAppTemplate.LEFT,TTkAppTemplate.RIGHT) else widget.maximumWidth() self._updateGeometries() def setItem(self, item, location): + if not self._panels[location]: + self._panels[location] = TTkAppTemplate._Panel() self._panels[location].item = item if wid:=self._panels[location].widget: self.layout().removeWdget(wid) @@ -115,6 +134,15 @@ class TTkAppTemplate(TTkContainer): self.layout().addItem(item) self._updateGeometries() + def setBorder(self, border=True, location=MAIN): + if not self._panels[location]: + self._panels[location] = TTkAppTemplate._Panel() + self._panels[location].border = border + self._updateGeometries() + + def resizeEvent(self, w, h): + self._updateGeometries() + def minimumWidth(self): pns = self._panels @@ -147,7 +175,7 @@ class TTkAppTemplate(TTkContainer): pns = self._panels # Header and Footer sizes - mh=mf=0 + mh=mf=0x10000 if p:=pns[TTkAppTemplate.HEADER]: mh = p.maximumWidth() if p:=pns[TTkAppTemplate.FOOTER]: @@ -167,7 +195,7 @@ class TTkAppTemplate(TTkContainer): if p:=pns[TTkAppTemplate.BOTTOM]: mcb = p.maximumWidth() - mcm = (p:=pns[TTkAppTemplate.MAIN]).minimumWidth() + mcm = (p:=pns[TTkAppTemplate.MAIN]).maximumWidth() return min(mh, mf, mcr+mcl+min(mct, mcb, mcm)) + (2 if p.border else 0) @@ -229,30 +257,120 @@ class TTkAppTemplate(TTkContainer): return mh+mf+min(mcr ,mcl, mcm+mct+mcb ) + ( 2 if p.border else 0 ) - - - def _updateGeometries(self): w,h = self.size() pns = self._panels - # E,F Splitters - p = pns[TTkAppTemplate.RIGHT] - if ( not p or not p.isVisible() ): - pass - p = pns[TTkAppTemplate.LEFT] - if ( not p or not p.isVisible() ): - pass - - - - + sl=sr=st=sb=sh=sf=0 + bm=bl=br=bt=bb=bh=bf=0 + # A,B,C,D HSplitters + pt=pb=ph=pf=None + if ( (p:=pns[TTkAppTemplate.TOP]) and p.isVisible() ): pt=p ; st=p.size ; bt=1 if p.border else 0 + if ( (p:=pns[TTkAppTemplate.BOTTOM]) and p.isVisible() ): pb=p ; sb=p.size ; bb=1 if p.border else 0 + if ( (p:=pns[TTkAppTemplate.HEADER]) and p.isVisible() ): ph=p ; sh=p.size ; bh=1 if p.border else 0 + if ( (p:=pns[TTkAppTemplate.FOOTER]) and p.isVisible() ): pf=p ; sf=p.size ; bf=1 if p.border else 0 + # E,F VSplitters + pl=pr=None + if ( (p:=pns[TTkAppTemplate.LEFT]) and p.isVisible() ): pl=p ; sl=p.size ; bl=1 if p.border else 0 + if ( (p:=pns[TTkAppTemplate.RIGHT]) and p.isVisible() ): pr=p ; sr=p.size ; br=1 if p.border else 0 + + # Main Boundaries + pm=pns[TTkAppTemplate.MAIN] + mmaxw = pm.maximumWidth() + mminw = pm.minimumWidth() + mmaxh = pm.maximumHeight() + mminh = pm.minimumHeight() + bm = 1 if pns[TTkAppTemplate.MAIN].border else 0 + w-=(bm<<1)+bl+br + h-=(bm<<1)+bt+bb+bh+bf + + + # check horizontal sizes + if not (mminw <= (newszw:=(w-sl-sr)) <= mmaxw): + # the main width does not fit + # we need to move the (E,F) splitters + # * to avoid extra complexity, + # Let's resize the right panel first + # and adjust the left one to allows the + # main panel to fit again + if newszw < mminw: + if pr: pr.size = sr = max(pr.minimumWidth(), w-mminw-sl) ; newszw=w-sl-sr + if newszw < mminw and pl: pl.size = sl = max(pl.minimumWidth(), w-mminw-sr) ; newszw=w-sl-sr + else: + if pr: pr.size = sr = min(pr.maximumWidth(), w-mmaxw-sl) ; newszw=w-sl-sr + if newszw > mmaxw and pl: pl.size = sl = min(pl.maximumWidth(), w-mmaxw-sr) ; newszw=w-sl-sr + + # check vertical sizes + if not (mminh <= (newszh:=(h-st-sb-sh-sf)) <= mmaxh): + # almost same as before except that there are 4 panels to be considered instead of 2 + if newszh < mminh: + if pf: pf.size = sf = max(pf.minimumHeight(), h-mminh-sb-st-sh) ; newszh=h-st-sb-sh-sf + if newszh < mminh and pb: pb.size = sb = max(pb.minimumHeight(), h-mminh-sf-st-sh) ; newszh=h-st-sb-sh-sf + if newszh < mminh and pt: pt.size = st = max(pt.minimumHeight(), h-mminh-sf-sb-sh) ; newszh=h-st-sb-sh-sf + if newszh < mminh and ph: ph.size = sh = max(ph.minimumHeight(), h-mminh-sf-sb-st) ; newszh=h-st-sb-sh-sf + else: + if pf: pf.size = sf = min(pf.maximumHeight(), h-mmaxh-sb-st-sh) ; newszh=h-st-sb-sh-sf + if newszh > mmaxh and pb: pb.size = sb = min(pb.maximumHeight(), h-mmaxh-sf-st-sh) ; newszh=h-st-sb-sh-sf + if newszh > mmaxh and pt: pt.size = st = min(pt.maximumHeight(), h-mmaxh-sf-sb-sh) ; newszh=h-st-sb-sh-sf + if newszh > mmaxh and ph: ph.size = sh = min(ph.maximumHeight(), h-mmaxh-sf-sb-st) ; newszh=h-st-sb-sh-sf + + # check vertical sizes + w+=bl+br + h+=bt+bb+bh+bf + pm.setGeometry( bm+sl+bl , bm+sh+bh+st+bt , newszw , newszh ) + + if pl: pl.setGeometry( bm , bm+sh+bh , sl , h-sh-bh-sf-bf ) + if pr: pr.setGeometry( bm+sl+bl+newszw+br , bm+sh+bh , sr , h-sh-bh-sf-bf ) + + if ph: ph.setGeometry( bm , bm , w , sh ) + if pt: pt.setGeometry( bm+sl+bl , bm+sh+bh , newszw , st ) + if pb: pb.setGeometry( bm+sl+bl , bm+sh+bh+st+bt+newszh+bb , newszw , sb ) + if pf: pf.setGeometry( bm , bm+sh+bh+st+bt+newszh+bb+sb+bf , w , sf ) self.update() + def update(self, repaint: bool =True, updateLayout: bool =False, updateParent: bool =False): + if updateLayout: + self._updateGeometries() + super().update(repaint=repaint,updateLayout=updateLayout,updateParent=updateParent) #def layout(self): # return self._panels[TTkAppTemplate.MAIN].item #def setLayout(self, layout): - # self._panels[TTkAppTemplate.MAIN].item = layout \ No newline at end of file + # self._panels[TTkAppTemplate.MAIN].item = layout + + def paintEvent(self, canvas: TTkCanvas): + w,h = self.size() + pns = self._panels + + sl=sr=st=sb=sh=sf=0 + bl=br=bt=bb=bh=bf=0 + + bm = 1 if pns[TTkAppTemplate.MAIN].border else 0 + smw,smh = pns[TTkAppTemplate.MAIN].getSize() + # A,B,C,D HSplitters + pt=pb=ph=pf=None + if ( (p:=pns[TTkAppTemplate.TOP]) and p.isVisible() ): pt=p ; st=p.size ; bt=1 if p.border else 0 + if ( (p:=pns[TTkAppTemplate.BOTTOM]) and p.isVisible() ): pb=p ; sb=p.size ; bb=1 if p.border else 0 + if ( (p:=pns[TTkAppTemplate.HEADER]) and p.isVisible() ): ph=p ; sh=p.size ; bh=1 if p.border else 0 + if ( (p:=pns[TTkAppTemplate.FOOTER]) and p.isVisible() ): pf=p ; sf=p.size ; bf=1 if p.border else 0 + # E,F VSplitters + pl=pr=None + if ( (p:=pns[TTkAppTemplate.LEFT]) and p.isVisible() ): pl=p ; sl=p.size ; bl=1 if p.border else 0 + if ( (p:=pns[TTkAppTemplate.RIGHT]) and p.isVisible() ): pr=p ; sr=p.size ; br=1 if p.border else 0 + + if bm: canvas.drawBox( pos= (0 , 0) , size= (w,h) ) + if bh: canvas.drawHLine(pos= (0 , bm+sh) , size= w ) + if bf: canvas.drawHLine(pos= (0 , bm+sh+bh+st+bt+smh+bb+sb) , size= w ) + + ca = sh + (bm if ph else 0 ) + cb = bm+sh+bh+st+bt+smh+bb+sb + (bf if pf else bm) + if bl: canvas.drawVLine(pos= (bm+sl , ca) , size= cb-ca ) + if br: canvas.drawVLine(pos= (bm+sl+bl+smw , ca) , size= cb-ca ) + ca = sl + (bm if pl else 0 ) + cb = bm+sl+bl+smw + (br if pr else bm) + if bt: canvas.drawHLine(pos= (ca , bm+sh+bh+st) , size= cb-ca ) + if bb: canvas.drawHLine(pos= (ca , bm+sh+bh+st+bt+smh) , size= cb-ca ) + + return super().paintEvent(canvas) \ No newline at end of file diff --git a/demo/showcase/apptemplate.py b/demo/showcase/apptemplate.py new file mode 100755 index 00000000..18438787 --- /dev/null +++ b/demo/showcase/apptemplate.py @@ -0,0 +1,106 @@ +#!/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 +from random import randint + +sys.path.append(os.path.join(sys.path[0],'../..')) +import TermTk as ttk + + +class AppTestWidget(ttk.TTkContainer): + def __init__(self, at, wids, **kwargs): + super().__init__(**kwargs) + self._at = at + self._wids = wids + + self.layout().addWidget(ttk.TTkLabel(pos=(1,0),text="v------< Border")) + self.layout().addWidget(ttk.TTkLabel(pos=(1,1),text=" v---< Fixed")) + self.layout().addWidget(ttk.TTkLabel(pos=(1,2),text=" <---< Main")) + self.layout().addWidget(cbbm:=ttk.TTkCheckbox(pos=(0,2),size=(3,1), checked=True)) + cbbm.clicked.connect(lambda b: at.setBorder(b)) + + for y,wn in enumerate(wids,3): + wid = wids[wn]['wid'] + style = wid.style() + # Choose a random color for the background + h,s,l = randint(0,359),100,randint(60,80) + r,g,b = ttk.TTkColor.hsl2rgb(((h+5)%360,s,l)) + style['default']['color'] = ttk.TTkColor.fg('#000000')+ttk.TTkColor.bg(f"#{r:02X}{g:02X}{b:02X}", modifier=ttk.TTkColorGradient(increment=+2)) + wid.setStyle(style) + self.layout().addWidget(cbw:=ttk.TTkCheckbox(pos=(7,y),size=(10,1),checked=True,text=wn)) + self.layout().addWidget(cbb:=ttk.TTkCheckbox(pos=(0,y),size=(3,1), checked=True)) + self.layout().addWidget(cbf:=ttk.TTkCheckbox(pos=(3,y),size=(3,1), checked=False)) + cbw.clicked.connect(wid.setVisible) + cbb.clicked.connect(lambda b,loc=wids[wn]['loc']: at.setBorder(b,loc)) + +def demoAppTemplate(root=None): + at = ttk.TTkAppTemplate(parent=root) + + twl = ttk.TTkTestWidgetSizes(border=False, name="Left", minSize=( 15, 5), maxSize=( 50, 25)) + twr = ttk.TTkTestWidgetSizes(border=False, name="Right", minSize=( 15, 5), maxSize=( 50, 25)) + twh = ttk.TTkTestWidgetSizes(border=False, name="Header", minSize=( 15, 3), maxSize=(160, 7)) + twt = ttk.TTkTestWidgetSizes(border=False, name="Top", minSize=( 15, 3), maxSize=(100, 7)) + twb = ttk.TTkTestWidgetSizes(border=False, name="Bottom", minSize=( 15, 3), maxSize=(100, 7)) + twf = ttk.TTkTestWidgetSizes(border=False, name="Footer", minSize=( 15, 3), maxSize=(160, 7)) + + twm = AppTestWidget( + at = at, + wids={ + "Header" : {'wid': twh, 'loc':at.HEADER}, + "Footer" : {'wid': twf, 'loc':at.FOOTER}, + "Top" : {'wid': twt, 'loc':at.TOP}, + "Bottom" : {'wid': twb, 'loc':at.BOTTOM}, + "Right" : {'wid': twr, 'loc':at.RIGHT}, + "Left" : {'wid': twl, 'loc':at.LEFT}}, + minSize=( 15, 5), maxSize=( 50, 10)) + + at.setWidget(twm, at.MAIN) + at.setWidget(twl, at.LEFT) + at.setWidget(twr, at.RIGHT) + at.setWidget(twh, at.HEADER) + at.setWidget(twt, at.TOP) + at.setWidget(twb, at.BOTTOM) + at.setWidget(twf, at.FOOTER) + + return at + + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-f', help='Full Screen', action='store_true') + args = parser.parse_args() + + root = ttk.TTk() + if args.f: + rootAppTemplate = root + root.setLayout(ttk.TTkGridLayout()) + else: + rootAppTemplate = ttk.TTkWindow(parent=root,pos = (0,0), size=(100,40), title="Test AppTemplate", border=True, layout=ttk.TTkGridLayout()) + demoAppTemplate(rootAppTemplate) + root.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file