Hi everyone!
I’m rather new to the Panda3D community, but thanks to you guys i learned rather quickly working with it in the last two months. On my way to develop a GUI for my prototype of a mobile game, i stumbled across this snipped:
https://discourse.panda3d.org/t/scrolledbuttonslist/3183
Giving me great ideas, but not meeting exactly what i needed i created my own container that allows to select an item in a list. I tried to design it multi-purpose and it comes with the following features:
- Scrollable: It uses a DirectScrolledFrame as the main container. Scroll bars are turned off and destroyed, in case you did actually need them.
- Expandable: At the moment an item is a DirectFrame, that can hold an image, a title and a text. With the add_item() function you can include all of these elements, but feel free to simply attach what ever you need in that item.
- Dynamic: Resizing and repositioning should be rather simple, as all metrics are relative and can be passed at construction of the object, in the same manner as a DirectObject.
- Selection function: The currently selected item (tapped on) can be accessed with get_active_item(), which returns the DirectFrame.
- Value List: The Object features a seperate list. This allows to specify a specific value for an item (like the key or ID for a DB or sorts). This value is also passed with add_item() and can be retrieved with get_active_value().
- Switch command: With the argument ‘command’, you can pass a function that gets triggerd every time a switch of the selection has happend. For instance to talk to the above mentioned DB.
- Touch optimised: Now this is quite an assumption, as i could only test it on my widnows laptop with touchscreen. So everything heavily relies on the Mouse. Scrolling is simply done by swiping the elements up and down and the boundaries are automatically ajusted to item hight and number. Flicking results in a damped scrolling until it stops. But to anyone with compile experience on phones: I would be more than happy if this gets some testing.
- Clearing / destroying function: With clear() you empty the whole list and reset all variables, with destroy() you destroy every item and the object itself.
ToDo:
- Add same Geom-Arguments that you would expect from a DirectObject, so this can get passed directly.
- Test if touch functionallity actually works on phones.
Let me know what you think about this or if this could be remotly of use to you, and i’ll keep you updated with further versions.
Cheers!
from direct.showbase.DirectObject import DirectObject
from direct.gui.OnscreenImage import OnscreenImage
from panda3d.core import *
from direct.gui.DirectGui import DirectFrame, DirectScrolledFrame, DirectLabel, DGG
class ScrolledItemSelector(DirectObject):
'''Touch optimised list that holds a set of items. By clicking on one you select it and return
its Value with get_selected(). To add an item simply use add_item(). You can also pass a
function with the 'command' field that gets called on a selection switch'''
def __init__(self,
frame_size=(1, 1.5),
frame_color=(0.2, 0.2, 0.2, 0.8),
pos=(0, 0, 0),
item_v_padding=0.02,
item_h_padding=0.04,
item_scale=1,
item_side_ratio=0.2,
item_background=(0.3, 0.3, 0.3, 1),
item_background_active=(0.6, 0.6, 0.6, 1),
command=lambda: None
):
self.f_x, self.f_y = frame_size
self.c_x = self.f_x
self.c_y = self.f_y
self.item_v_padding = item_v_padding
self.item_h_padding = item_h_padding
self.item_scale = item_scale
self.item_background = item_background
self.item_background_active = item_background_active
self.frame = DirectScrolledFrame(
frameSize=(-self.f_x / 2, self.f_x / 2, -self.f_y / 2, self.f_y / 2),
canvasSize=(-self.c_x / 2, self.c_x / 2, -self.c_y / 2, self.c_y / 2),
frameColor=frame_color,
pos=pos,
scrollBarWidth=(0),
manageScrollBars=False,
state=DGG.NORMAL)
self.frame.verticalScroll.destroy()
self.frame.horizontalScroll.destroy()
self.frame.bind(DGG.WITHIN, self._start_listening)
self.frame.bind(DGG.WITHOUT, self._stop_listening)
self.canvas = self.frame.getCanvas()
self.i_x = self.f_x * (1 - item_h_padding)
self.i_y = self.f_y * item_side_ratio
self.i_size = (-self.i_x / 2, self.i_x / 2, -self.i_y / 2, self.i_y / 2)
self.i_start = self.c_y / 2 + self.i_y / 2
self.active_item = None
self.item_list = []
self.value_list = []
self.start_mY = None
self.old_mY = None
self.m_diff = 0
self.is_scrolling = False
self.command = command
messenger.toggleVerbose()
def _start_listening(self, watcher):
print('start listening')
self.accept("mouse1", self._start_scroll)
self.accept("mouse1-up", self._stop_scroll)
def _stop_listening(self, watcher):
print('stop listening')
self.ignore("mouse1")
self.ignore("mouse1-up")
self._stop_scroll()
def _switch_active_item(self, item, watcher):
if not self.is_scrolling:
print('switched')
if self.active_item is not None:
self.active_item['frameColor'] = self.item_background
item['frameColor'] = self.item_background_active
self.active_item = item
self.command()
def _start_scroll(self):
n = len(self.item_list)
content_length = (
(n*(self.i_y + self.item_v_padding)) + # Size of all elements with padding
self.item_v_padding) # Add one padding for the bottom
if content_length > self.c_y:
taskMgr.doMethodLater(0.01, self._scroll_task, 'scroll_task')
self.start_mY = base.mouseWatcherNode.getMouse().getY()
self.old_mY = self.start_mY
def _stop_scroll(self):
taskMgr.remove('scroll_task')
taskMgr.doMethodLater(0.01, self._scroll_fade_task, 'scrollfadetask')
self.is_scrolling = False
def _scroll_task(self, task):
mY = base.mouseWatcherNode.getMouse().getY()
old_c = self.canvas.getZ()
self.m_diff = self.old_mY - mY
n = len(self.item_list)
if self.m_diff != 0:
self.is_scrolling = True
self.c_scroll_start = 0
self.c_scroll_stop = (
(n * (self.i_y + self.item_v_padding)) + # Size of all elements with padding
self.item_v_padding - # Add one padding for the bottom
self.f_y) # Substract the length of the canvas
self.c_new_pos = (old_c - self.m_diff)
hits_not_upper_bound = self.c_new_pos >= self.c_scroll_start
hits_not_lower_bound = self.c_new_pos <= self.c_scroll_stop
print('canvas : ' + str(self.canvas.getZ()))
if hits_not_upper_bound and hits_not_lower_bound:
self.canvas.setZ(self.c_new_pos)
elif not hits_not_upper_bound:
self.canvas.setZ(self.c_scroll_start)
elif not hits_not_lower_bound:
self.canvas.setZ(self.c_scroll_stop)
self.old_mY = mY
return task.again
def _scroll_fade_task(self, task):
if self.m_diff is None or abs(self.m_diff) < 0.005:
self.m_diff = 0
return task.done
old_c = self.canvas.getZ()
n = len(self.item_list)
self.c_scroll_start = 0
self.c_scroll_stop = (
(n * (self.i_y + self.item_v_padding)) + # Size of all elements with padding
self.item_v_padding - # Add one padding for the bottom
self.c_y) # Substract the length of the canvas
hits_not_upper_bound = (old_c - self.m_diff) >= self.c_scroll_start
hits_not_lower_bound = (old_c - self.m_diff) <= self.c_scroll_stop
if hits_not_upper_bound and hits_not_lower_bound:
self.canvas.setZ(old_c - self.m_diff)
self.m_diff *= 0.85
return task.again
elif not hits_not_upper_bound:
self.canvas.setZ(self.c_scroll_start)
self.m_diff = 0
return task.done
elif not hits_not_lower_bound:
self.canvas.setZ(self.c_scroll_stop)
self.m_diff = 0
return task.done
def add_item(self,
image=None,
image_scale=0.15,
image_pos=(0, 0, 0),
title=None,
title_pos=(0, 0, 0),
text=None,
text_pos=(0, 0, 0),
value=None):
'''Appends an item to the end of the list. It can hold an image,
title and text. Image: Panda3d path eg. 'models/picture.jpg'.
Value: The item has function 'get/set_value()' to work with individual
values of an activated element. Value gets set to the item on adding it.'''
item_nr = len(self.item_list) + 1
item_pos = self.i_start - (self.i_y + self.item_v_padding)*item_nr
item = DirectFrame(parent=self.canvas,
text=str(item_nr), # Abused as an ID tag
text_fg=(0, 0, 0, 0),
frameSize=self.i_size,
frameColor=self.item_background,
borderWidth=(0.01, 0.01),
pos=(0, 0, item_pos),
relief=DGG.FLAT,
state=DGG.NORMAL,
enableEdit=0,
suppressMouse=0)
item.bind(DGG.B1RELEASE, self._switch_active_item, [item])
if image is not None:
OnscreenImage( # Add an Image
image=image,
pos=image_pos,
scale=(1 * image_scale, 1, 1 * image_scale),
parent=(item))
DirectLabel(parent=item, # Add a Title
text=title,
text_scale=self.i_y / 4,
text_fg=(1, 1, 1, 1),
text_align=TextNode.ALeft,
frameColor=(0, 0, 0, 0),
pos=title_pos)
DirectLabel(parent=item, # Add a Text
text=text,
text_scale=self.i_y / 5,
text_fg=(1, 1, 1, 1),
text_align=TextNode.ALeft,
frameColor=(0, 0, 0, 0),
pos=text_pos)
self.item_list.append(item)
self.value_list.append(value)
def get_active_item(self):
return self.active_item
def get_active_id(self):
return int(self.active_item['text'])
def get_active_value(self):
return self.value_list[int(self.active_item['text']) - 1]
def hide(self):
'''Triggers the DirectFrame.hide() of the main frame'''
self.frame.hide()
def show(self):
'''Triggers the DirectFrame.show() of the main frame'''
self.frame.show()
def clear(self):
'''Destroys every item that was added to the list'''
for item in self.item_list:
item.destroy()
self.item_list = []
self.active_item = None
self.value_list = []
self.canvas.setZ(0)
def destroy(self):
'''Destroys the whole list and every item in it'''
self.canvas.destroy()
self.frame.destroy()