ScrolledButtonsList

Some1 asked me for this, so here it is.

__all__=['ScrolledButtonsList']

from pandac.PandaModules import * 
from direct.showbase.DirectObject import DirectObject 
from direct.gui.OnscreenText import OnscreenText 
from direct.gui.DirectGui import DirectFrame,DirectButton,DirectScrolledFrame,DGG 
from direct.interval.IntervalGlobal import Sequence 
from direct.task import Task 
from direct.showbase.PythonUtil import clampScalar 
from types import IntType 

class ScrolledButtonsList(DirectObject): 
  """ 
     A class to display a list of selectable buttons. 
     It is displayed using scrollable window (DirectScrolledFrame). 
  """ 
  def __init__(self, parent=None, frameSize=(.8,1.2), buttonTextColor=(1,1,1,1), 
               font=None, itemScale=.045, itemTextScale=1, itemTextZ=0, 
               command=None, contextMenu=None, autoFocus=0, 
               colorChange=1, colorChangeDuration=1.0, newItemColor=(0,1,0,1), 
               rolloverColor=Vec4(1,.8,.2,1), 
               suppressMouseWheel=1, modifier='control'): 
      self.focusButton=None 
      self.command=command 
      self.contextMenu=contextMenu 
      self.autoFocus=autoFocus 
      self.colorChange=colorChange 
      self.colorChangeDuration=colorChangeDuration*.5 
      self.newItemColor=Vec4(*newItemColor) 
      self.rolloverColor=Vec4(*rolloverColor) 
      self.rightClickTextColors=(Vec4(0,1,0,1),Vec4(0,35,100,1)) 
      self.font=font 
      if font: 
         self.fontHeight=font.getLineHeight() 
      else: 
         self.fontHeight=TextNode.getDefaultFont().getLineHeight() 
      self.fontHeight*=1.2 # let's enlarge font height a little 
      self.xtraSideSpace=.2*self.fontHeight 
      self.itemTextScale=itemTextScale 
      self.itemTextZ=itemTextZ 
      self.buttonTextColor=buttonTextColor 
      self.suppressMouseWheel=suppressMouseWheel 
      self.modifier=modifier 
      self.buttonsList=[] 
      self.numItems=0 
      self.__eventReceivers={} 
      # DirectScrolledFrame to hold items 
      self.itemScale=itemScale 
      self.itemVertSpacing=self.fontHeight*self.itemScale 
      self.frameWidth,self.frameHeight=frameSize 
      # I set canvas' Z size smaller than the frame to avoid the auto-generated vertical slider bar 
      self.frame = DirectScrolledFrame( 
                   parent=parent,pos=(-self.frameWidth*.5,0,.5*self.frameHeight), relief=DGG.GROOVE, 
                   state=DGG.NORMAL, # to create a mouse watcher region 
                   frameSize=(0, self.frameWidth, -self.frameHeight, 0), frameColor=(0,0,0,.7), 
                   canvasSize=(0, 0, -self.frameHeight*.5, 0), borderWidth=(0.01,0.01), 
                   manageScrollBars=0, enableEdit=0, suppressMouse=0, sortOrder=1000 ) 
      # the real canvas is "self.frame.getCanvas()", 
      # but if the frame is hidden since the beginning, 
      # no matter how I set the canvas Z pos, the transform would be resistant, 
      # so just create a new node under the canvas to be my canvas 
      self.canvas=self.frame.getCanvas().attachNewNode('myCanvas') 
      # slider background 
      SliderBG=DirectFrame( parent=self.frame,frameSize=(-.025,.025,-self.frameHeight,0), 
                   frameColor=(0,0,0,.7), pos=(-.03,0,0),enableEdit=0, suppressMouse=0) 
      # slider thumb track 
      sliderTrack = DirectFrame( parent=SliderBG, relief=DGG.FLAT, #state=DGG.NORMAL, 
                   frameColor=(1,1,1,.2), frameSize=(-.015,.015,-self.frameHeight+.01,-.01), 
                   enableEdit=0, suppressMouse=0) 
      # page up 
      self.pageUpRegion=DirectFrame( parent=SliderBG, relief=DGG.FLAT, state=DGG.NORMAL, 
                   frameColor=(1,.8,.2,.1), frameSize=(-.015,.015,0,0), 
                   enableEdit=0, suppressMouse=0) 
      self.pageUpRegion.setAlphaScale(0) 
      self.pageUpRegion.bind(DGG.B1PRESS,self.__startScrollPage,[-1]) 
      self.pageUpRegion.bind(DGG.WITHIN,self.__continueScrollUp) 
      self.pageUpRegion.bind(DGG.WITHOUT,self.__suspendScrollUp) 
      # page down 
      self.pageDnRegion=DirectFrame( parent=SliderBG, relief=DGG.FLAT, state=DGG.NORMAL, 
                   frameColor=(1,.8,.2,.1), frameSize=(-.015,.015,0,0), 
                   enableEdit=0, suppressMouse=0) 
      self.pageDnRegion.setAlphaScale(0) 
      self.pageDnRegion.bind(DGG.B1PRESS,self.__startScrollPage,[1]) 
      self.pageDnRegion.bind(DGG.WITHIN,self.__continueScrollDn) 
      self.pageDnRegion.bind(DGG.WITHOUT,self.__suspendScrollDn) 
      self.pageUpDnSuspended=[0,0] 
      # slider thumb 
      self.vertSliderThumb=DirectButton(parent=SliderBG, relief=DGG.FLAT, 
                   frameColor=(1,1,1,.6), frameSize=(-.015,.015,0,0), 
                   enableEdit=0, suppressMouse=0) 
      self.vertSliderThumb.bind(DGG.B1PRESS,self.__startdragSliderThumb) 
      self.vertSliderThumb.bind(DGG.WITHIN,self.__enteringThumb) 
      self.vertSliderThumb.bind(DGG.WITHOUT,self.__exitingThumb) 
      self.oldPrefix=base.buttonThrowers[0].node().getPrefix() 
      self.sliderThumbDragPrefix='draggingSliderThumb-' 
      # GOD & I DAMN IT !!! 
      # These things below don't work well if the canvas has a lot of buttons. 
      # So I end up checking the mouse region every frame by myself using a continuous task. 
#       self.accept(DGG.WITHIN+self.frame.guiId,self.__enteringFrame) 
#       self.accept(DGG.WITHOUT+self.frame.guiId,self.__exitingFrame) 
      self.isMouseInRegion=False 
      self.mouseOutInRegionCommand=(self.__exitingFrame,self.__enteringFrame) 
      taskMgr.doMethodLater(.2,self.__getFrameRegion,'getFrameRegion') 

  def __getFrameRegion(self,t): 
      for g in range(base.mouseWatcherNode.getNumGroups()): 
          region=base.mouseWatcherNode.getGroup(g).findRegion(self.frame.guiId) 
          if region!=None: 
             self.frameRegion=region 
             taskMgr.add(self.__mouseInRegionCheck,'mouseInRegionCheck') 
             break 

  def __mouseInRegionCheck(self,t): 
      """ 
         check if the mouse is within or without the scrollable frame, and 
         upon within or without, run the provided command 
      """ 
      if not base.mouseWatcherNode.hasMouse(): return Task.cont 
      m=base.mouseWatcherNode.getMouse() 
      bounds=self.frameRegion.getFrame() 
      inRegion=bounds[0]<m[0]<bounds[1] and bounds[2]<m[1]<bounds[3] 
      if self.isMouseInRegion==inRegion: return Task.cont 
      self.isMouseInRegion=inRegion 
      self.mouseOutInRegionCommand[inRegion]() 
      return Task.cont 

  def __startdragSliderThumb(self,m=None): 
      mpos=base.mouseWatcherNode.getMouse() 
      parentZ=self.vertSliderThumb.getParent().getZ(render2d) 
      sliderDragTask=taskMgr.add(self.__dragSliderThumb,'dragSliderThumb') 
      sliderDragTask.ZposNoffset=mpos[1]-self.vertSliderThumb.getZ(render2d)+parentZ 
#       sliderDragTask.mouseX=base.winList[0].getPointer(0).getX() 
      self.oldPrefix=base.buttonThrowers[0].node().getPrefix() 
      base.buttonThrowers[0].node().setPrefix(self.sliderThumbDragPrefix) 
      self.acceptOnce(self.sliderThumbDragPrefix+'mouse1-up',self.__stopdragSliderThumb) 

  def __dragSliderThumb(self,t): 
      if not base.mouseWatcherNode.hasMouse(): 
         return 
      mpos=base.mouseWatcherNode.getMouse() 
#       newY=base.winList[0].getPointer(0).getY() 
      self.__updateCanvasZpos((t.ZposNoffset-mpos[1])/self.canvasRatio) 
#       base.winList[0].movePointer(0, t.mouseX, newY) 
      return Task.cont 

  def __stopdragSliderThumb(self,m=None): 
      taskMgr.remove('dragSliderThumb') 
      self.__stopScrollPage() 
      base.buttonThrowers[0].node().setPrefix(self.oldPrefix) 
      if self.isMouseInRegion: 
         self.mouseOutInRegionCommand[self.isMouseInRegion]() 

  def __startScrollPage(self,dir,m): 
      self.oldPrefix=base.buttonThrowers[0].node().getPrefix() 
      base.buttonThrowers[0].node().setPrefix(self.sliderThumbDragPrefix) 
      self.acceptOnce(self.sliderThumbDragPrefix+'mouse1-up',self.__stopdragSliderThumb) 
      t=taskMgr.add(self.__scrollPage,'scrollPage',extraArgs=[int((dir+1)*.5),dir*.01/self.canvasRatio]) 
      self.pageUpDnSuspended=[0,0] 

  def __scrollPage(self,dir,scroll): 
      if not self.pageUpDnSuspended[dir]: 
         self.__scrollCanvas(scroll) 
      return Task.cont 

  def __stopScrollPage(self,m=None): 
      taskMgr.remove('scrollPage') 

  def __suspendScrollUp(self,m=None): 
      self.pageUpRegion.setAlphaScale(0) 
      self.pageUpDnSuspended[0]=1 
  def __continueScrollUp(self,m=None): 
      if taskMgr.hasTaskNamed('dragSliderThumb'): 
         return 
      self.pageUpRegion.setAlphaScale(1) 
      self.pageUpDnSuspended[0]=0 
  
  def __suspendScrollDn(self,m=None): 
      self.pageDnRegion.setAlphaScale(0) 
      self.pageUpDnSuspended[1]=1 
  def __continueScrollDn(self,m=None): 
      if taskMgr.hasTaskNamed('dragSliderThumb'): 
         return 
      self.pageDnRegion.setAlphaScale(1) 
      self.pageUpDnSuspended[1]=0 

  def __suspendScrollPage(self,m=None): 
      self.__suspendScrollUp() 
      self.__suspendScrollDn() 
  
  def __enteringThumb(self,m=None): 
      self.vertSliderThumb['frameColor']=(1,1,1,1) 
      self.__suspendScrollPage() 

  def __exitingThumb(self,m=None): 
      self.vertSliderThumb['frameColor']=(1,1,1,.6) 

  def __scrollCanvas(self,scroll): 
      if self.vertSliderThumb.isHidden(): 
         return 
      self.__updateCanvasZpos(self.canvas.getZ()+scroll) 

  def __updateCanvasZpos(self,Zpos): 
      newZ=clampScalar(Zpos, .0, self.canvasLen-self.frameHeight+.015) 
      self.canvas.setZ(newZ) 
      thumbZ=-newZ*self.canvasRatio 
      self.vertSliderThumb.setZ(thumbZ) 
      self.pageUpRegion['frameSize']=(-.015,.015,thumbZ-.01,-.01) 
      self.pageDnRegion['frameSize']=(-.015,.015,-self.frameHeight+.01,thumbZ+self.vertSliderThumb['frameSize'][2]) 

  def __adjustCanvasLength(self,numItem): 
      self.canvasLen=float(numItem)*self.itemVertSpacing 
      self.canvasRatio=(self.frameHeight-.015)/(self.canvasLen+.01) 
      if self.canvasLen<=self.frameHeight-.015: 
         canvasZ=.0 
         self.vertSliderThumb.hide() 
         self.pageUpRegion.hide() 
         self.pageDnRegion.hide() 
         self.canvasLen=self.frameHeight-.015 
      else: 
         canvasZ=self.canvas.getZ() 
         self.vertSliderThumb.show() 
         self.pageUpRegion.show() 
         self.pageDnRegion.show() 
      self.__updateCanvasZpos(canvasZ) 
      self.vertSliderThumb['frameSize']=(-.015,.015,-self.frameHeight*self.canvasRatio,-.01) 
      thumbZ=self.vertSliderThumb.getZ() 
      self.pageUpRegion['frameSize']=(-.015,.015,thumbZ-.01,-.01) 
      self.pageDnRegion['frameSize']=(-.015,.015,-self.frameHeight+.01,thumbZ+self.vertSliderThumb['frameSize'][2]) 

  def __acceptAndIgnoreWorldEvent(self,event,command,extraArgs=[]): 
      receivers=messenger.whoAccepts(event) 
      if receivers is None: 
         self.__eventReceivers[event]={} 
      else: 
         newD={}
         for r in receivers:
             newr=messenger._getObject(r) if type(r)==tuple else r
             newD[newr]=receivers[r]
         self.__eventReceivers[event]=newD
      for r in self.__eventReceivers[event].keys(): 
          r.ignore(event) 
      self.accept(event,command,extraArgs) 

  def __ignoreAndReAcceptWorldEvent(self,events): 
      for event in events: 
          self.ignore(event) 
          if self.__eventReceivers.has_key(event): 
             for r, method_xtraArgs_persist in self.__eventReceivers[event].items(): 
                 messenger.accept(event,r,*method_xtraArgs_persist) 
          self.__eventReceivers[event]={} 

  def __enteringFrame(self,m=None): 
      # sometimes the WITHOUT event for page down region doesn't fired, 
      # so directly suspend the page scrolling here 
      self.__suspendScrollPage() 
      BTprefix=base.buttonThrowers[0].node().getPrefix() 
      if BTprefix==self.sliderThumbDragPrefix: 
         return 
      self.inOutBTprefix=BTprefix 
      if self.suppressMouseWheel: 
         self.__acceptAndIgnoreWorldEvent(self.inOutBTprefix+'wheel_up', 
              command=self.__scrollCanvas, extraArgs=[-.07]) 
         self.__acceptAndIgnoreWorldEvent(self.inOutBTprefix+'wheel_down', 
              command=self.__scrollCanvas, extraArgs=[.07]) 
      else: 
         self.accept(self.inOutBTprefix+self.modifier+'-wheel_up',self.__scrollCanvas, [-.07]) 
         self.accept(self.inOutBTprefix+self.modifier+'-wheel_down',self.__scrollCanvas, [.07]) 
      print 'enteringFrame' 

  def __exitingFrame(self,m=None): 
      if not hasattr(self,'inOutBTprefix'): 
         return 
      if self.suppressMouseWheel: 
         self.__ignoreAndReAcceptWorldEvent( ( 
                                             self.inOutBTprefix+'wheel_up', 
                                             self.inOutBTprefix+'wheel_down', 
                                             ) ) 
      else: 
         self.ignore(self.inOutBTprefix+self.modifier+'-wheel_up') 
         self.ignore(self.inOutBTprefix+self.modifier+'-wheel_down') 
      print 'exitingFrame' 

  def __setFocusButton(self,button,item):
      if self.focusButton:
         self.deselect()
      self.focusButton=button
      self.select()
      if callable(self.command):
         # run user command and pass the selected item, it's index, and the button
         self.command(item,self.buttonsList.index(button),button)

  def __rightPressed(self,button,m): 
      self.__isRightIn=True 
#       text0 : normal 
#       text1 : pressed 
#       text2 : rollover 
#       text3 : disabled 
      button._DirectGuiBase__componentInfo['text2'][0].setColorScale(self.rightClickTextColors[self.focusButton==button]) 
      button.bind(DGG.B3RELEASE,self.__rightReleased,[button]) 
      button.bind(DGG.WITHIN,self.__rightIn,[button]) 
      button.bind(DGG.WITHOUT,self.__rightOut,[button]) 

  def __rightIn(self,button,m): 
      self.__isRightIn=True 
      button._DirectGuiBase__componentInfo['text2'][0].setColorScale(self.rightClickTextColors[self.focusButton==button]) 
  def __rightOut(self,button,m): 
      self.__isRightIn=False 
      button._DirectGuiBase__componentInfo['text2'][0].setColorScale(Vec4(1,1,1,1)) 

  def __rightReleased(self,button,m): 
      button.unbind(DGG.B3RELEASE) 
      button.unbind(DGG.WITHIN) 
      button.unbind(DGG.WITHOUT) 
      button._DirectGuiBase__componentInfo['text2'][0].setColorScale(self.rolloverColor) 
      if not self.__isRightIn: 
         return 
      if callable(self.contextMenu): 
         # run user command and pass the selected item, it's index, and the button 
         self.contextMenu(button['extraArgs'][1],self.buttonsList.index(button),button) 

  def deselect(self):
      """ 
         stop highlighting item 
      """
      if self.focusButton:
         self.focusButton['text_fg']=(1,1,1,1) 
         self.focusButton['frameColor']=(0,0,0,0) 

  def select(self,idx=None): 
      """ 
         highlight the item 
      """ 
      if idx is not None:
         if not 0<=idx<self.numItems:
            print 'SELECT : invalid index (%s)' %idx
            return
         self.focusButton=self.buttonsList[idx] 
      if self.focusButton:
         self.focusButton['text_fg']=(.01,.01,.01,1) 
         self.focusButton['frameColor']=(1,.8,.2,1) 

  def clear(self):
      """
         clear the list
      """
      for c in self.buttonsList:
          c.remove()
      self.buttonsList=[]
      self.focusButton=None
      self.numItems=0

  def addItem(self,text,extraArgs=None,atIndex=None): 
      """ 
         add item to the list 
         text : text for the button 
         extraArgs : the object which will be passed to user command(s) 
                     (both command and contextMenu) when the button get clicked 
         atIndex : where to add the item 
                   <None> : put item at the end of list 
                   <integer> : put item at index <integer> 
                   <button> : put item at <button>'s index 
      """ 
      button = DirectButton(parent=self.canvas, 
          scale=self.itemScale, 
          relief=DGG.FLAT, 
          frameColor=(0,0,0,0),text_scale=self.itemTextScale, 
          text=text, text_pos=(0,self.itemTextZ),text_fg=self.buttonTextColor, 
          text_font=self.font, text_align=TextNode.ALeft, 
          command=self.__setFocusButton, 
          enableEdit=0, suppressMouse=0) 

      l,r,b,t=button.getBounds() 
      # top & bottom are blindly set without knowing where exactly the baseline is, 
      # but this ratio fits most fonts 
      baseline=-self.fontHeight*.25 
      button['frameSize']=(l-self.xtraSideSpace,r+self.xtraSideSpace,baseline,baseline+self.fontHeight) 

#          Zc=NodePath(button).getBounds().getCenter()[1]-self.fontHeight*.5+.25 
# #          Zc=button.getCenter()[1]-self.fontHeight*.5+.25 
#          button['frameSize']=(l-self.xtraSideSpace,r+self.xtraSideSpace,Zc,Zc+self.fontHeight) 
      
      button['extraArgs']=[button,extraArgs] 
      button._DirectGuiBase__componentInfo['text2'][0].setColorScale(self.rolloverColor) 
      button.bind(DGG.B3PRESS,self.__rightPressed,[button]) 
      isButton=isinstance(atIndex,DirectButton)
      if isButton:
         if atIndex.isEmpty(): 
            atIndex=None 
         else: 
            index=self.buttonsList.index(atIndex) 
            self.buttonsList.insert(index,button) 
      if atIndex==None: 
         self.buttonsList.append(button) 
         index=self.numItems 
      elif not isButton:
         index=int(atIndex)
         self.buttonsList.insert(index,button) 
      Zpos=(-.7-index)*self.itemVertSpacing 
      button.setPos(.02,0,Zpos) 
      if index!=self.numItems: 
         for i in range(index+1,self.numItems+1): 
             self.buttonsList[i].setZ(self.buttonsList[i],-self.fontHeight) 
      self.numItems+=1 
      self.__adjustCanvasLength(self.numItems) 
      if self.autoFocus: 
         self.focusViewOnItem(index) 
      if self.colorChange: 
         Sequence( 
            button.colorScaleInterval(self.colorChangeDuration,self.newItemColor,Vec4(1,.2,0,1)), 
            button.colorScaleInterval(self.colorChangeDuration,Vec4(1,1,1,1),self.newItemColor) 
            ).start() 

  def focusViewOnItem(self,idx): 
      """ 
         Scroll the window so the newly added item will be displayed 
         in the middle of the window, if possible. 
      """ 
      Zpos=(idx+.7)*self.itemVertSpacing-self.frameHeight*.5 
      self.__updateCanvasZpos(Zpos) 
      
  def setAutoFocus(self,b): 
      """ 
         set auto-view-focus state of newly added item 
      """ 
      self.autoFocus=b 

  def index(self,button): 
      """ 
         get the index of button 
      """ 
      if not button in self.buttonsList: 
         return None 
      return self.buttonsList.index(button) 
      
  def getSelected(self):
      """ 
         get the currently selected button,
         returns None if no button is selected
      """
      return self.focusButton

  def getSelectedIndex(self):
      """ 
         get the currently selected button index,
         returns None if no button is selected
      """
      return self.index(self.focusButton)

  def getNumItems(self):
      """ 
         get the current number of items on the list 
      """ 
      return self.numItems

  def disableItem(self,i): 
      if not 0<=i<self.numItems: 
         print 'DISABLING : invalid index (%s)' %i 
         return 
      self.buttonsList[i]['state']=DGG.DISABLED 
      self.buttonsList[i].setColorScale(.3,.3,.3,1) 
  
  def enableItem(self,i): 
      if not 0<=i<self.numItems: 
         print 'ENABLING : invalid index (%s)' %i 
         return 
      self.buttonsList[i]['state']=DGG.NORMAL 
      self.buttonsList[i].setColorScale(1,1,1,1) 

  def removeItem(self,index): 
      if not 0<=index<self.numItems: 
         print 'REMOVAL : invalid index (%s)' %index 
         return 
      if self.numItems==0: return 
      if self.focusButton==self.buttonsList[index]: 
         self.focusButton=None 
      self.buttonsList[index].removeNode() 
      del self.buttonsList[index] 
      self.numItems-=1 
      for i in range(index,self.numItems): 
          self.buttonsList[i].setZ(self.buttonsList[i],self.fontHeight) 
      self.__adjustCanvasLength(self.numItems) 

  def destroy(self): 
      self.clear() 
      self.__exitingFrame() 
      self.ignoreAll() 
      self.frame.removeNode() 
      taskMgr.remove('mouseInRegionCheck') 

  def hide(self): 
      self.frame.hide() 
      self.isMouseInRegion=False 
      self.__exitingFrame() 
      taskMgr.remove('mouseInRegionCheck') 

  def show(self): 
      self.frame.show() 
      if not hasattr(self,'frameRegion'): 
         taskMgr.doMethodLater(.2,self.__getFrameRegion,'getFrameRegion') 
      elif not taskMgr.hasTaskNamed('mouseInRegionCheck'): 
         taskMgr.add(self.__mouseInRegionCheck,'mouseInRegionCheck') 

  def toggleVisibility(self):
      if self.frame.isHidden():
         self.show()
      else:
         self.hide()
  
  def sort(self,reverse=False):
      buttonsTexts=[(b['text'],b) for b in self.buttonsList]
      buttonsTexts.sort()
      if reverse:
         buttonsTexts.reverse()
      self.buttonsList=[bt[1] for bt in buttonsTexts]
      for i in range(self.getNumItems()):
          Zpos=(-.7-i)*self.itemVertSpacing
          self.buttonsList[i].setPos(.02,0,Zpos)



if __name__=='__main__': 
   import direct.directbase.DirectStart 
   import string,sys 
   from random import uniform 
    
   selectedButton=None 

   def itemSelected(item,index,button): # don't forget to receive item, it's index, and the button 
       global selectedButton 
       selectedButton=button 
       print item,'(idx : %s) SELECTED' %index 

   def itemRightClicked(item,index,button): # don't forget to receive item, it's index, and the button 
       print item,'(idx : %s) RIGHT-CLICKED' %index 

   def addNewItem(): 
       num=btnList.getNumItems()
       i='new item '+str(num)
       btnList.addItem(text=i, extraArgs='NEW_%s'%num)

   def insertNewItemAtSelected(): 
       num=btnList.getNumItems()
       i='new item '+str(num)
       btnList.addItem(text=i, extraArgs='NEW_%s'%num, atIndex=selectedButton)

   def insertNewItemBelowSelected(): 
       index=btnList.index(selectedButton) 
       num=btnList.getNumItems()
       if index==None:
          index=num-1 
       i='new item '+str(num)
       btnList.addItem(text=i, extraArgs='NEW_%s'%num, atIndex=index+1)

   def disableRandomItem():
       btnList.disableItem(int(uniform(0,btnList.getNumItems()))) 
        
   def enableRandomItem():
       btnList.enableItem(int(uniform(0,btnList.getNumItems()))) 

   def removeRandomItem():
       btnList.removeItem(int(uniform(0,btnList.getNumItems()))) 
        
   # 3d node 
   teapot=loader.loadModel('teapot') 
   teapot.reparentTo(render) 

   OnscreenText(text='''[ SPACE ] : toggle visibility       [ S ] : sort list       [ DEL ] : destroy list
[ MOUSE WHEEL ]  inside scrollable window : scroll window 
[ MOUSE WHEEL ]  outside scrollable window : change teapot scale 
hold  [ ENTER ] : add new item at the end of list 
hold  [ INSERT ] : insert new item at selected item 
hold  [ CTRL-INSERT ] : insert new item below selected item 
''', scale=.045, fg=(1,1,1,1), shadow=(0,0,0,1)).setZ(.96) 
   OnscreenText(text='''[ CAPS LOCK] : disable random item
[ TAB ] : enable random item
hold  [ X ] : remove random item
''', scale=.045, fg=(1,1,1,1), shadow=(0,0,0,1)).setZ(-.75)


   # create ScrolledButtonsList
   btnList = ScrolledButtonsList(
      parent=None, # attach to this parent node
      frameSize=(.8,1.2), buttonTextColor=(1,1,1,1),
      font=None, itemScale=.045, itemTextScale=1, itemTextZ=0,
#                font=transMtl, itemScale=.05, itemTextScale=1, itemTextZ=0,
      command=itemSelected, # user defined method, executed when a node get selected,
                            # receiving extraArgs (which passed to addItem)
      contextMenu=itemRightClicked, # user defined method, executed when a node right-clicked,
                                    # receiving extraArgs (which passed to addItem)
      autoFocus=0, # initial auto view-focus on newly added item
      colorChange=1,
      colorChangeDuration=.7,
      newItemColor=(0,1,0,1),
      rolloverColor=(1,.8,.2,1),
      suppressMouseWheel=1,  # 1 : blocks mouse wheel events from being sent to all other objects.
                             #     You can scroll the window by putting mouse cursor
                             #     inside the scrollable window.
                             # 0 : does not block mouse wheel events from being sent to all other objects.
                             #     You can scroll the window by holding down the modifier key
                             #     (defined below) while scrolling your wheel.
      modifier='control'  # shift/control/alt
      )
   # populate the list with alphabets and numbers
   for a in string.ascii_lowercase+string.digits:
       # extraArgs will be passed to command and contextMenu
       btnList.addItem(text='item  '+(a+' ')*10, extraArgs=a,)
   btnList.setAutoFocus(1)  # after adding those items,
                            # enable auto view-focus on newly added item

   DO=DirectObject()
   # events for btnList
   DO.accept('space',btnList.toggleVisibility)
   DO.accept('delete',btnList.destroy)
   DO.accept('enter',addNewItem)
   DO.accept('enter-repeat',addNewItem)
   DO.accept('insert',insertNewItemAtSelected)
   DO.accept('insert-repeat',insertNewItemAtSelected)
   DO.accept('control-insert',insertNewItemBelowSelected)
   DO.accept('control-insert-repeat',insertNewItemBelowSelected)
   DO.accept('caps_lock',disableRandomItem)
   DO.accept('tab',enableRandomItem)
   DO.accept('x',removeRandomItem)
   DO.accept('x-repeat',removeRandomItem)
   DO.accept('s',btnList.sort)
   # events for the world 
   DO.accept('wheel_up',teapot.setScale,[teapot,1.2]) 
   DO.accept('wheel_down',teapot.setScale,[teapot,.8]) 
   DO.accept('escape',sys.exit) 

   camera.setPos(11.06, -16.65, 8.73)
   camera.setHpr(42.23, -25.43, -0.05)
   base.setBackgroundColor(.2,.2,.2,1)
   base.disableMouse()
   run()

Thanks!

Hey ynjh, Thanks again for posting this code, I can see you put a lot of effort into this List Control and I love it! Its very full featured with the scrollable mouse wheel support, etc… I was able to easily put it into the Asteroids Sample as a test.

Couple Questions for you (or anyone interested)

  1. I noticed a significant frame rate drop if I expanded the window or went full-screen, is this normal when adding panda3d gui?

  2. I can live with 1) as long as there is a way to remove the overhead if I switch modes within my game (ie, is there a clean way to remove the list? I tried using the Destroy method, but the frame rate did not go back up, I tried setting List=None, that didn’t help either, any suggestions?

  3. ynjh, have you thought of expanding your obvious gui creating talents and making a nice gui toolkit for us developers to use? I can see you put a lot of effort into this ListView (and the treeview). I have seen that someone was starting a gui toolkit, but that kinda died off.

Thanks again,

C

no it kinda did not die off i am using it my game now
i guess i should try to make it public … and wait for features

  1. I’m not sure if it’s caused by the 2d scene, but I could be wrong though. What resolution for fullscreen did you use ? The same like when windowed ? If so, then there must be a deep problem.
  2. I’ll take a deeper look. I’m not on my dev pc.
  3. Sorry, I don’t have time for that.

ColShag, while you’re around, do you mind telling us about your experience with PyUI ? The pros and cons.
I guess it’s possible to use it within panda, as within PyGame, since it simply calls PyGame’s drawing commands, similar like using LineSegs in Panda for lines, and so on.

treeform: That’s great that you are already using it, I’m sure its the last thing on your mind, but if you could share the results I would appreciate it greatly (see if it would work out for my potential port).

ynjh: Yeah I’ve noticed that if I expand the window simply the framerate seems to half usually. I’m unsure of why, but more importantly, if I remove the listbox the framerate does not change at all (ie go back to the higher framerate), that worries me more.

As for pyui, yes, I’ve used it quite a bit with my game. I basically used the pyui, plus the simple open-source engine that the pyui author made as a part of his python game programming book to successfully make a full fledged game. It was quite successful. The drawbacks are that pyui depends on pyOpenGl, which is not being well supported, when I went to ubuntu-gutsy I noticed serious framerate problems so I started to look seriously at panda3d again.

Pyui seems to be setup to allow for rendering using other engines, certainly panda3d would be a possible option. I have looked at the source to see what kind of work that would be but I have not gone down that road as I’m afraid it might be a bit beyond my abilities as I am very new to panda3d.

I’ll share some screenshots of pyui within my game (these are a bit old, but you will get the idea).

Ok, I would share, but I don’t have a website setup right now (my game website is down for now), so any suggestions on how to post to this forum?

Thanks,

C

ColShag, what are you porting? i could build an example for you as a tutorial.

It’s not the 2d scene as far as I can see. Lower framerate is normal for greater resolution.
The results here (simply that scene, idle) are :
1. normal window 800x600
shown : 202 fps
hidden : 265 fps
2. maximized window 1014x705
shown : 158 fps
hidden : 188 fps
3. fullscreen 800x600
shown : 217 fps
hidden : 564 fps
Obviously going fullscreen naturally speeds things up, I guess the route is different. I haven’t read any technical explanation about this.
If removing/hiding the list didn’t restore the framerate, you’re in trouble I can’t see.
In clear(), try replacing c.remove with c.destroy, see if it help, but I doubt it since I couldn’t see the difference.
Have you tried imageshack.net ?

I have to say I’m really impressed with you guys, I really appreciate your help on this.

ynjh: That’s great news that you seem to have a new framerate when hidden, I guess I must be doing something wrong here, certainly that is possible as my experience with pandad3d is very limited. I’m running ubuntu gutsy with python 2.5 if that makes any difference.

treeform: I really appreciate your offer to make an example for me, now how to go about it? First off let me explain the game. Its a multiplayer client/server space strategy type game where you manage an empire of systems, ships, research, diplomacy etc… My friends really enjoy it, but I’ve decided to try and improve the client now that I am using a somewhat dead engine/ui.

As you can guess it requires some gui, rendered of course. Mainly I’ve managed to use pyui in its somewhat limited form. I use a lot of selectable lists, nothing as fancy as what ynjh made, but they did the job. I used buttons, textboxes, etc… The key for me was being able to select an object with the mouse which would bring up the gui as required. I used a mode system to traverse the game, I would be interested in your input on a good way to transition between modes in panda3d. By modes I mean allowing the game to switch between different scenes and properly load an unload objects, gui, etc. From reading the documentation I’m thinking maybe of the finite state machine logic for that. Of course an example of how you fine gents would do this would be helpful. One thing I’ve noticed from looking at most of the code in the examples, it seems that everything is thrown into the world object and then that object is run.

Modes in these examples are methods of that world object. I would prefer personally to have modes as their own objects, inheriting from a central node object with functions that all modes use. The player would then start in a mode and depending on actions close one mode and go to another. I like this method as it separates the code which can get messy when dealing with all the client and client/server logic involved. What are your thoughts on this?

There are two main functions of the game, the first is building the empire, research, diplomacy, fleet and army movement. The second is watching ship battle results. The Ship Battles are where I’m most worried about performance issues, but I’m sure Panda could handle it as its not that crazy. The other advantage of panda that I can see is the nice 3d abilities, currently my game is fully 2d, but I would like to transition to a mix of the two in the future.

I tried to setup image shack, as per your suggestion, here are a few screen shots… (hopefully they come through)

This is the research screen, beakers are selected and the gui panel allows for research, pretty basic here.

By colshag

This is the ship design screen, allows for ship design, you add components and weapons. I use a lot of list boxes, which work ok. Notice the 3 tabs at the top of the gui on the right, does panda allow for tabs somehow?

By colshag

This is the main galaxy screen, I have one panel opening another.

By colshag

This is a slightly older screenshot of the ship battle viewer. Battles are viewed as an afterfact. The player cannot “control” the ships, however he can give them orders before they go off to fight, much like a real emperor could in my opinion.

By colshag

Thanks again,

C

I’m actually using your ButtonList in my game (attempting), seems to work well, thanks for the post. Small bug fix:

    def clear(self):
        """
           clear the list
        """
        for c in self.buttonsList:
            c.remove()
        self.buttonsList=[]
        self.focusButton=None
        self.numItems=0

Sorry for not really using it.

Yes, thanks ynjh for all your help and sharing your code. I am still working on my game, its getting close, and I do use an old version of your scrolled button list which works great.

C

UPDATE :
[+] added sorting ability
[+] changed some methods’ name