forked from panda3d/panda3d
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDirectOptionMenu.py
More file actions
311 lines (292 loc) · 13.2 KB
/
DirectOptionMenu.py
File metadata and controls
311 lines (292 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
"""Implements a pop-up menu containing multiple clickable options.
See the :ref:`directoptionmenu` page in the programming manual for a more
in-depth explanation and an example of how to use this class.
"""
__all__ = ['DirectOptionMenu']
from panda3d.core import *
from direct.showbase import ShowBaseGlobal
from . import DirectGuiGlobals as DGG
from .DirectButton import *
from .DirectLabel import *
from .DirectFrame import *
class DirectOptionMenu(DirectButton):
"""
DirectOptionMenu(parent) - Create a DirectButton which pops up a
menu which can be used to select from a list of items.
Execute button command (passing the selected item through) if defined
To cancel the popup menu click anywhere on the screen outside of the
popup menu. No command is executed in this case.
"""
def __init__(self, parent = None, **kw):
# Inherits from DirectButton
optiondefs = (
# List of items to display on the popup menu
('items', [], self.setItems),
# Initial item to display on menu button
# Can be an integer index or the same string as the button
('initialitem', None, DGG.INITOPT),
# Amount of padding to place around popup button indicator
('popupMarkerBorder', (.1, .1), None),
# The initial position of the popup marker
('popupMarker_pos', None, None),
# Background color to use to highlight popup menu items
('highlightColor', (.5, .5, .5, 1), None),
# Extra scale to use on highlight popup menu items
('highlightScale', (1, 1), None),
# Alignment to use for text on popup menu button
# Changing this breaks button layout
('text_align', TextNode.ALeft, None),
# Remove press effect because it looks a bit funny
('pressEffect', 0, DGG.INITOPT),
)
# Merge keyword options with default options
self.defineoptions(kw, optiondefs)
# Initialize superclasses
DirectButton.__init__(self, parent)
# Record any user specified frame size
self.initFrameSize = self['frameSize']
# Create a small rectangular marker to distinguish this button
# as a popup menu button
self.popupMarker = self.createcomponent(
'popupMarker', (), None,
DirectFrame, (self,),
frameSize = (-0.5, 0.5, -0.2, 0.2),
scale = 0.4,
relief = DGG.RAISED)
# Record any user specified popup marker position
self.initPopupMarkerPos = self['popupMarker_pos']
# This needs to popup the menu too
self.popupMarker.bind(DGG.B1PRESS, self.showPopupMenu)
# Check if item is highlighted on release and select it if it is
self.popupMarker.bind(DGG.B1RELEASE, self.selectHighlightedIndex)
# Make popup marker have the same click sound
if self['clickSound']:
self.popupMarker.guiItem.setSound(
DGG.B1PRESS + self.popupMarker.guiId, self['clickSound'])
else:
self.popupMarker.guiItem.clearSound(DGG.B1PRESS + self.popupMarker.guiId)
# This is created when you set the menu's items
self.popupMenu = None
self.selectedIndex = None
self.highlightedIndex = None
if 'item_text_scale' in kw:
self._prevItemTextScale = kw['item_text_scale']
else:
self._prevItemTextScale = (1,1)
# A big screen encompassing frame to catch the cancel clicks
self.cancelFrame = self.createcomponent(
'cancelframe', (), None,
DirectFrame, (self,),
frameSize = (-1, 1, -1, 1),
relief = None,
state = 'normal')
# Make sure this is on top of all the other widgets
self.cancelFrame.setBin('gui-popup', 0)
self.cancelFrame.node().setBounds(OmniBoundingVolume())
self.cancelFrame.bind(DGG.B1PRESS, self.hidePopupMenu)
# Default action on press is to show popup menu
self.bind(DGG.B1PRESS, self.showPopupMenu)
# Check if item is highlighted on release and select it if it is
self.bind(DGG.B1RELEASE, self.selectHighlightedIndex)
# Call option initialization functions
self.initialiseoptions(DirectOptionMenu)
# Need to call this since we explicitly set frame size
self.resetFrameSize()
def setItems(self):
"""
self['items'] = itemList
Create new popup menu to reflect specified set of items
"""
# Remove old component if it exits
if self.popupMenu != None:
self.destroycomponent('popupMenu')
# Create new component
self.popupMenu = self.createcomponent('popupMenu', (), None,
DirectFrame,
(self,),
relief = 'raised',
)
# Make sure it is on top of all the other gui widgets
self.popupMenu.setBin('gui-popup', 0)
if not self['items']:
return
# Create a new component for each item
# Find the maximum extents of all items
itemIndex = 0
self.minX = self.maxX = self.minZ = self.maxZ = None
for item in self['items']:
c = self.createcomponent(
'item%d' % itemIndex, (), 'item',
DirectButton, (self.popupMenu,),
text = item, text_align = TextNode.ALeft,
command = lambda i = itemIndex: self.set(i))
bounds = c.getBounds()
if self.minX == None:
self.minX = bounds[0]
elif bounds[0] < self.minX:
self.minX = bounds[0]
if self.maxX == None:
self.maxX = bounds[1]
elif bounds[1] > self.maxX:
self.maxX = bounds[1]
if self.minZ == None:
self.minZ = bounds[2]
elif bounds[2] < self.minZ:
self.minZ = bounds[2]
if self.maxZ == None:
self.maxZ = bounds[3]
elif bounds[3] > self.maxZ:
self.maxZ = bounds[3]
itemIndex += 1
# Calc max width and height
self.maxWidth = self.maxX - self.minX
self.maxHeight = self.maxZ - self.minZ
# Adjust frame size for each item and bind actions to mouse events
for i in range(itemIndex):
item = self.component('item%d' %i)
# So entire extent of item's slot on popup is reactive to mouse
item['frameSize'] = (self.minX, self.maxX, self.minZ, self.maxZ)
# Move it to its correct position on the popup
item.setPos(-self.minX, 0, -self.maxZ - i * self.maxHeight)
item.bind(DGG.B1RELEASE, self.hidePopupMenu)
# Highlight background when mouse is in item
item.bind(DGG.WITHIN,
lambda x, i=i, item=item:self._highlightItem(item, i))
# Restore specified color upon exiting
fc = item['frameColor']
item.bind(DGG.WITHOUT,
lambda x, item=item, fc=fc: self._unhighlightItem(item, fc))
# Set popup menu frame size to encompass all items
f = self.component('popupMenu')
f['frameSize'] = (0, self.maxWidth, -self.maxHeight * itemIndex, 0)
# Determine what initial item to display and set text accordingly
if self['initialitem']:
self.set(self['initialitem'], fCommand = 0)
else:
# No initial item specified, just use first item
self.set(0, fCommand = 0)
# Position popup Marker to the right of the button
pm = self.popupMarker
pmw = (pm.getWidth() * pm.getScale()[0] +
2 * self['popupMarkerBorder'][0])
if self.initFrameSize:
# Use specified frame size
bounds = list(self.initFrameSize)
else:
# Or base it upon largest item
bounds = [self.minX, self.maxX, self.minZ, self.maxZ]
if self.initPopupMarkerPos:
# Use specified position
pmPos = list(self.initPopupMarkerPos)
else:
# Or base the position on the frame size.
pmPos = [bounds[1] + pmw/2.0, 0, bounds[2] + (bounds[3] - bounds[2])/2.0]
pm.setPos(pmPos[0], pmPos[1], pmPos[2])
# Adjust popup menu button to fit all items (or use user specified
# frame size
bounds[1] += pmw
self['frameSize'] = (bounds[0], bounds[1], bounds[2], bounds[3])
# Set initial state
self.hidePopupMenu()
def showPopupMenu(self, event = None):
"""
Make popup visible and try to position it just to right of
mouse click with currently selected item aligned with button.
Adjust popup position if default position puts it outside of
visible screen region
"""
# Needed attributes (such as minZ) won't be set unless the user has specified
# items to display. Let's assert that we've given items to work with.
items = self['items']
assert items and len(items) > 0, 'Cannot show an empty popup menu! You must add items!'
# Show the menu
self.popupMenu.show()
# Make sure its at the right scale
self.popupMenu.setScale(self, VBase3(1))
# Compute bounds
b = self.getBounds()
fb = self.popupMenu.getBounds()
# Position menu at midpoint of button
xPos = (b[1] - b[0])/2.0 - fb[0]
self.popupMenu.setX(self, xPos)
# Try to set height to line up selected item with button
self.popupMenu.setZ(
self, self.minZ + (self.selectedIndex + 1)*self.maxHeight)
# Make sure the whole popup menu is visible
pos = self.popupMenu.getPos(ShowBaseGlobal.render2d)
scale = self.popupMenu.getScale(ShowBaseGlobal.render2d)
# How are we doing relative to the right side of the screen
maxX = pos[0] + fb[1] * scale[0]
if maxX > 1.0:
# Need to move menu to the left
self.popupMenu.setX(ShowBaseGlobal.render2d, pos[0] + (1.0 - maxX))
# How about up and down?
minZ = pos[2] + fb[2] * scale[2]
maxZ = pos[2] + fb[3] * scale[2]
if minZ < -1.0:
# Menu too low, move it up
self.popupMenu.setZ(ShowBaseGlobal.render2d, pos[2] + (-1.0 - minZ))
elif maxZ > 1.0:
# Menu too high, move it down
self.popupMenu.setZ(ShowBaseGlobal.render2d, pos[2] + (1.0 - maxZ))
# Also display cancel frame to catch clicks outside of the popup
self.cancelFrame.show()
# Position and scale cancel frame to fill entire window
self.cancelFrame.setPos(ShowBaseGlobal.render2d, 0, 0, 0)
self.cancelFrame.setScale(ShowBaseGlobal.render2d, 1, 1, 1)
def hidePopupMenu(self, event = None):
""" Put away popup and cancel frame """
self.popupMenu.hide()
self.cancelFrame.hide()
def _highlightItem(self, item, index):
""" Set frame color of highlighted item, record index """
self._prevItemTextScale = item['text_scale']
item['frameColor'] = self['highlightColor']
item['frameSize'] = (self['highlightScale'][0]*self.minX, self['highlightScale'][0]*self.maxX, self['highlightScale'][1]*self.minZ, self['highlightScale'][1]*self.maxZ)
item['text_scale'] = self['highlightScale']
self.highlightedIndex = index
def _unhighlightItem(self, item, frameColor):
""" Clear frame color, clear highlightedIndex """
item['frameColor'] = frameColor
item['frameSize'] = (self.minX, self.maxX, self.minZ, self.maxZ)
item['text_scale'] = self._prevItemTextScale
self.highlightedIndex = None
def selectHighlightedIndex(self, event = None):
"""
Check to see if item is highlighted (by cursor being within
that item). If so, selected it. If not, do nothing
"""
if self.highlightedIndex is not None:
self.set(self.highlightedIndex)
self.hidePopupMenu()
def index(self, index):
intIndex = None
if isinstance(index, int):
intIndex = index
elif index in self['items']:
i = 0
for item in self['items']:
if item == index:
intIndex = i
break
i += 1
return intIndex
def set(self, index, fCommand = 1):
# 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 and self['command']:
# Pass any extra args to command
self['command'](*[item] + self['extraArgs'])
def get(self):
""" Get currently selected item """
return self['items'][self.selectedIndex]
def commandFunc(self, event):
"""
Override popup menu button's command func
Command is executed in response to selecting menu items
"""
pass