ya2 · news · projects · code · about

housekeeping: pmachines.gui.menu, pmachines.application.persistency
authorFlavio Calva <f.calva@gmail.com>
Sat, 14 Jan 2023 08:24:36 +0000 (10:24 +0200)
committerFlavio Calva <f.calva@gmail.com>
Fri, 13 Jan 2023 15:05:38 +0000 (16:05 +0100)
14 files changed:
assets/locale/po/it_IT.po
pmachines/application/application.py
pmachines/application/persistency.py [new file with mode: 0644]
pmachines/application/persistent.py [deleted file]
pmachines/gui/base_page.py [new file with mode: 0644]
pmachines/gui/credits_page.py [new file with mode: 0644]
pmachines/gui/main_page.py [new file with mode: 0644]
pmachines/gui/menu.py
pmachines/gui/options_page.py [new file with mode: 0644]
pmachines/gui/play_page.py [new file with mode: 0644]
tests/ya2/utils/test_audio.py
tests/ya2/utils/test_gui.py
ya2/utils/audio.py
ya2/utils/gui.py

index 8d8ee786e7da575a92174943a1d3eb46c60f6ce1..40484c79bfbf52920cf7b26b367f1fed2488a387 100644 (file)
@@ -211,62 +211,23 @@ msgstr "Nuovo"
 msgid "Next"
 msgstr "Prossimo"
 
-#: pmachines/gui/menu.py:96
+#: pmachines/gui/main_page.py:19
 msgid "Play"
 msgstr "Gioca"
 
-#: pmachines/gui/menu.py:101
+#: pmachines/gui/main_page.py:24
 msgid "Options"
 msgstr "Opzioni"
 
-#: pmachines/gui/menu.py:106
+#: pmachines/gui/main_page.py:29
 msgid "Credits"
 msgstr "Riconoscimenti"
 
-#: pmachines/gui/menu.py:116 pmachines/scene/scene.py:305
+#: pmachines/gui/main_page.py:39 pmachines/scene/scene.py:305
 msgid "Exit"
 msgstr "Esci"
 
-#: pmachines/gui/menu.py:126 pmachines/gui/menu.py:129
-#: pmachines/gui/menu.py:330
-msgid "English"
-msgstr "Inglese"
-
-#: pmachines/gui/menu.py:126 pmachines/gui/menu.py:130
-#: pmachines/gui/menu.py:331
-msgid "Italian"
-msgstr "Italiano"
-
-#: pmachines/gui/menu.py:142
-msgid "Language"
-msgstr "Linguaggio"
-
-#: pmachines/gui/menu.py:152
-msgid "Volume"
-msgstr "Volume"
-
-#: pmachines/gui/menu.py:170
-msgid "Fullscreen"
-msgstr "Schermo intero"
-
-#: pmachines/gui/menu.py:201
-msgid "Resolution"
-msgstr "Risoluzione"
-
-#: pmachines/gui/menu.py:213
-msgid "Antialiasing"
-msgstr "Antialiasing"
-
-#: pmachines/gui/menu.py:221
-msgid "Shadows"
-msgstr "Ombre"
-
-#: pmachines/gui/menu.py:229 pmachines/gui/menu.py:255
-#: pmachines/gui/menu.py:295
-msgid "Back"
-msgstr "Indietro"
-
-#: pmachines/gui/menu.py:242
+#: pmachines/gui/credits_page.py:25
 msgid ""
 "Code and gfx\n"
 "  \ 1scale\ 1Flavio Calva\ 2\n"
@@ -282,11 +243,11 @@ msgstr ""
 "Music\n"
 "  \ 1scale\ 1Stefan Grossmann\ 2"
 
-#: pmachines/gui/menu.py:247
+#: pmachines/gui/credits_page.py:30
 msgid "Website"
 msgstr "Sito web"
 
-#: pmachines/gui/menu.py:250
+#: pmachines/gui/credits_page.py:33
 msgid ""
 "Special thanks to:\n"
 "  \ 1scale\ 1rdb\ 2\n"
@@ -298,6 +259,45 @@ msgstr ""
 "  \ 1scale\ 1Luisa Tenuta\ 2\n"
 "  \ 1scale\ 1Damiana Ercolani\ 2"
 
+#: pmachines/gui/credits_page.py:38 pmachines/gui/play_page.py:43
+#: pmachines/gui/options_page.py:137
+msgid "Back"
+msgstr "Indietro"
+
+#: pmachines/gui/options_page.py:34 pmachines/gui/options_page.py:37
+#: pmachines/gui/options_page.py:145
+msgid "English"
+msgstr "Inglese"
+
+#: pmachines/gui/options_page.py:34 pmachines/gui/options_page.py:38
+#: pmachines/gui/options_page.py:146
+msgid "Italian"
+msgstr "Italiano"
+
+#: pmachines/gui/options_page.py:50
+msgid "Language"
+msgstr "Linguaggio"
+
+#: pmachines/gui/options_page.py:60
+msgid "Volume"
+msgstr "Volume"
+
+#: pmachines/gui/options_page.py:78
+msgid "Fullscreen"
+msgstr "Schermo intero"
+
+#: pmachines/gui/options_page.py:109
+msgid "Resolution"
+msgstr "Risoluzione"
+
+#: pmachines/gui/options_page.py:121
+msgid "Antialiasing"
+msgstr "Antialiasing"
+
+#: pmachines/gui/options_page.py:129
+msgid "Shadows"
+msgstr "Ombre"
+
 #: pmachines/scene/scene.py:106
 msgid "Scene: "
 msgstr "Scena: "
index f6cb6a35483f3907a1a4ad634daf44fd872fa4ad..b60e5f6ce7a851677535d1e493c0f8bb133b6039 100755 (executable)
@@ -18,7 +18,7 @@ from pmachines.audio.music import MusicManager
 from pmachines.items.background import Background
 from pmachines.gui.menu import Menu
 from pmachines.scene.scene import Scene
-from pmachines.application.persistent import Persistent
+from pmachines.application.persistency import Persistency
 from ya2.utils.dictfile import DctFile
 from ya2.utils.logics import LogicsTools
 from ya2.utils.language import LanguageManager
@@ -126,7 +126,7 @@ class Pmachines:
     def on_menu_enter(self):
         self._menu_bg = Background()
         self._menu = Menu(
-            self._fsm, self.lang_mgr, self._options, self._music,
+            self._fsm, self.lang_mgr, self._options,
             self._pipeline, self.scenes(), self._args.functional_test or self._args.functional_ref,
             self._pos_mgr)
 
@@ -231,7 +231,7 @@ class Pmachines:
             default_opt)
         if not opt_exists:
             self._options.store()
-        self.__persistent = Persistent(self._options['save']['scenes_done'], self._options)
+        self.__persistent = Persistency(self._options['save']['scenes_done'], self._options)
         Scene.scenes_done = self.__persistent.scenes_done
         res = self._options['settings']['resolution']
         if res:
diff --git a/pmachines/application/persistency.py b/pmachines/application/persistency.py
new file mode 100644 (file)
index 0000000..f728ead
--- /dev/null
@@ -0,0 +1,34 @@
+from json import loads, dumps
+
+
+class Persistency:
+
+    def __init__(self, scenes_done, option_file):
+        self.__scenes_done = scenes_done
+        self.__option_file = option_file
+        self.__fix_ini_parsing()
+
+    def __fix_ini_parsing(self):
+        if len(self.__scenes_done) == 1 and not self.__scenes_done[0]:
+            self.__scenes_done = []
+        if self.__scenes_done:
+            if not isinstance(self.__scenes_done, list):  # empty list: []
+                self.__scenes_done = self.__scenes_done.strip("'")
+        if self.__scenes_done:
+            if not isinstance(self.__scenes_done, list):
+                self.__scenes_done = loads(self.__scenes_done)
+
+    def save_scene(self, name, version):
+        self.__compute_scenes_done(name, version)
+        self.__store_scenes()
+
+    def __compute_scenes_done(self, name, version):
+        other_scenes = [s for s in self.__scenes_done if s[0] != name]
+        self.__scenes_done = other_scenes + [(name, version)]
+
+    def __store_scenes(self):
+        self.__option_file['save']['scenes_done'] = f"'{dumps(self.__scenes_done)}'"
+        self.__option_file.store()
+
+    @property
+    def scenes_done(self): return self.__scenes_done
diff --git a/pmachines/application/persistent.py b/pmachines/application/persistent.py
deleted file mode 100644 (file)
index eba2f39..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-import json
-
-
-class Persistent:
-
-    def __init__(self, scenes_done, opt_file):
-        self.__scenes_done = scenes_done
-        self.__fix_ini_parsing()
-        self.__opt_file = opt_file
-
-    def __fix_ini_parsing(self):
-        if len(self.__scenes_done) == 1 and not self.__scenes_done[0]:
-            self.__scenes_done = []
-        #print(self.__scenes_done)
-        #self.__scenes_done = self.__scenes_done[0]
-        if self.__scenes_done:
-            if not isinstance(self.__scenes_done, list):  # empty list: []
-                self.__scenes_done = self.__scenes_done.strip("'")
-        if self.__scenes_done:
-            if not isinstance(self.__scenes_done, list):
-                self.__scenes_done = json.loads(self.__scenes_done)
-
-    def save_scene(self, name, version):
-        scenes = []
-        scenes = [scene for scene in self.__scenes_done if scene[0] != name]
-        self.__scenes_done = scenes + [(name, version)]
-        self.__opt_file['save']['scenes_done'] = "'%s'" % json.dumps(self.__scenes_done)
-        self.__opt_file.store()
-
-    @property
-    def scenes_done(self):
-        return self.__scenes_done
diff --git a/pmachines/gui/base_page.py b/pmachines/gui/base_page.py
new file mode 100644 (file)
index 0000000..e26b726
--- /dev/null
@@ -0,0 +1,29 @@
+from logging import info
+from direct.showbase.DirectObject import DirectObject
+from ya2.utils.cursor import MouseCursor
+
+
+class BasePage(DirectObject):
+
+    def __init__(self, menu, running_functional_tests):
+        super().__init__()
+        self._menu = menu
+        self._enforced_resolution = ''
+        self._cursor = MouseCursor(
+            'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
+            (.01, .01), running_functional_tests)
+
+    def enforce_resolution(self, resolution):
+        self._enforced_resolution = resolution
+        info('enforced resolution: ' + resolution)
+
+    def on_back(self):
+        self._option_file.store()
+        self._menu.set_page('main')
+        self.destroy()
+
+    def destroy(self):
+        [wdg.destroy() for wdg in self._widgets]
+        self._cursor.destroy()
+        self.ignore('enforce_resolution')
+        self._menu = None
diff --git a/pmachines/gui/credits_page.py b/pmachines/gui/credits_page.py
new file mode 100644 (file)
index 0000000..bf5eeb2
--- /dev/null
@@ -0,0 +1,49 @@
+from sys import platform
+from os import environ, system
+from webbrowser import open_new_tab
+from panda3d.core import TextNode, \
+    TextProperties, TextPropertiesManager
+from direct.gui.DirectGui import DirectButton
+from direct.gui.OnscreenText import OnscreenText
+from ya2.utils.gfx import DirectGuiMixin
+from pmachines.gui.base_page import BasePage
+
+
+class CreditsPage(BasePage):
+
+    def __init__(self, menu, test_positions, gui_args, option_file, running_functional_tests):
+        super().__init__(menu, running_functional_tests)
+        self._test_positions = test_positions
+        self._option_file = option_file
+        self._running_functional_tests = running_functional_tests
+        for k in list(self._test_positions.keys()): del self._test_positions[k]
+        self._widgets = []
+        tp_scale = TextProperties()
+        tp_scale.set_text_scale(.64)
+        TextPropertiesManager.getGlobalPtr().setProperties('scale', tp_scale)
+        self._widgets += [OnscreenText(
+            _('Code and gfx\n  \1scale\1Flavio Calva\2\n\n\nMusic\n  \1scale\1Stefan Grossmann\2'),
+            pos=(-.9, .55), font=gui_args.button['text_font'],
+            scale=gui_args.button['scale'], fg=gui_args.button['text_fg'],
+            align=TextNode.A_left)]
+        self._widgets += [DirectButton(
+            text=_('Website'), pos=(-.6, 1, .29), command=self.on_website,
+            **gui_args.button | {'scale': .08})]
+        self._widgets += [OnscreenText(
+            _('Special thanks to:\n  \1scale\1rdb\2\n  \1scale\1Luisa Tenuta\2\n  \1scale\1Damiana Ercolani\2'),
+            pos=(.1, .55), font=gui_args.button['text_font'],
+            scale=gui_args.button['scale'], fg=gui_args.button['text_fg'],
+            align=TextNode.A_left)]
+        self._widgets += [DirectButton(
+            text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
+            **gui_args.button)]
+        self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        self._test_positions['back'] = self._widgets[-1].pos_pixel()
+        self.accept('enforce_resolution', self.enforce_resolution)
+
+    def on_website(self):
+        if platform.startswith('linux'):
+            environ['LD_LIBRARY_PATH'] = ''
+            system('xdg-open https://www.ya2.it')
+        else:
+            open_new_tab('https://www.ya2.it')
diff --git a/pmachines/gui/main_page.py b/pmachines/gui/main_page.py
new file mode 100644 (file)
index 0000000..afde786
--- /dev/null
@@ -0,0 +1,67 @@
+from sys import exit
+from xmlrpc.client import ServerProxy
+from direct.gui.DirectGui import DirectButton
+from ya2.utils.gfx import DirectGuiMixin
+from pmachines.gui.base_page import BasePage
+
+
+class MainPage(BasePage):
+
+    def __init__(self, menu, test_positions, gui_args, running_functional_tests, scenes, application_fsm):
+        super().__init__(menu, running_functional_tests)
+        self._running_functional_tests = running_functional_tests
+        self._test_positions = test_positions
+        self._scenes = scenes
+        self._application_fsm = application_fsm
+        for k in list(self._test_positions.keys()): del self._test_positions[k]
+        self._widgets = []
+        self._widgets += [DirectButton(
+            text=_('Play'), pos=(0, 1, .6), command=self.on_play,
+            **gui_args.button)]
+        self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        self._test_positions['play'] = self._widgets[-1].pos_pixel()
+        self._widgets += [DirectButton(
+            text=_('Options'), pos=(0, 1, .2), command=self.on_options,
+            **gui_args.button)]
+        self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        self._test_positions['options'] = self._widgets[-1].pos_pixel()
+        self._widgets += [DirectButton(
+            text=_('Credits'), pos=(0, 1, -.2), command=self.on_credits,
+            **gui_args.button)]
+        self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        self._test_positions['credits'] = self._widgets[-1].pos_pixel()
+
+        def btn_exit():
+            if self._running_functional_tests:
+                ServerProxy('http://localhost:7000').destroy()
+            exit()
+        self._widgets += [DirectButton(
+            text=_('Exit'), pos=(0, 1, -.6), command=lambda: btn_exit(),
+            **gui_args.button)]
+        self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        self._test_positions['exit'] = self._widgets[-1].pos_pixel()
+        self._rearrange_width()
+        self.accept('enforce_resolution', self.enforce_resolution)
+
+    def on_options(self):
+        self._menu.set_page('options')
+        self.destroy()
+
+    def on_credits(self):
+        self._menu.set_page('credits')
+        self.destroy()
+
+    def on_play(self):
+        self._menu.set_page('play')
+        self.destroy()
+
+    def _rearrange_width(self):
+        max_width = 0
+        for wdg in self._widgets:
+            t_n = wdg.component('text0')
+            u_l = t_n.textNode.get_upper_left_3d()
+            l_r = t_n.textNode.get_lower_right_3d()
+            max_width = max(l_r[0] - u_l[0], max_width)
+        for wdg in self._widgets:
+            m_w = max_width / 2 + .8
+            wdg['frameSize'] = -m_w, m_w, wdg['frameSize'][2], wdg['frameSize'][3]
index 4bd88901ed09b52664fe33c89fa4ee14b7a7a48e..85200ecc0c4439aa7ea5cc126d85ebc40885681a 100644 (file)
-from logging import info, debug
-from sys import platform, exit
-from os import environ, system
-from webbrowser import open_new_tab
-from xmlrpc.client import ServerProxy
-from panda3d.core import Texture, TextNode, WindowProperties, LVector2i, \
-    TextProperties, TextPropertiesManager
-from direct.gui.DirectGui import DirectButton, DirectCheckButton, \
-    DirectOptionMenu, DirectSlider, DirectFrame
+from collections import namedtuple
+from panda3d.core import TextNode
 from direct.gui.DirectGuiGlobals import FLAT
-from direct.gui.OnscreenText import OnscreenText
-from direct.showbase.DirectObject import DirectObject
-from ya2.utils.cursor import MouseCursor
-from ya2.utils.gfx import GfxTools, DirectGuiMixin, pos_pixel
+from ya2.utils.gui import GuiTools
+from ya2.utils.audio import AudioTools
+from pmachines.gui.main_page import MainPage
+from pmachines.gui.play_page import PlayPage
+from pmachines.gui.options_page import OptionsPage
+from pmachines.gui.credits_page import CreditsPage
 
 
-class DirectOptionMenuTest(DirectOptionMenu):
+class Menu:
 
-    def __init__(self, parent=None, **kw):
-        DirectOptionMenu.__init__(self, parent, **kw)
-        self.initialiseoptions(DirectOptionMenuTest)
-
-    def showPopupMenu(self, event=None):
-        #super().showPopupMenu(event)  # it does not work with mixins
-        DirectOptionMenu.showPopupMenu(self, event)
-        self._show_cb([self.component(cmp) for cmp in self.components()])
-
-
-
-class Menu(DirectObject):
-
-    def __init__(self, fsm, lang_mgr, opt_file, music, pipeline, scenes,
-                 fun_test, pos_mgr):
+    def __init__(self, application_fsm, language_manager, option_file,
+                 gfx_pipeline, scenes,
+                 running_functional_tests, test_positions):
         super().__init__()
-        self._fsm = fsm
-        self._lang_mgr = lang_mgr
-        self._opt_file = opt_file
-        self._music = music
-        self._pipeline = pipeline
-        self._scenes = scenes
-        self._fun_test = fun_test
-        self._pos_mgr = pos_mgr
-        self._enforced_res = ''
-        self._cursor = MouseCursor(
-            'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04),
-            (.5, .5, .5, 1), (.01, .01), fun_test)
-        self._font = base.loader.load_font(
-            'assets/fonts/Hanken-Book.ttf')
-        self._font.clear()
-        self._font.set_pixels_per_unit(60)
-        self._font.set_minfilter(Texture.FTLinearMipmapLinear)
-        self._font.set_outline((0, 0, 0, 1), .8, .2)
-        self._widgets = []
-        self._common = {
+        self.__application_fsm = application_fsm
+        self.__language_manager = language_manager
+        self.__option_file = option_file
+        self.__gfx_pipeline = gfx_pipeline
+        self.__scenes = scenes
+        self.__running_functional_tests = running_functional_tests
+        self.__test_positions = test_positions
+        self.__gui_args = self.__build_gui_args()
+        self.__page = None
+        self.set_page('main')
+
+    def __build_gui_args(self):
+        base = {
             'scale': .12,
-            'text_font': self._font,
+            'text_font': GuiTools.load_font('assets/fonts/Hanken-Book.ttf'),
             'text_fg': (.9, .9, .9, 1),
             'relief': FLAT,
             'frameColor': (.4, .4, .4, .14),
-            'rolloverSound': loader.load_sfx(
-                'assets/audio/sfx/rollover.ogg'),
-            'clickSound': loader.load_sfx(
-                'assets/audio/sfx/click.ogg')}
-        self._common_btn = {'frameSize': (-4.8, 4.8, -.6, 1.2)} | self._common
-        hlc = self._common_btn['frameColor']
-        hlc = (hlc[0] + .2, hlc[1] + .2, hlc[2] + .2, hlc[3] + .2)
-        self._common_opt = {
-            'item_frameColor': self._common_btn['frameColor'],
-            'popupMarker_frameColor': self._common_btn['frameColor'],
-            'item_relief': self._common_btn['relief'],
-            'item_text_font': self._common_btn['text_font'],
-            'item_text_fg': self._common_btn['text_fg'],
+            'rolloverSound': AudioTools.load_sfx('assets/audio/sfx/rollover.ogg'),
+            'clickSound': AudioTools.load_sfx('assets/audio/sfx/click.ogg')}
+        button = {'frameSize': (-4.8, 4.8, -.6, 1.2)} | base
+        h = button['frameColor']
+        h = (h[0] + .2, h[1] + .2, h[2] + .2, h[3] + .2)
+        option = {
+            'item_frameColor': button['frameColor'],
+            'popupMarker_frameColor': button['frameColor'],
+            'item_relief': button['relief'],
+            'item_text_font': button['text_font'],
+            'item_text_fg': button['text_fg'],
             'textMayChange': 1,
-            'highlightColor': hlc,
+            'highlightColor': h,
             'text_align': TextNode.A_center,
-        } | self._common_btn
-        f_s = self._common_opt['frameSize']
-        self._common_opt['frameSize'] = f_s[0], f_s[1] - .56, f_s[2], f_s[3]
-        self._common_slider = self._common | {
+        } | button
+        f = option['frameSize']
+        option['frameSize'] = f[0], f[1] - .56, f[2], f[3]
+        slider = base | {
             'range': (0, 1),
             'thumb_frameColor': (.4, .4, .4, .4),
             'thumb_scale': 1.6,
             'scale': .4}
-        del self._common_slider['rolloverSound']
-        del self._common_slider['clickSound']
-        self._set_main()
-
-    def enforce_res(self, val):
-        self._enforced_res = val
-        info('enforced resolution: ' + val)
-
-    def _set_main(self):
-        for k in list(self._pos_mgr.keys()): del self._pos_mgr[k]
-        self._widgets = []
-        self._widgets += [DirectButton(
-            text=_('Play'), pos=(0, 1, .6), command=self.on_play,
-            **self._common_btn)]
-        self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
-        self._pos_mgr['play'] = self._widgets[-1].pos_pixel()
-        self._widgets += [DirectButton(
-            text=_('Options'), pos=(0, 1, .2), command=self.on_options,
-            **self._common_btn)]
-        self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
-        self._pos_mgr['options'] = self._widgets[-1].pos_pixel()
-        self._widgets += [DirectButton(
-            text=_('Credits'), pos=(0, 1, -.2), command=self.on_credits,
-            **self._common_btn)]
-        self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
-        self._pos_mgr['credits'] = self._widgets[-1].pos_pixel()
-
-        def btn_exit():
-            if self._fun_test:
-                ServerProxy('http://localhost:7000').destroy()
-            exit()
-        self._widgets += [DirectButton(
-            text=_('Exit'), pos=(0, 1, -.6), command=lambda: btn_exit(),
-            **self._common_btn)]
-        self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
-        self._pos_mgr['exit'] = self._widgets[-1].pos_pixel()
-        self._rearrange_width()
-        self.accept('enforce_resolution', self.enforce_res)
-
-    def _set_options(self):
-        for k in list(self._pos_mgr.keys()): del self._pos_mgr[k]
-        self._widgets = []
-        self._lang_funcs = [lambda: _('English'), lambda: _('Italian')]
-        items = [fnc() for fnc in self._lang_funcs]
-        inititem = {
-            'en': _('English'),
-            'it': _('Italian')
-        }[self._opt_file['settings']['language']]
-
-        def lang_cb(comps):
-            for element in ['english', 'italian']:
-                if element in self._pos_mgr:
-                    del self._pos_mgr[element]
-            for lng, btn in zip(['english', 'italian'], comps):
-                btn.__class__ = type('DirectFrameMixed', (DirectFrame, DirectGuiMixin), {})
-                pos = btn.pos_pixel()
-                self._pos_mgr[lng] = (pos[0] + 5, pos[1])
-        btn = DirectOptionMenuTest(
-            text=_('Language'), items=items, initialitem=inititem,
-            pos=(0, 1, .8), command=self.on_language, **self._common_opt)
-        btn.__class__ = type('DirectOptionMenuMixed', (DirectOptionMenu, DirectGuiMixin), {})
-        btn._show_cb = lang_cb
-        btn.popupMenu['frameColor'] = self._common_btn['frameColor']
-        btn.popupMenu['relief'] = self._common_btn['relief']
-        self._widgets += [btn]
-        pos_lang = self._widgets[-1].pos_pixel()
-        self._pos_mgr['languages'] = pos_lang
-        self._widgets += [OnscreenText(
-            _('Volume'), pos=(-.1, .55), font=self._common['text_font'],
-            scale=self._common['scale'], fg=self._common['text_fg'],
-            align=TextNode.A_right)]
-        self._widgets += [DirectSlider(
-            pos=(.5, 1, .57),
-            value=self._opt_file['settings']['volume'],
-            command=self.on_volume,
-            **self._common_slider)]
-        self._widgets[-1].__class__ = type('DirectSliderMixed', (DirectSlider, DirectGuiMixin), {})
-        vol_pos = self._widgets[-1].pos_pixel()
-        self._pos_mgr['volume'] = vol_pos
-        np_left = GfxTools.build_empty_node('left_slider')
-        np_left.set_pos(self._widgets[-1].get_net_transform().get_pos())
-        np_left.set_x(np_left.get_x() - self._widgets[-1].get_scale()[0])
-        lpos = np_left.pos_as_widget()  # try with pos2d_pixel and remove pos_as_widget if ok
-        self._pos_mgr['volume_0'] = lpos
-        self._slider = self._widgets[-1]
-        self._widgets += [DirectCheckButton(
-            text=_('Fullscreen'), pos=(0, 1, .3), command=self.on_fullscreen,
-            indicator_frameColor=self._common_opt['highlightColor'],
-            indicator_relief=self._common_btn['relief'],
-            indicatorValue=self._opt_file['settings']['fullscreen'],
-            **self._common_btn)]
-        self._widgets[-1].__class__ = type('DirectCheckButtonMixed', (DirectCheckButton, DirectGuiMixin), {})
-        self._pos_mgr['fullscreen'] = self._widgets[-1].pos_pixel()
-        res = self._opt_file['settings']['resolution']
-        d_i = base.pipe.get_display_information()
-        def _res(idx):
-            return d_i.get_display_mode_width(idx), \
-                d_i.get_display_mode_height(idx)
-        resolutions = [
-            _res(idx) for idx in range(d_i.get_total_display_modes())]
-        resolutions = list(set(resolutions))
-        resolutions = sorted(resolutions)
-        resolutions = [(str(_res[0]), str(_res[1])) for _res in resolutions]
-        resolutions = ['x'.join(_res) for _res in resolutions]
-        if not res:
-            res = resolutions[-1]
-
-        def res_cb(comps):
-            for element in ['res_1440x900', 'res_1360x768']:
-                if element in self._pos_mgr:
-                    del self._pos_mgr[element]
-            for tgt_res in ['1440x900', '1360x768']:
-                for btn in comps:
-                    if btn['text'] == tgt_res:
-                        pos = pos_pixel(btn)
-                        self._pos_mgr['res_' + tgt_res] = (pos[0] + 5, pos[1])
-        btn = DirectOptionMenuTest(
-            text=_('Resolution'), items=resolutions, initialitem=res,
-            pos=(0, 1, .05), command=self.on_resolution, **self._common_opt)
-        btn.__class__ = type('DirectOptionMenuMixed', (DirectOptionMenu, DirectGuiMixin), {})
-        btn._show_cb = res_cb
-        btn.popupMenu['frameColor'] = self._common_btn['frameColor']
-        btn.popupMenu['relief'] = self._common_btn['relief']
-        self._widgets += [btn]
-        pos_res = self._widgets[-1].pos_pixel()
-        self._pos_mgr['resolutions'] = pos_res  # 680 365
-        self._pos_mgr['res_1440x900'] = [pos_res[0] + 320, pos_res[1] + 75]
-        self._pos_mgr['res_1360x768'] = [pos_res[0] + 430, pos_res[1] -285]
-        self._widgets += [DirectCheckButton(
-            text=_('Antialiasing'), pos=(0, 1, -.2), command=self.on_aa,
-            indicator_frameColor=self._common_opt['highlightColor'],
-            indicator_relief=self._common_btn['relief'],
-            indicatorValue=self._opt_file['settings']['antialiasing'],
-            **self._common_btn)]
-        self._widgets[-1].__class__ = type('DirectCheckButtonMixed', (DirectCheckButton, DirectGuiMixin), {})
-        self._pos_mgr['aa'] = self._widgets[-1].pos_pixel()
-        self._widgets += [DirectCheckButton(
-            text=_('Shadows'), pos=(0, 1, -.45), command=self.on_shadows,
-            indicator_frameColor=self._common_opt['highlightColor'],
-            indicator_relief=self._common_btn['relief'],
-            indicatorValue=self._opt_file['settings']['shadows'],
-            **self._common_btn)]
-        self._widgets[-1].__class__ = type('DirectCheckButtonMixed', (DirectCheckButton, DirectGuiMixin), {})
-        self._pos_mgr['shadows'] = self._widgets[-1].pos_pixel()
-        self._widgets += [DirectButton(
-            text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
-            **self._common_btn)]
-        self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
-        self._pos_mgr['back'] = self._widgets[-1].pos_pixel()
-        self.accept('enforce_resolution', self.enforce_res)
-
-    def _set_credits(self):
-        for k in list(self._pos_mgr.keys()): del self._pos_mgr[k]
-        self._widgets = []
-        tp_scale = TextProperties()
-        tp_scale.set_text_scale(.64)
-        TextPropertiesManager.getGlobalPtr().setProperties('scale', tp_scale)
-        self._widgets += [OnscreenText(
-            _('Code and gfx\n  \1scale\1Flavio Calva\2\n\n\nMusic\n  \1scale\1Stefan Grossmann\2'),
-            pos=(-.9, .55), font=self._common['text_font'],
-            scale=self._common['scale'], fg=self._common['text_fg'],
-            align=TextNode.A_left)]
-        self._widgets += [DirectButton(
-            text=_('Website'), pos=(-.6, 1, .29), command=self.on_website,
-            **self._common_btn | {'scale': .08})]
-        self._widgets += [OnscreenText(
-            _('Special thanks to:\n  \1scale\1rdb\2\n  \1scale\1Luisa Tenuta\2\n  \1scale\1Damiana Ercolani\2'),
-            pos=(.1, .55), font=self._common['text_font'],
-            scale=self._common['scale'], fg=self._common['text_fg'],
-            align=TextNode.A_left)]
-        self._widgets += [DirectButton(
-            text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
-            **self._common_btn)]
-        self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
-        self._pos_mgr['back'] = self._widgets[-1].pos_pixel()
-        self.accept('enforce_resolution', self.enforce_res)
-
-    def on_play(self):
-        for k in list(self._pos_mgr.keys()): del self._pos_mgr[k]
-        self.destroy()
-        self._cursor = MouseCursor(
-            'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
-            (.01, .01), self._fun_test)
-        self._widgets = []
-        cmn = self._common_btn.copy() | {
-            'frameSize': (-2.4, 2.4, -2.4, 2.4),
-            'frameColor': (1, 1, 1, .8),
-            'text_scale': .64}
-        left = - (dx := .8) * (min(4, len(self._scenes)) - 1) / 2
-        from pmachines.scene.scene import Scene
-        for i, scene_name in enumerate(self._scenes):
-            top = .1 if len(self._scenes) < 5 else .6
-            row = 0 if i < 4 else 1
-            new_cmn = cmn.copy()
-            if Scene.is_done(scene_name):
-                new_cmn['frameColor'] = (1, 1, 1, .4)
-                new_cmn['text_fg'] = (.9, .9, .9, .4)
-            self._widgets += [DirectButton(
-                text=Scene.name(scene_name), pos=(left + dx * (i % 4), 1, top - dx * row),
-                command=self.start, extraArgs=[scene_name], text_wordwrap=6,
-                frameTexture='assets/images/scenes/%s.dds' % scene_name,
-                **new_cmn)]
-            self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
-            name = scene_name.lower()
-            self._pos_mgr[name] = self._widgets[-1].pos_pixel()
-            for j in range(4):
-                tnode = self._widgets[-1].component('text%s' % j).textNode
-                height = - tnode.getLineHeight() / 2
-                height += (tnode.get_height() - tnode.get_line_height()) / 2
-                self._widgets[-1].component('text%s' % j).set_pos(0, 0, height)
-        self._widgets += [DirectButton(
-            text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
-            **self._common_btn)]
-        self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
-        self._pos_mgr['back'] = self._widgets[-1].pos_pixel()
-
-    def start(self, cls):
-        self._fsm.demand('Scene', cls)
-
-    def on_options(self):
-        self.destroy()
-        self._cursor = MouseCursor(
-            'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
-            (.01, .01), self._fun_test)
-        self._set_options()
-
-    def on_credits(self):
-        self.destroy()
-        self._cursor = MouseCursor(
-            'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
-            (.01, .01), self._fun_test)
-        self._set_credits()
-
-    def _rearrange_width(self):
-        max_width = 0
-        for wdg in self._widgets:
-            t_n = wdg.component('text0')
-            u_l = t_n.textNode.get_upper_left_3d()
-            l_r = t_n.textNode.get_lower_right_3d()
-            max_width = max(l_r[0] - u_l[0], max_width)
-        for wdg in self._widgets:
-            m_w = max_width / 2 + .8
-            wdg['frameSize'] = -m_w, m_w, wdg['frameSize'][2], wdg['frameSize'][3]
-
-    def on_language(self, arg):
-        lang_code = {
-            _('English'): 'en_EN',
-            _('Italian'): 'it_IT'}[arg]
-        self._lang_mgr.set_language(lang_code)
-        self._opt_file['settings']['language'] = lang_code[:2]
-        self._opt_file.store()
-        self.on_options()
-
-    def on_volume(self):
-        self._opt_file['settings']['volume'] = self._slider['value']
-        self._music.set_volume(self._slider['value'])
-
-    def on_fullscreen(self, arg):
-        props = WindowProperties()
-        props.set_fullscreen(arg)
-        if not self._fun_test:
-            base.win.request_properties(props)
-            # if we actually switch to fullscreen during the tests then
-            # exwm inside qemu can't restore the correct resolution
-            # i may re-enable this if/when i run the tests onto a
-            # physical machine
-        self._opt_file['settings']['fullscreen'] = int(arg)
-        self._opt_file.store()
-
-    def on_resolution(self, arg):
-        info('on resolution: %s (%s)' % (arg, self._enforced_res))
-        arg = self._enforced_res or arg
-        info('set resolution: %s' % arg)
-        props = WindowProperties()
-        props.set_size(LVector2i(*[int(_res) for _res in arg.split('x')]))
-        base.win.request_properties(props)
-        self._opt_file['settings']['resolution'] = arg
-        self._opt_file.store()
-
-    def on_aa(self, arg):
-        self._pipeline.msaa_samples = 4 if arg else 1
-        debug(f'msaa: {self._pipeline.msaa_samples}')
-        self._opt_file['settings']['antialiasing'] = int(arg)
-        self._opt_file.store()
-
-    def on_shadows(self, arg):
-        self._pipeline.enable_shadows = int(arg)
-        debug(f'shadows: {self._pipeline.enable_shadows}')
-        self._opt_file['settings']['shadows'] = int(arg)
-        self._opt_file.store()
-
-    def on_website(self):
-        if platform.startswith('linux'):
-            environ['LD_LIBRARY_PATH'] = ''
-            system('xdg-open https://www.ya2.it')
-        else:
-            open_new_tab('https://www.ya2.it')
+        del slider['rolloverSound']
+        del slider['clickSound']
+        GuiArgs = namedtuple('GuiArgs', 'button option slider')
+        gui_args = GuiArgs(button, option, slider)
+        return gui_args
 
-    def on_back(self):
-        self._opt_file.store()
+    def set_page(self, page_name):
         self.destroy()
-        self._cursor = MouseCursor(
-            'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
-            (.01, .01), self._fun_test)
-        self._set_main()
+        if self.__page: self.__page.destroy()
+        match page_name:
+            case 'main': p = MainPage(self, self.__test_positions, self.__gui_args, self.__running_functional_tests, self.__scenes, self.__application_fsm)
+            case 'credits': p = CreditsPage(self, self.__test_positions, self.__gui_args, self.__option_file, self.__running_functional_tests)
+            case 'options': p = OptionsPage(self, self.__test_positions, self.__option_file, self.__gui_args, self.__language_manager, self.__running_functional_tests, self.__gfx_pipeline)
+            case 'play': p = PlayPage(self, self.__test_positions, self.__gui_args, self.__scenes, self.__option_file, self.__running_functional_tests, self.__application_fsm)
+        self.__page = p
 
     def destroy(self):
-        [wdg.destroy() for wdg in self._widgets]
-        self._cursor.destroy()
-        self.ignore('enforce_resolution')
+        if self.__page:
+            self.__page.destroy()
+        self.__page = None
diff --git a/pmachines/gui/options_page.py b/pmachines/gui/options_page.py
new file mode 100644 (file)
index 0000000..29d8a72
--- /dev/null
@@ -0,0 +1,188 @@
+from logging import info, debug
+from panda3d.core import TextNode, WindowProperties, LVector2i
+from direct.gui.DirectGui import DirectButton, DirectCheckButton, \
+    DirectOptionMenu, DirectSlider, DirectFrame
+from direct.gui.OnscreenText import OnscreenText
+from ya2.utils.gfx import GfxTools, DirectGuiMixin, pos_pixel
+from ya2.utils.audio import AudioTools
+from pmachines.gui.base_page import BasePage
+
+
+class DirectOptionMenuTestable(DirectOptionMenu):
+
+    def __init__(self, parent=None, **kw):
+        DirectOptionMenu.__init__(self, parent, **kw)
+        self.initialiseoptions(DirectOptionMenuTestable)
+
+    def showPopupMenu(self, event=None):
+        #super().showPopupMenu(event)  # it does not work with mixins
+        DirectOptionMenu.showPopupMenu(self, event)
+        self._show_cb([self.component(c) for c in self.components()])
+
+
+class OptionsPage(BasePage):
+
+    def __init__(self, menu, test_positions, option_file, gui_args, language_manager, running_functional_tests, gfx_pipeline):
+        super().__init__(menu, running_functional_tests)
+        self._test_positions = test_positions
+        self._option_file = option_file
+        self._language_manager = language_manager
+        self._running_functional_tests = running_functional_tests
+        self._gfx_pipeline = gfx_pipeline
+        for k in list(self._test_positions.keys()): del self._test_positions[k]
+        self._widgets = []
+        self._lang_funcs = [lambda: _('English'), lambda: _('Italian')]
+        items = [fnc() for fnc in self._lang_funcs]
+        inititem = {
+            'en': _('English'),
+            'it': _('Italian')
+        }[self._option_file['settings']['language']]
+
+        def lang_cb(comps):
+            for element in ['english', 'italian']:
+                if element in self._test_positions:
+                    del self._test_positions[element]
+            for lng, btn in zip(['english', 'italian'], comps):
+                btn.__class__ = type('DirectFrameMixed', (DirectFrame, DirectGuiMixin), {})
+                pos = btn.pos_pixel()
+                self._test_positions[lng] = (pos[0] + 5, pos[1])
+        btn = DirectOptionMenuTestable(
+            text=_('Language'), items=items, initialitem=inititem,
+            pos=(0, 1, .8), command=self.on_language, **gui_args.option)
+        btn.__class__ = type('DirectOptionMenuMixed', (DirectOptionMenu, DirectGuiMixin), {})
+        btn._show_cb = lang_cb
+        btn.popupMenu['frameColor'] = gui_args.button['frameColor']
+        btn.popupMenu['relief'] = gui_args.button['relief']
+        self._widgets += [btn]
+        pos_lang = self._widgets[-1].pos_pixel()
+        self._test_positions['languages'] = pos_lang
+        self._widgets += [OnscreenText(
+            _('Volume'), pos=(-.1, .55), font=gui_args.button['text_font'],
+            scale=gui_args.button['scale'], fg=gui_args.button['text_fg'],
+            align=TextNode.A_right)]
+        self._widgets += [DirectSlider(
+            pos=(.5, 1, .57),
+            value=self._option_file['settings']['volume'],
+            command=self.on_volume,
+            **gui_args.slider)]
+        self._widgets[-1].__class__ = type('DirectSliderMixed', (DirectSlider, DirectGuiMixin), {})
+        vol_pos = self._widgets[-1].pos_pixel()
+        self._test_positions['volume'] = vol_pos
+        np_left = GfxTools.build_empty_node('left_slider')
+        np_left.set_pos(self._widgets[-1].get_net_transform().get_pos())
+        np_left.set_x(np_left.get_x() - self._widgets[-1].get_scale()[0])
+        lpos = np_left.pos_as_widget()  # try with pos2d_pixel and remove pos_as_widget if ok
+        self._test_positions['volume_0'] = lpos
+        self._slider = self._widgets[-1]
+        self._widgets += [DirectCheckButton(
+            text=_('Fullscreen'), pos=(0, 1, .3), command=self.on_fullscreen,
+            indicator_frameColor=gui_args.option['highlightColor'],
+            indicator_relief=gui_args.button['relief'],
+            indicatorValue=self._option_file['settings']['fullscreen'],
+            **gui_args.button)]
+        self._widgets[-1].__class__ = type('DirectCheckButtonMixed', (DirectCheckButton, DirectGuiMixin), {})
+        self._test_positions['fullscreen'] = self._widgets[-1].pos_pixel()
+        res = self._option_file['settings']['resolution']
+        d_i = base.pipe.get_display_information()
+        def _res(idx):
+            return d_i.get_display_mode_width(idx), \
+                d_i.get_display_mode_height(idx)
+        resolutions = [
+            _res(idx) for idx in range(d_i.get_total_display_modes())]
+        resolutions = list(set(resolutions))
+        resolutions = sorted(resolutions)
+        resolutions = [(str(_res[0]), str(_res[1])) for _res in resolutions]
+        resolutions = ['x'.join(_res) for _res in resolutions]
+        if not res:
+            res = resolutions[-1]
+
+        def res_cb(comps):
+            for element in ['res_1440x900', 'res_1360x768']:
+                if element in self._test_positions:
+                    del self._test_positions[element]
+            for tgt_res in ['1440x900', '1360x768']:
+                for btn in comps:
+                    if btn['text'] == tgt_res:
+                        pos = pos_pixel(btn)
+                        self._test_positions['res_' + tgt_res] = (pos[0] + 5, pos[1])
+        btn = DirectOptionMenuTestable(
+            text=_('Resolution'), items=resolutions, initialitem=res,
+            pos=(0, 1, .05), command=self.on_resolution, **gui_args.option)
+        btn.__class__ = type('DirectOptionMenuMixed', (DirectOptionMenu, DirectGuiMixin), {})
+        btn._show_cb = res_cb
+        btn.popupMenu['frameColor'] = gui_args.button['frameColor']
+        btn.popupMenu['relief'] = gui_args.button['relief']
+        self._widgets += [btn]
+        pos_res = self._widgets[-1].pos_pixel()
+        self._test_positions['resolutions'] = pos_res  # 680 365
+        self._test_positions['res_1440x900'] = [pos_res[0] + 320, pos_res[1] + 75]
+        self._test_positions['res_1360x768'] = [pos_res[0] + 430, pos_res[1] -285]
+        self._widgets += [DirectCheckButton(
+            text=_('Antialiasing'), pos=(0, 1, -.2), command=self.on_aa,
+            indicator_frameColor=gui_args.option['highlightColor'],
+            indicator_relief=gui_args.button['relief'],
+            indicatorValue=self._option_file['settings']['antialiasing'],
+            **gui_args.button)]
+        self._widgets[-1].__class__ = type('DirectCheckButtonMixed', (DirectCheckButton, DirectGuiMixin), {})
+        self._test_positions['aa'] = self._widgets[-1].pos_pixel()
+        self._widgets += [DirectCheckButton(
+            text=_('Shadows'), pos=(0, 1, -.45), command=self.on_shadows,
+            indicator_frameColor=gui_args.option['highlightColor'],
+            indicator_relief=gui_args.button['relief'],
+            indicatorValue=self._option_file['settings']['shadows'],
+            **gui_args.button)]
+        self._widgets[-1].__class__ = type('DirectCheckButtonMixed', (DirectCheckButton, DirectGuiMixin), {})
+        self._test_positions['shadows'] = self._widgets[-1].pos_pixel()
+        self._widgets += [DirectButton(
+            text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
+            **gui_args.button)]
+        self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        self._test_positions['back'] = self._widgets[-1].pos_pixel()
+        self.accept('enforce_resolution', self.enforce_resolution)
+
+    def on_language(self, arg):
+        lang_code = {
+            _('English'): 'en_EN',
+            _('Italian'): 'it_IT'}[arg]
+        self._language_manager.set_language(lang_code)
+        self._option_file['settings']['language'] = lang_code[:2]
+        self._option_file.store()
+        self._menu.set_page('options')
+
+    def on_volume(self):
+        self._option_file['settings']['volume'] = self._slider['value']
+        AudioTools.set_volume(self._slider['value'])
+
+    def on_fullscreen(self, arg):
+        props = WindowProperties()
+        props.set_fullscreen(arg)
+        if not self._running_functional_tests:
+            base.win.request_properties(props)
+            # if we actually switch to fullscreen during the tests then
+            # exwm inside qemu can't restore the correct resolution
+            # i may re-enable this if/when i run the tests onto a
+            # physical machine
+        self._option_file['settings']['fullscreen'] = int(arg)
+        self._option_file.store()
+
+    def on_resolution(self, arg):
+        info('on resolution: %s (%s)' % (arg, self._enforced_resolution))
+        arg = self._enforced_resolution or arg
+        info('set resolution: %s' % arg)
+        props = WindowProperties()
+        props.set_size(LVector2i(*[int(_res) for _res in arg.split('x')]))
+        base.win.request_properties(props)
+        self._option_file['settings']['resolution'] = arg
+        self._option_file.store()
+
+    def on_aa(self, arg):
+        self._gfx_pipeline.msaa_samples = 4 if arg else 1
+        debug(f'msaa: {self._gfx_pipeline.msaa_samples}')
+        self._option_file['settings']['antialiasing'] = int(arg)
+        self._option_file.store()
+
+    def on_shadows(self, arg):
+        self._gfx_pipeline.enable_shadows = int(arg)
+        debug(f'shadows: {self._gfx_pipeline.enable_shadows}')
+        self._option_file['settings']['shadows'] = int(arg)
+        self._option_file.store()
diff --git a/pmachines/gui/play_page.py b/pmachines/gui/play_page.py
new file mode 100644 (file)
index 0000000..11c6e48
--- /dev/null
@@ -0,0 +1,49 @@
+from direct.gui.DirectGui import DirectButton
+from ya2.utils.gfx import DirectGuiMixin
+from pmachines.gui.base_page import BasePage
+
+
+class PlayPage(BasePage):
+
+    def __init__(self, menu, test_positions, gui_args, scenes, option_file, running_functional_tests, application_fsm):
+        super().__init__(menu, running_functional_tests)
+        self._test_positions = test_positions
+        self._scenes = scenes
+        self._option_file = option_file
+        self._application_fsm = application_fsm
+        for k in list(self._test_positions.keys()): del self._test_positions[k]
+        self._widgets = []
+        cmn = gui_args.button.copy() | {
+            'frameSize': (-2.4, 2.4, -2.4, 2.4),
+            'frameColor': (1, 1, 1, .8),
+            'text_scale': .64}
+        left = - (dx := .8) * (min(4, len(self._scenes)) - 1) / 2
+        from pmachines.scene.scene import Scene
+        for i, scene_name in enumerate(self._scenes):
+            top = .1 if len(self._scenes) < 5 else .6
+            row = 0 if i < 4 else 1
+            new_cmn = cmn.copy()
+            if Scene.is_done(scene_name):
+                new_cmn['frameColor'] = (1, 1, 1, .4)
+                new_cmn['text_fg'] = (.9, .9, .9, .4)
+            self._widgets += [DirectButton(
+                text=Scene.name(scene_name), pos=(left + dx * (i % 4), 1, top - dx * row),
+                command=self.start, extraArgs=[scene_name], text_wordwrap=6,
+                frameTexture='assets/images/scenes/%s.dds' % scene_name,
+                **new_cmn)]
+            self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+            name = scene_name.lower()
+            self._test_positions[name] = self._widgets[-1].pos_pixel()
+            for j in range(4):
+                tnode = self._widgets[-1].component('text%s' % j).textNode
+                height = - tnode.getLineHeight() / 2
+                height += (tnode.get_height() - tnode.get_line_height()) / 2
+                self._widgets[-1].component('text%s' % j).set_pos(0, 0, height)
+        self._widgets += [DirectButton(
+            text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
+            **gui_args.button)]
+        self._widgets[-1].__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        self._test_positions['back'] = self._widgets[-1].pos_pixel()
+
+    def start(self, cls):
+        self._application_fsm.demand('Scene', cls)
index 66a8c8395d056971d469500213c55c9d93737f13..2b9af912cc022dac532cec6981a11f0f921a9c45 100644 (file)
@@ -1,4 +1,4 @@
-from panda3d.core import load_prc_file_data
+from panda3d.core import load_prc_file_data, AudioSound
 load_prc_file_data('', 'window-type none')
 from pathlib import Path
 import sys
@@ -18,7 +18,7 @@ class AudioTests(TestCase):
     def tearDown(self):
         self.__app.destroy()
 
-    def test_audio(self):
+    def test_volume(self):
         with (patch.object(base, 'musicManager', autospec=True) as m,
               patch.object(base, 'sfxManagerList', autospec=True) as s):
             AudioTools.set_volume(.8)
@@ -30,3 +30,7 @@ class AudioTests(TestCase):
             s_args = s[0].set_volume.call_args_list[0].args
             self.assertAlmostEqual(s_args[0], .8, delta=.01)
             self.assertEqual(len(s_args), 1)
+
+    def test_loading(self):
+        s = AudioTools.load_sfx('assets/audio/sfx/rollover.ogg')
+        self.assertIsInstance(s, AudioSound)
index be0d69d03b27a13f90e003ae2c9df94b6d654a1e..da34e569f112f87c8fdaed0b252e80c7abc49aa5 100644 (file)
@@ -6,7 +6,7 @@ if '' in sys.path: sys.path.remove('')
 sys.path.append(str(Path(__file__).parent.parent.parent))
 from unittest import TestCase
 from unittest.mock import patch
-from panda3d.core import get_model_path
+from panda3d.core import get_model_path, Texture
 from direct.showbase.ShowBase import ShowBase
 from ya2.utils.gui import GuiTools
 import ya2
@@ -32,3 +32,11 @@ class GraphicsToolsTests(TestCase):
         self.assertEqual(l_args[0], '')
         self.assertEqual(l_args[1], 'window-type none')
         self.assertEqual(len(l_args), 2)
+
+    def test_font(self):
+        f = GuiTools.load_font('assets/fonts/Hanken-Book.ttf')
+        self.assertEqual(f.get_pixels_per_unit(), 60)
+        self.assertEqual(f.get_minfilter(), Texture.FTLinearMipmapLinear)
+        self.assertEqual(f.get_outline_color(), (0, 0, 0, 1))
+        self.assertAlmostEqual(f.get_outline_feather(), .2, delta=.01)
+        self.assertAlmostEqual(f.get_outline_width(), .8, delta=.01)
index 10b2dd0de67acd9e75a4a3bf543d0195e8327da1..98b519591b2bb15b9e3b5c02a9f62a31d4745303 100644 (file)
@@ -4,3 +4,6 @@ class AudioTools:
     def set_volume(volume):
         base.musicManager.set_volume(.8 * volume)
         base.sfxManagerList[0].set_volume(volume)
+
+    @staticmethod
+    def load_sfx(path): return loader.load_sfx(path)
index f5ba740ce93a4f9cebe0f88a97724aae2648dfe2..aa68e9a9f983a7885992af18476a7dc5396e404f 100644 (file)
@@ -1,4 +1,4 @@
-from panda3d.core import load_prc_file_data
+from panda3d.core import load_prc_file_data, Texture
 from ya2.utils.gfx import Point
 
 class GuiTools:
@@ -10,3 +10,11 @@ class GuiTools:
     @staticmethod
     def get_mouse():
         return Point(base.mouseWatcherNode.get_mouse())
+
+    def load_font(path):
+        font = base.loader.load_font(path)
+        font.clear()
+        font.set_pixels_per_unit(60)
+        font.set_minfilter(Texture.FTLinearMipmapLinear)
+        font.set_outline((0, 0, 0, 1), .8, .2)
+        return font