DirectScrollOptionMenu

Hi guys.

I decided to share with you something useful I’ve made for my project - DirectScrollOptionMenu.
It has all features of DirectOptionMenu and adds functionality for working with big list of items, which would go outside the screen.
The code has lots of comments, but in general it has:

  • setIndex function - should be used for setting specific index - DO NOT USE the set function directly.
  • setItem function - should be used for setting item - DO NOT USE the set function directly.
  • PrevItem and NextItem, which controlls scrolling throught previous and next pages;
  • EmptyItem - always the first Item of the first page (This wasn’t crucial for the implementation, but was very useful for my project)
  • maxShownItem keyword - how many items want to see on a page - must be bigger than 3
  • command function will be with body - function(item, isRealItem, [eventual args]) (isRealItem - if item is not the EmptyItem)
  • some helpful functions for checking if current item is a real item (not Empty, Prev or Next items)

Here it is the code and some basic example of working with the menu.

from pandac.PandaModules import *
from direct.gui.DirectOptionMenu import DirectOptionMenu


class DirectScrollOptionMenu(DirectOptionMenu):
    """ Expand the functionality of DirectOptionMenu with option for different pages.
    It uses EmptyItem for first item on first page; PrevItem for firstItem on every other page; NextItem on last item on every page except the last one.
    DO NOT USE Items with the same names as Empty, Prev and NextItem or the entire logic will break.
    DO NOT USE directly the set function - use setIndex insted, which works only with indeces of allItems list.
    Using initialItem is not recommended, but if you want it keep in mind it must be smaller than maxShownItems
    Example: Given list [7, 8, 9, 3, 234, 7]. If maxShownItems is 5: [[Empty, 7, 8, 9, Next], [Prev, 3, 234, 7]]"""
    def __init__(self, parent = None, maxShownItems = 15, prevItem = 'Prev', nextItem = 'Next', emptyItem = '----', **kw):
        optiondefs = (
            ('maxShownItems',           15,             None),
            ('allitems',       [],             self.setAllItems),
            ('prevItem',       'P',             None),
            ('nextItem',       'N',             None),
            ('emptyItem',       'E',             None),
        )
        assert maxShownItems > 3, 'You can not create ScrollOptionMenu with less than 4 maxShownItems - and there is no point of doing it either!!!'

        self.curListIndex = 0
        self.maxShownItems = maxShownItems
        self.prevItem = prevItem
        self.nextItem = nextItem
        self.emptyItem = emptyItem
        self.numPages = 0
        # Merge keyword options with default options
        self.defineoptions(kw, optiondefs)

        self.__recalculateItems()

        DirectOptionMenu.__init__(self, parent, items = self.screenItems[self.curListIndex])
        self.initialiseoptions(DirectScrollOptionMenu)

    def changeEmptyItem(self, newEmptyItem):
        self['emptyItem'] = newEmptyItem
        self.emptyItem = newEmptyItem
        self.__recalculateItems()

    def __recalculateItems(self):
        """ Remake the screenItems adding Empty, Prev and Next items according to the leght of allitems and maxShownItems."""
        SHOWN_ITEMS = self.maxShownItems - 2
        c = list()
        for x in range(len(self['allitems'])/SHOWN_ITEMS + 1):
            subList = self['allitems'][x*SHOWN_ITEMS :(x+1)*SHOWN_ITEMS]
            if len(subList) == 1  and x > 0:
                c[x-1].append(self['allitems'][x*SHOWN_ITEMS])
                break
            if x*SHOWN_ITEMS < len(self['allitems']):
                c.append(subList)
            else:
                break

        self.screenItems = list(c)
        for x in range(len(self.screenItems)):
            if x == 0:
                self.screenItems[x].insert(0, self.emptyItem)
            if x != 0:
                self.screenItems[x].insert(0, self.prevItem)

            if x != len(self.screenItems)-1:
                self.screenItems[x].insert(self.maxShownItems-1, self.nextItem)

        if not self.screenItems:
            self.screenItems = [[self.emptyItem]]
#            print 'remake screenItems since there is no items'

        self.numPages = len(self.screenItems)

    def setAllItems(self):
        self.__recalculateItems()
        self.curListIndex = 0
        self['items'] = self.screenItems[self.curListIndex]

    def __selectedItem(self, arg):
        """ Must be called every time when is executed the command function.
        If Next or Prev is chosen shows the correct new list of items.
        Return True if was choosen normal element; False if was selected Next or Prev Element."""
        if arg == self.nextItem or arg == self.prevItem: #no need to add an element
            if arg == self.nextItem:
                self.curListIndex += 1
            else:
                self.curListIndex -= 1
            self['items'] = self.screenItems[self.curListIndex]
            taskMgr.doMethodLater(0.01, self.__updateItemMenu, 'UpdateItemMenuTask')
            return False

        return True

    def setIndex(self, index, fCommand = 1):
        """ Use this function instead set function of DirectOptionMenu. Use always index for the whole list - allitems.
        The function automatic adds +1 to the index to compensate the EmptyItem. Use index=-1 if you want to set the EmptyItem.
        The result is always EmptyItem or some realItem - never PrevItem or NextItem."""
        index += 1                                                  # compensate the EmptyItem
        if self.numPages > 1:                                       # if has multiple pages in the menu
            if index >= self.maxShownItems - 1:                     # if is going to a scrollMenu different than the first one
                self.curListIndex = index/(self.maxShownItems - 2)  # 2 - (empty and Next) or (Prev and Next)
                index = index%(self.maxShownItems -2)               # emptyItem/prevItem auto compensate the index in list logic
                if index == 0:                                      # specific case - show the last realItem from the previous page
                    self.curListIndex -= 1
                    index = self.maxShownItems - 2

                self['items'] = self.screenItems[self.curListIndex]

        self.set(index, fCommand)

    def setItem(self, item, fCommand = 1):
        assert self.isRealItem(item), 'You can not set directly Empty, Prev or Next Items'

        self.setIndex(self['allitems'].index(item), fCommand)

    def set(self, index, fCommand = 1):
        """ Override the set function of DirectOptionMenu - this fucntion will make the checks for Prev and Next items."""
        # Item was selected, record item and call command if any
        newIndex = self.index(index)
        if newIndex is not None:
            self.selectedIndex = newIndex
            item = self['items'][self.selectedIndex]
            self['text'] = item
            if fCommand:
                if self.__selectedItem(item):
                    if self['command']:
                        # Pass isRealItem and any extra args to command
                        apply(self['command'], [item, item != self.emptyItem] + self['extraArgs'])

    def __updateItemMenu(self, task):
        self.showPopupMenu()
        return task.done

    def isEmpty(self):
        """ Return True/False indicating if the current selected item is the emptyItem."""
        return self.get() == self.emptyItem

    def isSelectedItem(self):
        """ Return True/False indicating if the current selected item is item from the given ItemList - not preserved Empty, Prev or NextItem."""
        item = self.get()
        return item != self.emptyItem and item != self.prevItem and item != self.nextItem

    def isRealItem(self, item):
        """ Return True/False indicating if the item is not Empty, Prev or NextItem."""
        return item != self.emptyItem and item != self.prevItem and item != self.nextItem

example:

import direct.directbase.DirectStart
from direct.gui.OnscreenText import OnscreenText
from direct.gui.DirectGui import *
from panda3d.core import *

from DirectScrollOptionMenu import DirectScrollOptionMenu

# Add some text
bk_text = "DirectOptionMenu Demo"
textObject = OnscreenText(text = bk_text, pos = (0.85,0.85), scale = 0.07,fg=(1,0.5,0.5,1),align=TextNode.ACenter,mayChange=1)

# Add some text
output = ""
textObject = OnscreenText(text = output, pos = (0,-0.95), scale = 0.07,fg=(1,0.5,0.5,1),align=TextNode.ACenter,mayChange=1)

# Callback function to set  text
def itemSel(arg, isRealItem):
    output = "Item Selected is: %s. It's emptyItem - %s" %(arg, not isRealItem)
    textObject.setText(output)

items = ['item%d' %x for x in xrange(20)]

# Create a frame
menu = DirectScrollOptionMenu(text="options", scale=0.1, allitems=items, highlightColor=(0.65,0.65,0.65,1), maxShownItems = 6, command=itemSel)

print 'Pages in the scrollOptionMenu:'
for page in menu.screenItems:
    print page

#menu.setIndex(11)
menu.setIndex(items.index('item%d' %11))
#menu.setItem('item%d' %11)

# Run the tutorial
run()

Hey thx!
I may be able to use this.