ya2 · news · projects · code · about

fixes for building
[pmachines.git] / pmachines / gui / menu.py
1 from logging import info, debug
2 from sys import platform, exit
3 from os import environ, system
4 from webbrowser import open_new_tab
5 from xmlrpc.client import ServerProxy
6 from panda3d.core import Texture, TextNode, WindowProperties, LVector2i, \
7 TextProperties, TextPropertiesManager
8 from direct.gui.DirectGui import DirectButton, DirectCheckButton, \
9 DirectOptionMenu, DirectSlider
10 from direct.gui.DirectGuiGlobals import FLAT
11 from direct.gui.OnscreenText import OnscreenText
12 from direct.showbase.DirectObject import DirectObject
13 from ya2.utils.cursor import MouseCursor
14 from ya2.p3d.p3d import LibP3d
15
16
17 class DirectOptionMenuTest(DirectOptionMenu):
18
19 def __init__(self, parent=None, **kw):
20 DirectOptionMenu.__init__(self, parent, **kw)
21 self.initialiseoptions(DirectOptionMenuTest)
22
23 def showPopupMenu(self, event=None):
24 super().showPopupMenu(event)
25 self._show_cb([self.component(cmp) for cmp in self.components()])
26
27
28
29 class Menu(DirectObject):
30
31 def __init__(self, fsm, lang_mgr, opt_file, music, pipeline, scenes,
32 fun_test, pos_mgr):
33 super().__init__()
34 self._fsm = fsm
35 self._lang_mgr = lang_mgr
36 self._opt_file = opt_file
37 self._music = music
38 self._pipeline = pipeline
39 self._scenes = scenes
40 self._fun_test = fun_test
41 self._pos_mgr = pos_mgr
42 self._enforced_res = ''
43 self._cursor = MouseCursor(
44 'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04),
45 (.5, .5, .5, 1), (.01, .01))
46 self._font = base.loader.load_font(
47 'assets/fonts/Hanken-Book.ttf')
48 self._font.clear()
49 self._font.set_pixels_per_unit(60)
50 self._font.set_minfilter(Texture.FTLinearMipmapLinear)
51 self._font.set_outline((0, 0, 0, 1), .8, .2)
52 self._widgets = []
53 self._common = {
54 'scale': .12,
55 'text_font': self._font,
56 'text_fg': (.9, .9, .9, 1),
57 'relief': FLAT,
58 'frameColor': (.4, .4, .4, .14),
59 'rolloverSound': loader.load_sfx(
60 'assets/audio/sfx/rollover.ogg'),
61 'clickSound': loader.load_sfx(
62 'assets/audio/sfx/click.ogg')}
63 self._common_btn = {'frameSize': (-4.8, 4.8, -.6, 1.2)} | self._common
64 hlc = self._common_btn['frameColor']
65 hlc = (hlc[0] + .2, hlc[1] + .2, hlc[2] + .2, hlc[3] + .2)
66 self._common_opt = {
67 'item_frameColor': self._common_btn['frameColor'],
68 'popupMarker_frameColor': self._common_btn['frameColor'],
69 'item_relief': self._common_btn['relief'],
70 'item_text_font': self._common_btn['text_font'],
71 'item_text_fg': self._common_btn['text_fg'],
72 'textMayChange': 1,
73 'highlightColor': hlc,
74 'text_align': TextNode.A_center,
75 } | self._common_btn
76 f_s = self._common_opt['frameSize']
77 self._common_opt['frameSize'] = f_s[0], f_s[1] - .56, f_s[2], f_s[3]
78 self._common_slider = self._common | {
79 'range': (0, 1),
80 'thumb_frameColor': (.4, .4, .4, .4),
81 'thumb_scale': 1.6,
82 'scale': .4}
83 del self._common_slider['rolloverSound']
84 del self._common_slider['clickSound']
85 self._set_main()
86
87 def enforce_res(self, val):
88 self._enforced_res = val
89 info('enforced resolution: ' + val)
90
91 def _set_main(self):
92 self._pos_mgr.reset()
93 self._widgets = []
94 self._widgets += [DirectButton(
95 text=_('Play'), pos=(0, 1, .6), command=self.on_play,
96 **self._common_btn)]
97 self._pos_mgr.register('play', LibP3d.wdg_pos(self._widgets[-1]))
98 self._widgets += [DirectButton(
99 text=_('Options'), pos=(0, 1, .2), command=self.on_options,
100 **self._common_btn)]
101 self._pos_mgr.register('options', LibP3d.wdg_pos(self._widgets[-1]))
102 self._widgets += [DirectButton(
103 text=_('Credits'), pos=(0, 1, -.2), command=self.on_credits,
104 **self._common_btn)]
105 self._pos_mgr.register('credits', LibP3d.wdg_pos(self._widgets[-1]))
106
107 def btn_exit():
108 if self._fun_test:
109 ServerProxy('http://localhost:6000').destroy()
110 exit()
111 self._widgets += [DirectButton(
112 text=_('Exit'), pos=(0, 1, -.6), command=lambda: btn_exit(),
113 **self._common_btn)]
114 self._pos_mgr.register('exit', LibP3d.wdg_pos(self._widgets[-1]))
115 self._rearrange_width()
116 self.accept('enforce_resolution', self.enforce_res)
117
118 def _set_options(self):
119 self._pos_mgr.reset()
120 self._widgets = []
121 self._lang_funcs = [lambda: _('English'), lambda: _('Italian')]
122 items = [fnc() for fnc in self._lang_funcs]
123 inititem = {
124 'en': _('English'),
125 'it': _('Italian')
126 }[self._opt_file['settings']['language']]
127
128 def lang_cb(comps):
129 self._pos_mgr.remove(['english', 'italian'])
130 for lng, btn in zip(['english', 'italian'], comps):
131 pos = LibP3d.wdg_pos(btn)
132 self._pos_mgr.register(lng, (pos[0] + 5, pos[1]))
133 btn = DirectOptionMenuTest(
134 text=_('Language'), items=items, initialitem=inititem,
135 pos=(0, 1, .8), command=self.on_language, **self._common_opt)
136 btn._show_cb = lang_cb
137 btn.popupMenu['frameColor'] = self._common_btn['frameColor']
138 btn.popupMenu['relief'] = self._common_btn['relief']
139 self._widgets += [btn]
140 pos_lang = LibP3d.wdg_pos(self._widgets[-1])
141 self._pos_mgr.register('languages', pos_lang)
142 self._widgets += [OnscreenText(
143 _('Volume'), pos=(-.1, .55), font=self._common['text_font'],
144 scale=self._common['scale'], fg=self._common['text_fg'],
145 align=TextNode.A_right)]
146 self._widgets += [DirectSlider(
147 pos=(.5, 1, .57),
148 value=self._opt_file['settings']['volume'],
149 command=self.on_volume,
150 **self._common_slider)]
151 vol_pos = LibP3d.wdg_pos(self._widgets[-1])
152 self._pos_mgr.register('volume', vol_pos)
153 self._pos_mgr.register('volume_0', [vol_pos[0] - 153, vol_pos[1]])
154 self._slider = self._widgets[-1]
155 self._widgets += [DirectCheckButton(
156 text=_('Fullscreen'), pos=(0, 1, .3), command=self.on_fullscreen,
157 indicator_frameColor=self._common_opt['highlightColor'],
158 indicator_relief=self._common_btn['relief'],
159 indicatorValue=self._opt_file['settings']['fullscreen'],
160 **self._common_btn)]
161 self._pos_mgr.register('fullscreen', LibP3d.wdg_pos(self._widgets[-1]))
162 res = self._opt_file['settings']['resolution']
163 d_i = base.pipe.get_display_information()
164 def _res(idx):
165 return d_i.get_display_mode_width(idx), \
166 d_i.get_display_mode_height(idx)
167 resolutions = [
168 _res(idx) for idx in range(d_i.get_total_display_modes())]
169 resolutions = list(set(resolutions))
170 resolutions = sorted(resolutions)
171 resolutions = [(str(_res[0]), str(_res[1])) for _res in resolutions]
172 resolutions = ['x'.join(_res) for _res in resolutions]
173 if not res:
174 res = resolutions[-1]
175
176 def res_cb(comps):
177 self._pos_mgr.remove(['res_1440x900', 'res_1360x768'])
178 for tgt_res in ['1440x900', '1360x768']:
179 for btn in comps:
180 if btn['text'] == tgt_res:
181 pos = LibP3d.wdg_pos(btn)
182 self._pos_mgr.register('res_' + tgt_res, (pos[0] + 5, pos[1]))
183 btn = DirectOptionMenuTest(
184 text=_('Resolution'), items=resolutions, initialitem=res,
185 pos=(0, 1, .05), command=self.on_resolution, **self._common_opt)
186 btn._show_cb = res_cb
187 btn.popupMenu['frameColor'] = self._common_btn['frameColor']
188 btn.popupMenu['relief'] = self._common_btn['relief']
189 self._widgets += [btn]
190 pos_res = LibP3d.wdg_pos(self._widgets[-1])
191 self._pos_mgr.register('resolutions', pos_res) # 680 365
192 self._pos_mgr.register('res_1440x900', [pos_res[0] + 320, pos_res[1] + 75])
193 self._pos_mgr.register('res_1360x768', [pos_res[0] + 430, pos_res[1] -285])
194 self._widgets += [DirectCheckButton(
195 text=_('Antialiasing'), pos=(0, 1, -.2), command=self.on_aa,
196 indicator_frameColor=self._common_opt['highlightColor'],
197 indicator_relief=self._common_btn['relief'],
198 indicatorValue=self._opt_file['settings']['antialiasing'],
199 **self._common_btn)]
200 self._pos_mgr.register('aa', LibP3d.wdg_pos(self._widgets[-1]))
201 self._widgets += [DirectCheckButton(
202 text=_('Shadows'), pos=(0, 1, -.45), command=self.on_shadows,
203 indicator_frameColor=self._common_opt['highlightColor'],
204 indicator_relief=self._common_btn['relief'],
205 indicatorValue=self._opt_file['settings']['shadows'],
206 **self._common_btn)]
207 self._pos_mgr.register('shadows', LibP3d.wdg_pos(self._widgets[-1]))
208 self._widgets += [DirectButton(
209 text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
210 **self._common_btn)]
211 self._pos_mgr.register('back', LibP3d.wdg_pos(self._widgets[-1]))
212 self.accept('enforce_resolution', self.enforce_res)
213
214 def _set_credits(self):
215 self._pos_mgr.reset()
216 self._widgets = []
217 tp_scale = TextProperties()
218 tp_scale.set_text_scale(.64)
219 TextPropertiesManager.getGlobalPtr().setProperties('scale', tp_scale)
220 self._widgets += [OnscreenText(
221 _('Code and gfx\n \1scale\1Flavio Calva\2\n\n\nMusic\n \1scale\1Stefan Grossmann\2'),
222 pos=(-.9, .55), font=self._common['text_font'],
223 scale=self._common['scale'], fg=self._common['text_fg'],
224 align=TextNode.A_left)]
225 self._widgets += [DirectButton(
226 text=_('Website'), pos=(-.6, 1, .29), command=self.on_website,
227 **self._common_btn | {'scale': .08})]
228 self._widgets += [OnscreenText(
229 _('Special thanks to:\n \1scale\1rdb\2\n \1scale\1Luisa Tenuta\2\n \1scale\1Damiana Ercolani\2'),
230 pos=(.1, .55), font=self._common['text_font'],
231 scale=self._common['scale'], fg=self._common['text_fg'],
232 align=TextNode.A_left)]
233 self._widgets += [DirectButton(
234 text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
235 **self._common_btn)]
236 self._pos_mgr.register('back', LibP3d.wdg_pos(self._widgets[-1]))
237 self.accept('enforce_resolution', self.enforce_res)
238
239 def on_play(self):
240 self._pos_mgr.reset()
241 self.destroy()
242 self._cursor = MouseCursor(
243 'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
244 (.01, .01))
245 self._widgets = []
246 cmn = self._common_btn.copy() | {
247 'frameSize': (-2.4, 2.4, -2.4, 2.4),
248 'frameColor': (1, 1, 1, .8),
249 'text_scale': .64}
250 left = - (dx := .8) * (min(4, len(self._scenes)) - 1) / 2
251 for i, cls in enumerate(self._scenes):
252 top = .1 if len(self._scenes) < 5 else .6
253 row = 0 if i < 4 else 1
254 self._widgets += [DirectButton(
255 text=cls.name(), pos=(left + dx * (i % 4), 1, top - dx * row),
256 command=self.start, extraArgs=[cls], text_wordwrap=6,
257 frameTexture='assets/images/scenes/%s.dds' % cls.__name__,
258 **cmn)]
259 name = cls.__name__[5:].lower()
260 self._pos_mgr.register(name, LibP3d.wdg_pos(self._widgets[-1]))
261 for j in range(4):
262 tnode = self._widgets[-1].component('text%s' % j).textNode
263 height = - tnode.getLineHeight() / 2
264 height += (tnode.get_height() - tnode.get_line_height()) / 2
265 self._widgets[-1].component('text%s' % j).set_pos(0, 0, height)
266 self._widgets += [DirectButton(
267 text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
268 **self._common_btn)]
269 self._pos_mgr.register('back', LibP3d.wdg_pos(self._widgets[-1]))
270
271 def start(self, cls):
272 self._fsm.demand('Scene', cls)
273
274 def on_options(self):
275 self.destroy()
276 self._cursor = MouseCursor(
277 'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
278 (.01, .01))
279 self._set_options()
280
281 def on_credits(self):
282 self.destroy()
283 self._cursor = MouseCursor(
284 'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
285 (.01, .01))
286 self._set_credits()
287
288 def _rearrange_width(self):
289 max_width = 0
290 for wdg in self._widgets:
291 t_n = wdg.component('text0')
292 u_l = t_n.textNode.get_upper_left_3d()
293 l_r = t_n.textNode.get_lower_right_3d()
294 max_width = max(l_r[0] - u_l[0], max_width)
295 for wdg in self._widgets:
296 m_w = max_width / 2 + .8
297 wdg['frameSize'] = -m_w, m_w, wdg['frameSize'][2], wdg['frameSize'][3]
298
299 def on_language(self, arg):
300 lang_code = {
301 _('English'): 'en_EN',
302 _('Italian'): 'it_IT'}[arg]
303 self._lang_mgr.set_lang(lang_code)
304 self._opt_file['settings']['language'] = lang_code[:2]
305 self._opt_file.store()
306 self.on_options()
307
308 def on_volume(self):
309 self._opt_file['settings']['volume'] = self._slider['value']
310 self._music.set_volume(self._slider['value'])
311
312 def on_fullscreen(self, arg):
313 props = WindowProperties()
314 props.set_fullscreen(arg)
315 if not self._fun_test:
316 base.win.request_properties(props)
317 # if we actually switch to fullscreen during the tests then
318 # exwm inside qemu can't restore the correct resolution
319 # i may re-enable this if/when i run the tests onto a
320 # physical machine
321 self._opt_file['settings']['fullscreen'] = int(arg)
322 self._opt_file.store()
323
324 def on_resolution(self, arg):
325 info('on resolution: %s (%s)' % (arg, self._enforced_res))
326 arg = self._enforced_res or arg
327 info('set resolution: %s' % arg)
328 props = WindowProperties()
329 props.set_size(LVector2i(*[int(_res) for _res in arg.split('x')]))
330 base.win.request_properties(props)
331 self._opt_file['settings']['resolution'] = arg
332 self._opt_file.store()
333
334 def on_aa(self, arg):
335 self._pipeline.msaa_samples = 4 if arg else 1
336 debug(f'msaa: {self._pipeline.msaa_samples}')
337 self._opt_file['settings']['antialiasing'] = int(arg)
338 self._opt_file.store()
339
340 def on_shadows(self, arg):
341 self._pipeline.enable_shadows = int(arg)
342 debug(f'shadows: {self._pipeline.enable_shadows}')
343 self._opt_file['settings']['shadows'] = int(arg)
344 self._opt_file.store()
345
346 def on_website(self):
347 if platform.startswith('linux'):
348 environ['LD_LIBRARY_PATH'] = ''
349 system('xdg-open https://www.ya2.it')
350 else:
351 open_new_tab('https://www.ya2.it')
352
353 def on_back(self):
354 self._opt_file.store()
355 self.destroy()
356 self._cursor = MouseCursor(
357 'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
358 (.01, .01))
359 self._set_main()
360
361 def destroy(self):
362 [wdg.destroy() for wdg in self._widgets]
363 self._cursor.destroy()
364 self.ignore('enforce_resolution')