Code archives/Miscellaneous/Foldable Panel Gadget
This code has been declared by its author to be Public Domain code.
Download source code
| |||||
Use Proxygadget class to create : - TCollapser = an extended panel that accept any kind of gadget, and update Y position automatically when updated - TCollapserItem = a special item composed from a titlebar (with text) a button on the left (or the right is COLLAPSEITEM_RIGHT is specified in the style parameter) and a body (the client area) hitting the button allows to collapse/expand the client CollaserItem can only be used in a TCollapser gadget. while a TCollapser can contain any gadget. All gadgets are stylized with minimal skin (on windows platform -> I don't know how it reacts on macos or linux ... it's to be tested) Collapsed/Expanded gadgets emit a EVENT_GADGETACTION with data = 0 or 1 (0 = gadget has been collapsed / 1 = gadget has been expanded) - HOTFIX 1 - * Add support for automatic Scrollbar when gadgets inside a collapser use more space than the client height * hotfix : fixed a small issue while collapsing an item that didn't fit the client height, the last item was wrapped on the top of the collapser - add a "RedrawGadget Self" on TCollapser.update fixed the problem. - HOTFIX 2 - * Add a macos specific value for scroll_width to fix the size that was too small. - HOTFIX 3 - * fixed the knob size of the scroll bar. Now it fits perfectly the required size. - HOTFIX 4 - * Add CollapserClient and CollapsableClient function to retreive respectively TCollapser client panel and TCollapserItem client panel. (those are the panels on which we can add stuff) - HOTFIX 5 - * modified the return type of CreateCollapsable:TCollapserItem (was returning a TGadget) There was no error, but it can be more usefull to actually return the TCollapserItem instance. - HOTFIX 6 - * fixed the y position of the client whithout scrollbar enabled. > removed the self.title_height from the position. (don't know where I was headed ... it had nothing to do whith the position of the collapser client -_-) | |||||
SuperStrict Import maxgui.drivers Const COLLAPSEITEM_RIGHT:Int = 8192 Type TCollapserItem Extends TProxyGadget Field core:TGadget; Field bt:TGadget; Field title:TGadget; Field realheight:Int; Field client:TGadget; Field _style:Int; Field title_height:Int; Field _pix:TPixmap[]; Field _state:Byte; Field _on:Byte; '#region private (internal) stuff Method CleanUp :Int () If Self.getCollapser()<>Null Then Self.getCollapser().RemoveGadget(Self); Return Self.proxy.CleanUp(); End Method ' update the pixmap icon of the top button Method updatePixmap() Self.bt .SetPixmap ( Self._pix[Self._on*2+Self._state], PANELPIXMAP_STRETCH ); End Method ' when mouse enter the button Method onEnter() Self._on = 1; Self.updatePixmap ( ); End Method ' when mouse leave the button Method onLeave() Self._on = 0; Self.updatePixmap ( ); End Method Method updateShape() If Self._state Self.core.SetShape(Self.xpos,Self.ypos,Self.width,Self.realheight); Self.client.SetShape(1,Self.title_height,Self.width-2,Self.realheight-Self.title_height-1); Self.client.SetShow(True); Else Self.core.SetShape(Self.xpos,Self.ypos,Self.width,Self.title_height); Self.client.SetShape(1,Self.title_height,Self.width-2,Self.realheight-Self.title_height-1); Self.client.SetShow(False); EndIf; If ( (Self._style & COLLAPSEITEM_RIGHT) > 0 ) ' title is left / button is right Self.title .SetShape ( 1, 1, Self.width-Self.title_height-1, Self.title_height-2 ); Self.bt .SetShape ( Self.width-Self.title_height+1, 1, Self.title_height-2, Self.title_height-2 ); Else ' default : button is left Self.bt .SetShape ( 1,1,Self.title_height-2,Self.title_height-2 ); Self.title .SetShape ( Self.title_height, 1, Self.width-Self.title_height-1, Self.title_height-2 ); EndIf End Method '#end region '#region create Method Create:TCollapserItem ( pTitle:String, pX:Int, pY:Int, pW:Int,pH:Int, pCollapser:TCollapser, pStyle:Int=0) ' only add items to a Collapser gadget ! If pCollapser = Null Then Return Null; Self.title_height= Int(Floor(pCollapser.title_height*0.5))*2+1; Self.realheight = pH; Self._state = True; Self._style = pStyle; Self.parent = pCollapser; Self._on = 0; ' generate pixmaps Local l_th:Int = Self.title_height; Local pix_h:Int = (l_th-2); Self._pix = [ CreatePixmap(pix_h,pix_h, PF_RGB888), .. CreatePixmap(pix_h,pix_h, PF_RGB888), .. CreatePixmap(pix_h,pix_h, PF_RGB888), .. CreatePixmap(pix_h,pix_h, PF_RGB888)]; Self._pix[0] .ClearPixels($808080); Self._pix[1] .ClearPixels($808080); Self._pix[2] .ClearPixels($A0A0A0); Self._pix[3] .ClearPixels($A0A0A0); Local pix_x:Int; Local pix_m:Int = Floor(pix_h*0.5); For pix_x = 3 Until pix_h-3 Self._pix[0].WritePixel(pix_x, pix_m, $010101); Self._pix[0].WritePixel(pix_m, pix_x, $010101); Self._pix[1].WritePixel(pix_x, pix_m, $010101); Self._pix[2].WritePixel(pix_x, pix_m, $010101); Self._pix[2].WritePixel(pix_m, pix_x, $010101); Self._pix[3].WritePixel(pix_x, pix_m, $010101); Next ' title panel Self.core = CreatePanel ( pX,pY,pW,ph, pCollapser.getClient(), pStyle ); Self.core .SetColor ( $01,$01,$01 ); Self.core .SetLayout ( EDGE_ALIGNED, EDGE_ALIGNED, EDGE_ALIGNED, 0 ); ' button + title If ( (pStyle & COLLAPSEITEM_RIGHT) > 0 ) ' title is left / button is right Self.title= CreateLabel ( " "+pTitle+" ", 1, 1, pw-l_th-1, l_th-2, Self.core, pStyle ); Self.bt = CreatePanel ( pw-l_th+1, 1, l_th-2, l_th-2, Self.core, PANEL_ACTIVE ); Self.bt .SetLayout ( 0, EDGE_ALIGNED, EDGE_ALIGNED, EDGE_ALIGNED ); Else ' default : button is left Self.bt = CreatePanel ( 1,1,l_th-2,l_th-2, Self.core, PANEL_ACTIVE ); Self.bt .SetLayout ( EDGE_ALIGNED, 0, EDGE_ALIGNED, EDGE_ALIGNED ); Self.title= CreateLabel ( " "+pTitle+" ", l_th, 1, pw-l_th-1, l_th-2, Self.core, pStyle ); EndIf Self.bt .SetColor ( $80,$80,$80 ); Self.title .SetColor ( $60,$60,$60 ); Self.title .SetTextColor ( $FF,$80,$20 ); Self.title .SetLayout ( EDGE_ALIGNED, EDGE_ALIGNED, EDGE_ALIGNED, EDGE_ALIGNED ); Self .updatePixmap ( ); Self.bt.extra = Self; ' client panel Self.client = CreatePanel ( 1, l_th, pw-2, ph-l_th-1, Self.core ); Self.client .SetLayout ( EDGE_ALIGNED, EDGE_ALIGNED, EDGE_ALIGNED, EDGE_ALIGNED ); Self.client .SetColor ( $40,$40,$40 ); ' set proxy Self.proxy = Self.core; ' Update layout (by Default : stick Left And Right) ' we align on top, not bottom, but whatever we set, top and bottom will be forced after collapser update ' ( and collapser update is called by addItem, so it's updated immediatly after setlayout. ) Self.SetLayout(EDGE_ALIGNED,EDGE_ALIGNED,EDGE_ALIGNED,0); pCollapser.addItem(Self); Return Self; End Method '#end region '#region text ' text is sent to the title gadget Method SetText(text:String) Self.title.SetText(text); End Method Method GetText:String() Return Self.title.GetText(); End Method '#end region '#region fold/unfold ' returns true if gadget is expanded else returns false. Method State:Int() Return Self._state; End Method ' alternates the collapsed/expanded state relatives to the previous state Method SwitchState() Self.setState(Not(Self._state)); End Method ' collapse (0) or expand (1) the gadget Method SetState(bool:Byte) If bool Self.Expand(); Else Self.Collapse(); EndIf End Method ' collapse the gadget Method Collapse() If ( Self._state = False ) Then Return; Self._state = 0; Self.Height = Self.title_height; Self .updatePixmap ( ) Self .updateShape ( ); Self.getCollapser() .Update ( ); EmitEvent ( CreateEvent ( EVENT_GADGETACTION, Self, 0 ) ); End Method ' expand the gadget Method Expand() If ( Self._state = True ) Then Return; Self._state = 1; Self.Height = Self.realheight; Self .updatePixmap ( ); Self .updateShape ( ); Self.getCollapser() .Update ( ); EmitEvent ( CreateEvent ( EVENT_GADGETACTION, Self, 1 ) ); End Method ' fold the gadget (alias for collapse) Method fold() Self.Collapse(); End Method ' unfold the gadget (alias for expand) Method unfold() Self.Expand(); End Method '#end region '#region shape ' returns the real Height the item is supposed To be (Not the collapsed Height) Method GetHeight:Int() Return Self.realheight; End Method ' the client gadget. Method getClient:TGadget() Return Self.client; End Method ' client size Method ClientWidth:Int() Return Self.client.ClientWidth() End Method Method ClientHeight:Int() Return Self.client.ClientHeight() End Method Method SetShape(pX:Int,pY:Int,pW:Int,pH:Int) ' copy shape pos and size Self.xpos = px; Self.ypos = py; Self.width = pw; Self.Height = ph; If Not(Self._state) Then Self.Height = Self.title_height; Self.realheight = ph; ' get real size of the gadget (whatever it is collapsed or not) ' the core gadget is updated after this Self.updateShape(); End Method Method SetLayout( lft:Int,rht:Int,top:Int,bot:Int ) Self.proxy.SetLayout(lft,rht,top,bot); ' copy core layout to self Self.lockl = Self.proxy.lockl; Self.lockr = Self.proxy.lockr; Self.lockt = Self.proxy.lockt; Self.lockb = Self.proxy.lockb; Self.lockx = Self.proxy.lockx; Self.locky = Self.proxy.locky; Self.lockw = Self.proxy.lockw; Self.lockh = Self.proxy.lockh; Self.lockcw = Self.proxy.lockcw; Self.lockch = Self.proxy.lockch; End Method '#end region ' the Collapser object that owns this item Method getCollapser:TCollapser() Return TCollapser(Self.parent); End Method Function gadgetCollapserItem:TCollapserItem(gad:TGadget) If gad=Null Then Return Null; If gad.GetGroup() = Null Then Return Null; ' button.parent = core Return TCollapserItem(gad.extra); End Function End Type Const COLLAPSER_AUTORESIZE:Int = 512; ' set collapser client height to match kids Const COLLAPSER_AUTOSCROLL:Int = 1024; ' add scrollbar if required Type TCollapser Extends TProxyGadget Global collapsedTime:Int; ?MacOs Global DEFAULT_SCROLLWIDTH:Int = 16; ?Not MacOs Global DEFAULT_SCROLLWIDTH:Int = 14; ? Field title_height:Int; Field scroll_size:Int; Field client:TGadget; Field scrollbarY:TGadget; Field paddingy:Int; Field items:TList; Method Create:TCollapser(pX:Int, pY:Int, pW:Int, pH:Int, pGroup:TGadget, pStyle:Int) collapsedTime = MilliSecs()-101; Self.items = New TList; Self.title_height= 17; Self.scroll_size= DEFAULT_SCROLLWIDTH; Self.paddingy = 5; Self.SetProxy ( CreatePanel(pX,pY,pW,pH,pGroup,pStyle) ); ' not implemented yet Self.scrollbarY = CreateSlider ( pw-Self.scroll_size,0,Self.scroll_size,ph, Self.proxy, SLIDER_VERTICAL | SLIDER_SCROLLBAR ); Self.scrollbarY .SetLayout ( 0,EDGE_ALIGNED, EDGE_ALIGNED,EDGE_ALIGNED ); Self.scrollbarY .SetShow ( False ); Self.client = CreatePanel ( Self.scroll_size/2,Self.title_height,pw-Self.scroll_size,ph, Self.proxy ); Self.client .SetLayout(EDGE_ALIGNED,EDGE_ALIGNED, EDGE_ALIGNED,EDGE_ALIGNED); AddHook ( EmitEventHook, EventHook, Self ); Return Self; End Method Method Rethink() Self.Update(); Super.Rethink(); End Method Function EventHook:Object(id:Int, data:Object, context:Object) If TEvent(data) = Null Then Return data; Local ev:TEvent = TEvent(data); Local col:TCollapser Local itm:TCollapserItem; Select ev.id Case EVENT_WINDOWSIZE col = TCollapser(context); If col<>Null Then col.Rethink(); Return data; Case EVENT_MOUSEENTER If TGadget(ev.source)<>Null itm = TCollapserItem.gadgetCollapserItem(TGadget(ev.source)); If (itm<>Null) Then If itm.getCollapser()=context Then itm.onEnter(); Return data; EndIf; Return data; Case EVENT_MOUSELEAVE If TGadget(ev.source)<>Null itm = TCollapserItem.gadgetCollapserItem(TGadget(ev.source)); If (itm<>Null) Then If itm.getCollapser()=context Then itm.onLeave(); Return data; EndIf; Return data; Case EVENT_MOUSEUP If (ev.data = 1) If TGadget(ev.source)<>Null itm = TCollapserItem.gadgetCollapserItem(TGadget(ev.source)); If (itm<>Null) If (itm.getCollapser()=context) If (ev.x>=0 And ev.x<itm.bt.GetWidth()) If (ev.y>=0 And ev.y<itm.bt.GetHeight()) If Abs(MilliSecs()-TCollapser.collapsedTime)>100 itm.SwitchState(); TCollapser.collapsedTime = MilliSecs(); EndIf; EndIf; EndIf; Return Null; EndIf; EndIf; EndIf; EndIf; Case EVENT_GADGETACTION If TCollapser(context)<>Null col = TCollapser(context); If ( ev.source = col.scrollbarY ) col.client.SetShape(0,-SliderValue(col.scrollbarY), col.proxy.width-col.scroll_size, col.client.Height); col._curpos = SliderValue(col.scrollbarY); RedrawGadget col; EndIf; EndIf; End Select; Return data; End Function Method CleanUp :Int () Self.proxy.CleanUp(); RemoveHook(EmitEventHook, EventHook, Null); Return Super.CleanUp(); End Method Method AddItem(item:TGadget) ' Always layout stuff with <ALIGN TOP> and <NOT ALIGNED BOTTOM> ! item.SetLayout(item.lockl,item.lockr, EDGE_ALIGNED,0); Self.items.AddLast(item); Self.items.Sort(True, compareGadgetY); Self.Update(); End Method Method RemoveItem(index:Int) If index<0 Or index>=Self.items.Count() Then Return; Self.items.Remove(Self.items.ValueAtIndex(index)); Self.Update(); End Method Method RemoveGadget:Int(gad:TGadget) Self.items.Remove(gad); Self.Update(); End Method Method GetItem:TGadget(index:Int) Return TGadget(Self.items.ValueAtIndex(index)); End Method Method GetClient:TGadget() Return Self.client; End Method Method ClientWidth:Int() Return Self.client.ClientWidth(); End Method Method ClientHeight:Int() Return Self.client.ClientHeight(); End Method Field _curpos:Int; Method Update() ' resize/repos clients ' Self.items.Sort(True, compareGadgetY) Local item:TGadget; Local posy:Int = Self.paddingy; Local gadh:Int = 0; For item = EachIn Self.items If Not(GadgetHidden(item)) If ( TCollapserItem(item)<>Null ) If TCollapserItem(item)._state gadh = TCollapserItem(item).GetHeight(); Else gadh = TCollapserItem(item).title_height; EndIf; Else gadh = item.GetHeight(); EndIf; item.SetShape ( item.GetXPos(), posy, item.GetWidth(), item.GetHeight() ); posy :+ gadh + Self.paddingy; EndIf; Next; gadh = posy; Local clh:Int = Self.proxy.Height; If ( gadh > clh ) Self.scrollbarY.SetShow(True) SetSliderRange(Self.scrollbarY, clh, gadh); SetSliderValue(Self.scrollbarY, gadh-clh); Self._curpos = min( max( Self._curpos,clh), gadh-clh) SetSliderValue(Self.scrollbarY, Self._curpos ); Self.client.SetShape(0,-Self._curpos, Self.proxy.width-Self.scroll_size, gadh); RedrawGadget Self.client; Else Self.scrollbarY.SetShow(False); Self.client.SetShape(Self.scroll_size/2,0,Self.proxy.width-Self.scroll_size, clh); EndIf; RedrawGadget Self; End Method Function compareGadgetY:Int(o1:Object, o2:Object) Return -1+2*(TGadget(o1).GetYPos()>TGadget(o2).GetYPos()); End Function End Type Function CreateCollapser:TCollapser ( pX:Int, pY:Int, pW:Int,pH:Int, pGroup:TGadget, pStyle:Int=0 ) Return New TCollapser.Create(pX,pY,pW,pH,pGroup,pStyle); End Function Function CreateCollapsable:TGadget ( pTitle:String, pX:Int, pY:Int, pW:Int, pH:Int, pCollapser:TGadget, pStyle:Int=0 ) If TCollapser(pCollapser)=Null Then Return Null; Return New TCollapserItem.Create(pTitle, pX,pY,pW,pH, TCollapser(pCollapser), pStyle); End Function Function CollapserItem:TGadget(pCollapser:TGadget, index:Int) If TCollapser(pCollapser) = Null Then Return Null; Return TCollapser(pCollapser).GetItem(index); End Function Function CollapserAddItem(pCollapser:TGadget, item:TGadget) If TCollapser(pCollapser) = Null Then Return; TCollapser(pCollapser).AddItem(item); End Function Function CollapserClient:TGadget(pCollapser:TGadget) If TCollapser(pCollapser) = Null Then Return Null; Return TCollapser(pCollapser).getClient(); End Function Function CollapsableClient:TGadget(pCollapsable:TGadget) If TCollapserItem(pCollapsable) = Null Then Return Null; Return TCollapserItem(pCollapsable).getClient(); End Function Function TCollapserDemo() Local Win:TGadget = CreateWindow("window", 10,10,800,600,, WINDOW_TITLEBAR | WINDOW_CLIENTCOORDS | WINDOW_CENTER | WINDOW_RESIZABLE | WINDOW_STATUS ); SetMinWindowSize(Win, 640,400) Local body:TGadget[2] Local a:Int; Local panI:TGadget[2]; Local killMe:TGadget[2]; For a = 0 To 1 body[a] = CreatePanel(a*ClientWidth(Win)/2+5,5,ClientWidth(Win)/2-10, ClientHeight(Win)-10,Win) body[a].SetColor($90,$90,$90); If a=0 body[a].SetLayout(EDGE_ALIGNED,EDGE_RELATIVE,EDGE_ALIGNED,EDGE_ALIGNED); Else body[a].SetLayout(EDGE_RELATIVE,EDGE_ALIGNED,EDGE_ALIGNED,EDGE_ALIGNED); EndIf; ' create a collapser Local col:TCollapser = CreateCollapser( 5,5, ClientWidth(body[a])-10, ClientHeight(body[a])-10, body[a] ); col.SetColor($20,$20,$20); col.SetLayout(EDGE_ALIGNED,EDGE_ALIGNED,EDGE_ALIGNED,EDGE_ALIGNED); ' create collapsable region in the collapser CreateCollapsable ( "Buttons" , 005,005,ClientWidth(col)-10, 100, col ); CreateCollapsable ( "Radio Buttons" , 005,110,ClientWidth(col)-10, 100, col ); CreateCollapsable ( "it's really tight" , 005,215,ClientWidth(col)-10, 200, col ); ' get collapsables from collapser Local pan0:TGadget = TCollapserItem(col.getitem(0)).getClient(); ' > Add stuff to the client ! Local pan1:TGadget = TCollapserItem(col.getitem(1)).getClient(); Local pan2:TGadget = TCollapserItem(col.getitem(2)).getClient(); Local colI:TCollapser = CreateCollapser( 5,5,ClientWidth(pan2)-10, ClientHeight(pan2)-10, pan2 ); colI.SetColor($30,$30,$30); colI.SetLayout(EDGE_ALIGNED,EDGE_ALIGNED,EDGE_ALIGNED,EDGE_ALIGNED); ' you can use "Y" position at the creation to specify order of gadgets. ' gadgets, when collapser updates, are sorted by their "Y" position. ' for example, here the "btKill" is created on top of the two regions. CreateCollapsable("let me get out i'm locked :'(", 5, 1,ClientWidth(colI)-10, 100, colI); CreateCollapsable("speak for you bro ...", 5, 2,ClientWidth(colI)-10, 100, colI); Local btKill:TGadget = CreateButton("Kill Me Please", 5, 0, 100,20, colI.GetClient() ); btKill.SetLayout(EDGE_ALIGNED,0,EDGE_ALIGNED,0); ColI.additem ( btKill ); panI[a] = colI; ' as mentioned, items are sorted, so, the collapsable regions are at index 1 and 2 Local panI0:TGadget = TCollapserItem(colI.getitem(1)).getClient(); Local panI1:TGadget = TCollapserItem(colI.getitem(2)).getClient(); ' and the button is at index 0 killMe[a] = colI.getitem(0); ' fill regions with some buttons Local i:Int, j:Int; For j = 0 To 2 For i = 0 To 2 Local b:TGadget = CreateButton("button "+i+"-"+j,5+85*i,5+25*j,80,20,pan0, BUTTON_OK); b.SetLayout(1,0,1,0); Next Next For j = 0 To 2 For i = 0 To 2 Local b:TGadget = CreateButton("radio "+i+"-"+j,5+85*i,5+25*j,80,20,pan1, BUTTON_RADIO); b.SetLayout(1,0,1,0); b.SetColor($50,$50,$50); b.SetTextColor($FF,$70,$00); Next Next For j = 0 To 2 For i = 0 To 2 Local b:TGadget = CreateButton("check "+i+"-"+j,5+85*i,5+25*j,80,20,panI0, BUTTON_CHECKBOX); b.SetLayout(1,0,1,0); b.SetColor($50,$50,$50); b.SetTextColor($FF,$70,$00); Next Next For j = 0 To 2 For i = 0 To 2 Local b:TGadget = CreateLabel("label "+i+"-"+j,5+85*i,5+25*j,80,20,panI1, LABEL_CENTER); b.SetLayout(1,0,1,0); b.SetColor($50,$50,$50); b.SetTextColor($FF,$70,$00); Next Next ' add a button in the collapser (outside collapsable regions) Local button:TGadget = CreateButton("click", 5,320, 90, 20, col.getClient() ); button.SetLayout(EDGE_ALIGNED,0,EDGE_ALIGNED,0); col.AddItem(button); Next; Repeat WaitEvent() Select EventID() Case EVENT_WINDOWCLOSE Exit; Case EVENT_GADGETACTION SetStatusText (Win, "Hit a button '"+TGadget(EventSource()).GetText()+"' state='"+TGadget(EventSource()).State()+"'") For a = 0 To 1 If killMe[a]<>Null If EventSource()=killMe[a] FreeGadget(panI[a]) killMe[a] = Null; panI[a] = Null; EndIf; EndIf; Next; ' If EventSource() = panI1 ' ' EndIf; End Select Forever End End Function TCollapserDemo() |
Comments
| ||
Nice! |
| ||
Nice. |
| ||
Very nice! |
| ||
Glad you like it :) Here is a small update (not accurate ATM) It enables automatic scrollbars when gadgets in collapser doesn't fit the gadget height (still need to fix a small issue with the width of collapser client and items) I probably should add a scrollbar on the bottom to scroll the width ... but it's a bit more complicate to implement (while not that hard ... but I'm lazzy for now) and, it's not very nice. I could just say : make inner gadgets that fit the width with a scrollbar, it's nicer look than having a scrollbar on the bottom. [edit] I updated the codes above and below : -> client is now offseted from half the scrollbar width, so when the scrollbar happens, it is just offseted to 0 without width modification, so, no issues. And BTW, I added a RedrawGadget on the TCollapser.update (I forgot it) |
| ||
Code updated, I fixed the Y position whith scrollbar disabled It was offseted by self.title_height (in TCollapser.update) while the title_height value is only to setup the CollapserItems > TCollapser has no titlebar, it's just a panel (or kind of) |
Code Archives Forum