ya2 · news · projects · code · about

3fa571d8668ff076eb786cfb6a2e00b806c16acb
[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, NodePath
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 np_left = NodePath('left_slider')
154 np_left.set_pos(self._widgets[-1].get_net_transform().get_pos())
155 np_left.set_x(np_left.get_x() - self._widgets[-1].get_scale()[0])
156 lpos = LibP3d.wdg_pos(np_left)
157 self._pos_mgr.register('volume_0', lpos)
158 self._slider = self._widgets[-1]
159 self._widgets += [DirectCheckButton(
160 text=_('Fullscreen'), pos=(0, 1, .3), command=self.on_fullscreen,
161 indicator_frameColor=self._common_opt['highlightColor'],
162 indicator_relief=self._common_btn['relief'],
163 indicatorValue=self._opt_file['settings']['fullscreen'],
164 **self._common_btn)]
165 self._pos_mgr.register('fullscreen', LibP3d.wdg_pos(self._widgets[-1]))
166 res = self._opt_file['settings']['resolution']
167 d_i = base.pipe.get_display_information()
168 def _res(idx):
169 return d_i.get_display_mode_width(idx), \
170 d_i.get_display_mode_height(idx)
171 resolutions = [
172 _res(idx) for idx in range(d_i.get_total_display_modes())]
173 resolutions = list(set(resolutions))
174 resolutions = sorted(resolutions)
175 resolutions = [(str(_res[0]), str(_res[1])) for _res in resolutions]
176 resolutions = ['x'.join(_res) for _res in resolutions]
177 if not res:
178 res = resolutions[-1]
179
180 def res_cb(comps):
181 self._pos_mgr.remove(['res_1440x900', 'res_1360x768'])
182 for tgt_res in ['1440x900', '1360x768']:
183 for btn in comps:
184 if btn['text'] == tgt_res:
185 pos = LibP3d.wdg_pos(btn)
186 self._pos_mgr.register('res_' + tgt_res, (pos[0] + 5, pos[1]))
187 btn = DirectOptionMenuTest(
188 text=_('Resolution'), items=resolutions, initialitem=res,
189 pos=(0, 1, .05), command=self.on_resolution, **self._common_opt)
190 btn._show_cb = res_cb
191 btn.popupMenu['frameColor'] = self._common_btn['frameColor']
192 btn.popupMenu['relief'] = self._common_btn['relief']
193 self._widgets += [btn]
194 pos_res = LibP3d.wdg_pos(self._widgets[-1])
195 self._pos_mgr.register('resolutions', pos_res) # 680 365
196 self._pos_mgr.register('res_1440x900', [pos_res[0] + 320, pos_res[1] + 75])
197 self._pos_mgr.register('res_1360x768', [pos_res[0] + 430, pos_res[1] -285])
198 self._widgets += [DirectCheckButton(
199 text=_('Antialiasing'), pos=(0, 1, -.2), command=self.on_aa,
200 indicator_frameColor=self._common_opt['highlightColor'],
201 indicator_relief=self._common_btn['relief'],
202 indicatorValue=self._opt_file['settings']['antialiasing'],
203 **self._common_btn)]
204 self._pos_mgr.register('aa', LibP3d.wdg_pos(self._widgets[-1]))
205 self._widgets += [DirectCheckButton(
206 text=_('Shadows'), pos=(0, 1, -.45), command=self.on_shadows,
207 indicator_frameColor=self._common_opt['highlightColor'],
208 indicator_relief=self._common_btn['relief'],
209 indicatorValue=self._opt_file['settings']['shadows'],
210 **self._common_btn)]
211 self._pos_mgr.register('shadows', LibP3d.wdg_pos(self._widgets[-1]))
212 self._widgets += [DirectButton(
213 text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
214 **self._common_btn)]
215 self._pos_mgr.register('back', LibP3d.wdg_pos(self._widgets[-1]))
216 self.accept('enforce_resolution', self.enforce_res)
217
218 def _set_credits(self):
219 self._pos_mgr.reset()
220 self._widgets = []
221 tp_scale = TextProperties()
222 tp_scale.set_text_scale(.64)
223 TextPropertiesManager.getGlobalPtr().setProperties('scale', tp_scale)
224 self._widgets += [OnscreenText(
225 _('Code and gfx\n \1scale\1Flavio Calva\2\n\n\nMusic\n \1scale\1Stefan Grossmann\2'),
226 pos=(-.9, .55), font=self._common['text_font'],
227 scale=self._common['scale'], fg=self._common['text_fg'],
228 align=TextNode.A_left)]
229 self._widgets += [DirectButton(
230 text=_('Website'), pos=(-.6, 1, .29), command=self.on_website,
231 **self._common_btn | {'scale': .08})]
232 self._widgets += [OnscreenText(
233 _('Special thanks to:\n \1scale\1rdb\2\n \1scale\1Luisa Tenuta\2\n \1scale\1Damiana Ercolani\2'),
234 pos=(.1, .55), font=self._common['text_font'],
235 scale=self._common['scale'], fg=self._common['text_fg'],
236 align=TextNode.A_left)]
237 self._widgets += [DirectButton(
238 text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
239 **self._common_btn)]
240 self._pos_mgr.register('back', LibP3d.wdg_pos(self._widgets[-1]))
241 self.accept('enforce_resolution', self.enforce_res)
242
243 def on_play(self):
244 self._pos_mgr.reset()
245 self.destroy()
246 self._cursor = MouseCursor(
247 'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
248 (.01, .01))
249 self._widgets = []
250 cmn = self._common_btn.copy() | {
251 'frameSize': (-2.4, 2.4, -2.4, 2.4),
252 'frameColor': (1, 1, 1, .8),
253 'text_scale': .64}
254 left = - (dx := .8) * (min(4, len(self._scenes)) - 1) / 2
255 for i, cls in enumerate(self._scenes):
256 top = .1 if len(self._scenes) < 5 else .6
257 row = 0 if i < 4 else 1
258 self._widgets += [DirectButton(
259 text=cls.name(), pos=(left + dx * (i % 4), 1, top - dx * row),
260 command=self.start, extraArgs=[cls], text_wordwrap=6,
261 frameTexture='assets/images/scenes/%s.dds' % cls.__name__,
262 **cmn)]
263 name = cls.__name__[5:].lower()
264 self._pos_mgr.register(name, LibP3d.wdg_pos(self._widgets[-1]))
265 for j in range(4):
266 tnode = self._widgets[-1].component('text%s' % j).textNode
267 height = - tnode.getLineHeight() / 2
268 height += (tnode.get_height() - tnode.get_line_height()) / 2
269 self._widgets[-1].component('text%s' % j).set_pos(0, 0, height)
270 self._widgets += [DirectButton(
271 text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
272 **self._common_btn)]
273 self._pos_mgr.register('back', LibP3d.wdg_pos(self._widgets[-1]))
274
275 def start(self, cls):
276 self._fsm.demand('Scene', cls)
277
278 def on_options(self):
279 self.destroy()
280 self._cursor = MouseCursor(
281 'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
282 (.01, .01))
283 self._set_options()
284
285 def on_credits(self):
286 self.destroy()
287 self._cursor = MouseCursor(
288 'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
289 (.01, .01))
290 self._set_credits()
291
292 def _rearrange_width(self):
293 max_width = 0
294 for wdg in self._widgets:
295 t_n = wdg.component('text0')
296 u_l = t_n.textNode.get_upper_left_3d()
297 l_r = t_n.textNode.get_lower_right_3d()
298 max_width = max(l_r[0] - u_l[0], max_width)
299 for wdg in self._widgets:
300 m_w = max_width / 2 + .8
301 wdg['frameSize'] = -m_w, m_w, wdg['frameSize'][2], wdg['frameSize'][3]
302
303 def on_language(self, arg):
304 lang_code = {
305 _('English'): 'en_EN',
306 _('Italian'): 'it_IT'}[arg]
307 self._lang_mgr.set_lang(lang_code)
308 self._opt_file['settings']['language'] = lang_code[:2]
309 self._opt_file.store()
310 self.on_options()
311
312 def on_volume(self):
313 self._opt_file['settings']['volume'] = self._slider['value']
314 self._music.set_volume(self._slider['value'])
315
316 def on_fullscreen(self, arg):
317 props = WindowProperties()
318 props.set_fullscreen(arg)
319 if not self._fun_test:
320 base.win.request_properties(props)
321 # if we actually switch to fullscreen during the tests then
322 # exwm inside qemu can't restore the correct resolution
323 # i may re-enable this if/when i run the tests onto a
324 # physical machine
325 self._opt_file['settings']['fullscreen'] = int(arg)
326 self._opt_file.store()
327
328 def on_resolution(self, arg):
329 info('on resolution: %s (%s)' % (arg, self._enforced_res))
330 arg = self._enforced_res or arg
331 info('set resolution: %s' % arg)
332 props = WindowProperties()
333 props.set_size(LVector2i(*[int(_res) for _res in arg.split('x')]))
334 base.win.request_properties(props)
335 self._opt_file['settings']['resolution'] = arg
336 self._opt_file.store()
337
338 def on_aa(self, arg):
339 self._pipeline.msaa_samples = 4 if arg else 1
340 debug(f'msaa: {self._pipeline.msaa_samples}')
341 self._opt_file['settings']['antialiasing'] = int(arg)
342 self._opt_file.store()
343
344 def on_shadows(self, arg):
345 self._pipeline.enable_shadows = int(arg)
346 debug(f'shadows: {self._pipeline.enable_shadows}')
347 self._opt_file['settings']['shadows'] = int(arg)
348 self._opt_file.store()
349
350 def on_website(self):
351 if platform.startswith('linux'):
352 environ['LD_LIBRARY_PATH'] = ''
353 system('xdg-open https://www.ya2.it')
354 else:
355 open_new_tab('https://www.ya2.it')
356
357 def on_back(self):
358 self._opt_file.store()
359 self.destroy()
360 self._cursor = MouseCursor(
361 'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
362 (.01, .01))
363 self._set_main()
364
365 def destroy(self):
366 [wdg.destroy() for wdg in self._widgets]
367 self._cursor.destroy()
368 self.ignore('enforce_resolution')