ya2 · news · projects · code · about

housekeeping: ya2 module
[pmachines.git] / pmachines / gui / menu.py
CommitLineData
94a18c21 1from logging import info, debug
2612dfc8
FC
2from sys import platform, exit
3from os import environ, system
4from webbrowser import open_new_tab
158b04a8 5from xmlrpc.client import ServerProxy
2612dfc8 6from panda3d.core import Texture, TextNode, WindowProperties, LVector2i, \
bf77b5d5 7 TextProperties, TextPropertiesManager
4071c6d8 8from direct.gui.DirectGui import DirectButton, DirectCheckButton, \
e65a09cf 9 DirectOptionMenu, DirectSlider
4071c6d8 10from direct.gui.DirectGuiGlobals import FLAT
e1e44d5c 11from direct.gui.OnscreenText import OnscreenText
74ed9732 12from direct.showbase.DirectObject import DirectObject
b35b1f62 13from ya2.utils.cursor import MouseCursor
bf77b5d5 14from ya2.utils.gfx import GfxTools, DirectGuiMixin
4071c6d8
FC
15
16
ea71df6e
FC
17class 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
74ed9732 29class Menu(DirectObject):
4071c6d8 30
e65a09cf
FC
31 def __init__(self, fsm, lang_mgr, opt_file, music, pipeline, scenes,
32 fun_test, pos_mgr):
74ed9732 33 super().__init__()
4071c6d8
FC
34 self._fsm = fsm
35 self._lang_mgr = lang_mgr
36 self._opt_file = opt_file
e1e44d5c 37 self._music = music
a9aba267 38 self._pipeline = pipeline
6168d0c2 39 self._scenes = scenes
e982cdde 40 self._fun_test = fun_test
2d1773b1 41 self._pos_mgr = pos_mgr
74ed9732 42 self._enforced_res = ''
407412a5 43 self._cursor = MouseCursor(
e65a09cf
FC
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')
4071c6d8
FC
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)
e1e44d5c 52 self._widgets = []
4071c6d8
FC
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),
e65a09cf
FC
59 'rolloverSound': loader.load_sfx(
60 'assets/audio/sfx/rollover.ogg'),
61 'clickSound': loader.load_sfx(
62 'assets/audio/sfx/click.ogg')}
75b89b83 63 self._common_btn = {'frameSize': (-4.8, 4.8, -.6, 1.2)} | self._common
6fff1464
FC
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]
e1e44d5c
FC
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']
4071c6d8
FC
85 self._set_main()
86
74ed9732
FC
87 def enforce_res(self, val):
88 self._enforced_res = val
89 info('enforced resolution: ' + val)
90
4071c6d8 91 def _set_main(self):
2d1773b1 92 self._pos_mgr.reset()
e1e44d5c
FC
93 self._widgets = []
94 self._widgets += [DirectButton(
4071c6d8
FC
95 text=_('Play'), pos=(0, 1, .6), command=self.on_play,
96 **self._common_btn)]
bf77b5d5
FC
97 self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
98 self._pos_mgr.register('play', self._widgets[-1].pos_pixel())
e1e44d5c 99 self._widgets += [DirectButton(
2612dfc8 100 text=_('Options'), pos=(0, 1, .2), command=self.on_options,
4071c6d8 101 **self._common_btn)]
bf77b5d5
FC
102 self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
103 self._pos_mgr.register('options', self._widgets[-1].pos_pixel())
e1e44d5c 104 self._widgets += [DirectButton(
2612dfc8 105 text=_('Credits'), pos=(0, 1, -.2), command=self.on_credits,
4071c6d8 106 **self._common_btn)]
bf77b5d5
FC
107 self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
108 self._pos_mgr.register('credits', self._widgets[-1].pos_pixel())
e65a09cf 109
158b04a8
FC
110 def btn_exit():
111 if self._fun_test:
a33967c7 112 ServerProxy('http://localhost:7000').destroy()
158b04a8 113 exit()
e1e44d5c 114 self._widgets += [DirectButton(
158b04a8 115 text=_('Exit'), pos=(0, 1, -.6), command=lambda: btn_exit(),
4071c6d8 116 **self._common_btn)]
bf77b5d5
FC
117 self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
118 self._pos_mgr.register('exit', self._widgets[-1].pos_pixel())
75b89b83 119 self._rearrange_width()
74ed9732 120 self.accept('enforce_resolution', self.enforce_res)
4071c6d8
FC
121
122 def _set_options(self):
2d1773b1 123 self._pos_mgr.reset()
e1e44d5c 124 self._widgets = []
4071c6d8
FC
125 self._lang_funcs = [lambda: _('English'), lambda: _('Italian')]
126 items = [fnc() for fnc in self._lang_funcs]
127 inititem = {
128 'en': _('English'),
129 'it': _('Italian')
130 }[self._opt_file['settings']['language']]
ea71df6e
FC
131
132 def lang_cb(comps):
133 self._pos_mgr.remove(['english', 'italian'])
134 for lng, btn in zip(['english', 'italian'], comps):
bf77b5d5 135 pos = btn.pos_pixel()
ea71df6e
FC
136 self._pos_mgr.register(lng, (pos[0] + 5, pos[1]))
137 btn = DirectOptionMenuTest(
4071c6d8 138 text=_('Language'), items=items, initialitem=inititem,
a9aba267 139 pos=(0, 1, .8), command=self.on_language, **self._common_opt)
bf77b5d5 140 btn.__class__ = type('DirectOptionMenuMixed', (DirectOptionMenu, DirectGuiMixin), {})
ea71df6e 141 btn._show_cb = lang_cb
4071c6d8
FC
142 btn.popupMenu['frameColor'] = self._common_btn['frameColor']
143 btn.popupMenu['relief'] = self._common_btn['relief']
e1e44d5c 144 self._widgets += [btn]
bf77b5d5 145 pos_lang = self._widgets[-1].pos_pixel()
2d1773b1 146 self._pos_mgr.register('languages', pos_lang)
e1e44d5c 147 self._widgets += [OnscreenText(
a9aba267 148 _('Volume'), pos=(-.1, .55), font=self._common['text_font'],
e1e44d5c
FC
149 scale=self._common['scale'], fg=self._common['text_fg'],
150 align=TextNode.A_right)]
151 self._widgets += [DirectSlider(
a9aba267 152 pos=(.5, 1, .57),
e1e44d5c
FC
153 value=self._opt_file['settings']['volume'],
154 command=self.on_volume,
155 **self._common_slider)]
bf77b5d5
FC
156 self._widgets[-1].__class__ = type('DirectSliderMixed', (DirectSlider, DirectGuiMixin), {})
157 vol_pos = self._widgets[-1].pos_pixel()
2d1773b1 158 self._pos_mgr.register('volume', vol_pos)
bf77b5d5 159 np_left = GfxTools.build_empty_node('left_slider')
f2ac3712
FC
160 np_left.set_pos(self._widgets[-1].get_net_transform().get_pos())
161 np_left.set_x(np_left.get_x() - self._widgets[-1].get_scale()[0])
bf77b5d5 162 lpos = np_left.pos_as_widget() # try with pos2d_pixel and remove pos_as_widget if ok
f2ac3712 163 self._pos_mgr.register('volume_0', lpos)
e1e44d5c 164 self._slider = self._widgets[-1]
6fff1464 165 self._widgets += [DirectCheckButton(
a9aba267 166 text=_('Fullscreen'), pos=(0, 1, .3), command=self.on_fullscreen,
6fff1464
FC
167 indicator_frameColor=self._common_opt['highlightColor'],
168 indicator_relief=self._common_btn['relief'],
169 indicatorValue=self._opt_file['settings']['fullscreen'],
170 **self._common_btn)]
bf77b5d5
FC
171 self._widgets[-1].__class__ = type('DirectCheckButtonMixed', (DirectCheckButton, DirectGuiMixin), {})
172 self._pos_mgr.register('fullscreen', self._widgets[-1].pos_pixel())
6fff1464
FC
173 res = self._opt_file['settings']['resolution']
174 d_i = base.pipe.get_display_information()
175 def _res(idx):
176 return d_i.get_display_mode_width(idx), \
177 d_i.get_display_mode_height(idx)
178 resolutions = [
179 _res(idx) for idx in range(d_i.get_total_display_modes())]
180 resolutions = list(set(resolutions))
181 resolutions = sorted(resolutions)
182 resolutions = [(str(_res[0]), str(_res[1])) for _res in resolutions]
183 resolutions = ['x'.join(_res) for _res in resolutions]
184 if not res:
185 res = resolutions[-1]
ea71df6e
FC
186
187 def res_cb(comps):
188 self._pos_mgr.remove(['res_1440x900', 'res_1360x768'])
189 for tgt_res in ['1440x900', '1360x768']:
190 for btn in comps:
191 if btn['text'] == tgt_res:
bf77b5d5 192 pos = btn.pos_pixel()
ea71df6e
FC
193 self._pos_mgr.register('res_' + tgt_res, (pos[0] + 5, pos[1]))
194 btn = DirectOptionMenuTest(
6fff1464 195 text=_('Resolution'), items=resolutions, initialitem=res,
a9aba267 196 pos=(0, 1, .05), command=self.on_resolution, **self._common_opt)
bf77b5d5 197 btn.__class__ = type('DirectOptionMenuMixed', (DirectOptionMenu, DirectGuiMixin), {})
ea71df6e 198 btn._show_cb = res_cb
6fff1464
FC
199 btn.popupMenu['frameColor'] = self._common_btn['frameColor']
200 btn.popupMenu['relief'] = self._common_btn['relief']
201 self._widgets += [btn]
bf77b5d5 202 pos_res = self._widgets[-1].pos_pixel()
2d1773b1
FC
203 self._pos_mgr.register('resolutions', pos_res) # 680 365
204 self._pos_mgr.register('res_1440x900', [pos_res[0] + 320, pos_res[1] + 75])
205 self._pos_mgr.register('res_1360x768', [pos_res[0] + 430, pos_res[1] -285])
a9aba267
FC
206 self._widgets += [DirectCheckButton(
207 text=_('Antialiasing'), pos=(0, 1, -.2), command=self.on_aa,
208 indicator_frameColor=self._common_opt['highlightColor'],
209 indicator_relief=self._common_btn['relief'],
210 indicatorValue=self._opt_file['settings']['antialiasing'],
211 **self._common_btn)]
bf77b5d5
FC
212 self._widgets[-1].__class__ = type('DirectCheckButtonMixed', (DirectCheckButton, DirectGuiMixin), {})
213 self._pos_mgr.register('aa', self._widgets[-1].pos_pixel())
5fdf77d0
FC
214 self._widgets += [DirectCheckButton(
215 text=_('Shadows'), pos=(0, 1, -.45), command=self.on_shadows,
216 indicator_frameColor=self._common_opt['highlightColor'],
217 indicator_relief=self._common_btn['relief'],
218 indicatorValue=self._opt_file['settings']['shadows'],
219 **self._common_btn)]
bf77b5d5
FC
220 self._widgets[-1].__class__ = type('DirectCheckButtonMixed', (DirectCheckButton, DirectGuiMixin), {})
221 self._pos_mgr.register('shadows', self._widgets[-1].pos_pixel())
e1e44d5c 222 self._widgets += [DirectButton(
a9aba267 223 text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
4071c6d8 224 **self._common_btn)]
bf77b5d5
FC
225 self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
226 self._pos_mgr.register('back', self._widgets[-1].pos_pixel())
74ed9732 227 self.accept('enforce_resolution', self.enforce_res)
4071c6d8 228
2612dfc8 229 def _set_credits(self):
2d1773b1 230 self._pos_mgr.reset()
2612dfc8
FC
231 self._widgets = []
232 tp_scale = TextProperties()
233 tp_scale.set_text_scale(.64)
234 TextPropertiesManager.getGlobalPtr().setProperties('scale', tp_scale)
235 self._widgets += [OnscreenText(
236 _('Code and gfx\n \1scale\1Flavio Calva\2\n\n\nMusic\n \1scale\1Stefan Grossmann\2'),
237 pos=(-.9, .55), font=self._common['text_font'],
238 scale=self._common['scale'], fg=self._common['text_fg'],
239 align=TextNode.A_left)]
240 self._widgets += [DirectButton(
241 text=_('Website'), pos=(-.6, 1, .29), command=self.on_website,
242 **self._common_btn | {'scale': .08})]
243 self._widgets += [OnscreenText(
e67dbe53 244 _('Special thanks to:\n \1scale\1rdb\2\n \1scale\1Luisa Tenuta\2\n \1scale\1Damiana Ercolani\2'),
2612dfc8
FC
245 pos=(.1, .55), font=self._common['text_font'],
246 scale=self._common['scale'], fg=self._common['text_fg'],
247 align=TextNode.A_left)]
248 self._widgets += [DirectButton(
249 text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
250 **self._common_btn)]
bf77b5d5
FC
251 self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
252 self._pos_mgr.register('back', self._widgets[-1].pos_pixel())
74ed9732 253 self.accept('enforce_resolution', self.enforce_res)
2612dfc8 254
4071c6d8 255 def on_play(self):
2d1773b1 256 self._pos_mgr.reset()
0d5a5427 257 self.destroy()
638deddf 258 self._cursor = MouseCursor(
7e487769 259 'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
638deddf 260 (.01, .01))
0d5a5427 261 self._widgets = []
8c9bf90e
FC
262 cmn = self._common_btn.copy() | {
263 'frameSize': (-2.4, 2.4, -2.4, 2.4),
bb956c26
FC
264 'frameColor': (1, 1, 1, .8),
265 'text_scale': .64}
6168d0c2 266 left = - (dx := .8) * (min(4, len(self._scenes)) - 1) / 2
f7eade0f
FC
267 from pmachines.scene import Scene
268 for i, scene_name in enumerate(self._scenes):
6168d0c2 269 top = .1 if len(self._scenes) < 5 else .6
a5fddddc 270 row = 0 if i < 4 else 1
92c29685 271 new_cmn = cmn.copy()
f7eade0f 272 if Scene.is_done(scene_name):
92c29685
FC
273 new_cmn['frameColor'] = (1, 1, 1, .4)
274 new_cmn['text_fg'] = (.9, .9, .9, .4)
28acea3a 275 self._widgets += [DirectButton(
f7eade0f
FC
276 text=Scene.name(scene_name), pos=(left + dx * (i % 4), 1, top - dx * row),
277 command=self.start, extraArgs=[scene_name], text_wordwrap=6,
278 frameTexture='assets/images/scenes/%s.dds' % scene_name,
92c29685 279 **new_cmn)]
bf77b5d5 280 self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
f7eade0f 281 name = scene_name.lower()
bf77b5d5 282 self._pos_mgr.register(name, self._widgets[-1].pos_pixel())
bb956c26
FC
283 for j in range(4):
284 tnode = self._widgets[-1].component('text%s' % j).textNode
285 height = - tnode.getLineHeight() / 2
286 height += (tnode.get_height() - tnode.get_line_height()) / 2
287 self._widgets[-1].component('text%s' % j).set_pos(0, 0, height)
0d5a5427
FC
288 self._widgets += [DirectButton(
289 text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
290 **self._common_btn)]
bf77b5d5
FC
291 self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
292 self._pos_mgr.register('back', self._widgets[-1].pos_pixel())
0d5a5427 293
8c9bf90e
FC
294 def start(self, cls):
295 self._fsm.demand('Scene', cls)
4071c6d8
FC
296
297 def on_options(self):
298 self.destroy()
638deddf 299 self._cursor = MouseCursor(
7e487769 300 'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
638deddf 301 (.01, .01))
4071c6d8
FC
302 self._set_options()
303
2612dfc8
FC
304 def on_credits(self):
305 self.destroy()
638deddf 306 self._cursor = MouseCursor(
7e487769 307 'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
638deddf 308 (.01, .01))
2612dfc8
FC
309 self._set_credits()
310
75b89b83
FC
311 def _rearrange_width(self):
312 max_width = 0
313 for wdg in self._widgets:
314 t_n = wdg.component('text0')
315 u_l = t_n.textNode.get_upper_left_3d()
316 l_r = t_n.textNode.get_lower_right_3d()
317 max_width = max(l_r[0] - u_l[0], max_width)
318 for wdg in self._widgets:
319 m_w = max_width / 2 + .8
320 wdg['frameSize'] = -m_w, m_w, wdg['frameSize'][2], wdg['frameSize'][3]
321
4071c6d8
FC
322 def on_language(self, arg):
323 lang_code = {
324 _('English'): 'en_EN',
325 _('Italian'): 'it_IT'}[arg]
326 self._lang_mgr.set_lang(lang_code)
327 self._opt_file['settings']['language'] = lang_code[:2]
328 self._opt_file.store()
329 self.on_options()
330
e1e44d5c
FC
331 def on_volume(self):
332 self._opt_file['settings']['volume'] = self._slider['value']
333 self._music.set_volume(self._slider['value'])
334
6fff1464
FC
335 def on_fullscreen(self, arg):
336 props = WindowProperties()
337 props.set_fullscreen(arg)
e982cdde
FC
338 if not self._fun_test:
339 base.win.request_properties(props)
340 # if we actually switch to fullscreen during the tests then
341 # exwm inside qemu can't restore the correct resolution
342 # i may re-enable this if/when i run the tests onto a
343 # physical machine
6fff1464
FC
344 self._opt_file['settings']['fullscreen'] = int(arg)
345 self._opt_file.store()
346
347 def on_resolution(self, arg):
74ed9732
FC
348 info('on resolution: %s (%s)' % (arg, self._enforced_res))
349 arg = self._enforced_res or arg
350 info('set resolution: %s' % arg)
6fff1464
FC
351 props = WindowProperties()
352 props.set_size(LVector2i(*[int(_res) for _res in arg.split('x')]))
353 base.win.request_properties(props)
354 self._opt_file['settings']['resolution'] = arg
355 self._opt_file.store()
356
a9aba267
FC
357 def on_aa(self, arg):
358 self._pipeline.msaa_samples = 4 if arg else 1
94a18c21 359 debug(f'msaa: {self._pipeline.msaa_samples}')
a9aba267
FC
360 self._opt_file['settings']['antialiasing'] = int(arg)
361 self._opt_file.store()
362
5fdf77d0
FC
363 def on_shadows(self, arg):
364 self._pipeline.enable_shadows = int(arg)
94a18c21 365 debug(f'shadows: {self._pipeline.enable_shadows}')
5fdf77d0
FC
366 self._opt_file['settings']['shadows'] = int(arg)
367 self._opt_file.store()
368
2612dfc8
FC
369 def on_website(self):
370 if platform.startswith('linux'):
371 environ['LD_LIBRARY_PATH'] = ''
372 system('xdg-open https://www.ya2.it')
373 else:
374 open_new_tab('https://www.ya2.it')
375
4071c6d8 376 def on_back(self):
e1e44d5c 377 self._opt_file.store()
4071c6d8 378 self.destroy()
638deddf 379 self._cursor = MouseCursor(
7e487769 380 'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
638deddf 381 (.01, .01))
4071c6d8
FC
382 self._set_main()
383
384 def destroy(self):
e1e44d5c 385 [wdg.destroy() for wdg in self._widgets]
407412a5 386 self._cursor.destroy()
74ed9732 387 self.ignore('enforce_resolution')