ya2 · news · projects · code · about

housekeeping: ya2.utils.gui
authorFlavio Calva <f.calva@gmail.com>
Sat, 4 Feb 2023 08:43:05 +0000 (10:43 +0200)
committerFlavio Calva <f.calva@gmail.com>
Sat, 4 Feb 2023 08:43:05 +0000 (10:43 +0200)
26 files changed:
assets/locale/po/it_IT.po
main.py
pmachines/application/application.py
pmachines/editor/inspector.py
pmachines/editor/scene.py
pmachines/editor/start_items.py
pmachines/gui/base_page.py [deleted file]
pmachines/gui/credits_page.py
pmachines/gui/main_page.py
pmachines/gui/menu.py
pmachines/gui/options_page.py
pmachines/gui/play_page.py
pmachines/scene/scene.py
prj.org
tests/ya2/utils/gui/__init__.py [new file with mode: 0644]
tests/ya2/utils/gui/test_cursor.py [new file with mode: 0644]
tests/ya2/utils/gui/test_gui.py [new file with mode: 0644]
tests/ya2/utils/test_cursor.py [deleted file]
tests/ya2/utils/test_gui.py [deleted file]
ya2/utils/cursor.py [deleted file]
ya2/utils/gui.py [deleted file]
ya2/utils/gui/__init__.py [new file with mode: 0644]
ya2/utils/gui/base_page.py [new file with mode: 0644]
ya2/utils/gui/cursor.py [new file with mode: 0644]
ya2/utils/gui/gui.py [new file with mode: 0644]
ya2/utils/gui/menu.py [new file with mode: 0644]

index 40484c79bfbf52920cf7b26b367f1fed2488a387..a0adf15d25a1aa7a8a35ba7fb72cd3a477ccb976 100644 (file)
@@ -17,217 +17,202 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: pmachines/editor/augmented_frame.py:48
+#: pmachines/editor/augmented_frame.py:53
 msgid "Collapse/Expand"
 msgstr "Nascondi/Mostra"
 
-#: pmachines/editor/inspector.py:65 pmachines/editor/inspector.py:322
-#: pmachines/editor/inspector.py:450
+#: pmachines/editor/inspector.py:70 pmachines/editor/inspector.py:345
+#: pmachines/editor/inspector.py:476
 msgid "position"
 msgstr "posizione"
 
-#: pmachines/editor/inspector.py:65
+#: pmachines/editor/inspector.py:70 pmachines/editor/inspector.py:476
 msgid "position (e.g. 0.1 2.3 4.5)"
 msgstr "posizione (e.g. 0.1 2.3 4.5)"
 
-#: pmachines/editor/inspector.py:66
+#: pmachines/editor/inspector.py:71
 msgid "roll"
 msgstr "roll"
 
-#: pmachines/editor/inspector.py:66
+#: pmachines/editor/inspector.py:71
 msgid "roll (e.g. 90)"
 msgstr "roll (e.g. 90)"
 
-#: pmachines/editor/inspector.py:67 pmachines/editor/start_items.py:59
+#: pmachines/editor/inspector.py:72 pmachines/editor/start_items.py:76
 msgid "scale"
 msgstr "scala"
 
-#: pmachines/editor/inspector.py:67 pmachines/editor/start_items.py:59
+#: pmachines/editor/inspector.py:72 pmachines/editor/start_items.py:76
 msgid "scale (e.g. 1.2)"
 msgstr "scala (e.g. 1.2)"
 
-#: pmachines/editor/inspector.py:68 pmachines/editor/start_items.py:60
+#: pmachines/editor/inspector.py:73 pmachines/editor/start_items.py:77
 msgid "mass"
 msgstr "massa"
 
-#: pmachines/editor/inspector.py:68
+#: pmachines/editor/inspector.py:73
 msgid "mass (default 1; 0 if fixed)"
 msgstr "massa (default 1; 0 se fisso)"
 
-#: pmachines/editor/inspector.py:69 pmachines/editor/start_items.py:61
+#: pmachines/editor/inspector.py:74 pmachines/editor/start_items.py:78
 msgid "restitution"
 msgstr "rimbalzo"
 
-#: pmachines/editor/inspector.py:69 pmachines/editor/start_items.py:61
+#: pmachines/editor/inspector.py:74 pmachines/editor/start_items.py:78
 msgid "restitution (default 0.5)"
 msgstr "rimbalzo (default 0.5)"
 
-#: pmachines/editor/inspector.py:70 pmachines/editor/start_items.py:62
+#: pmachines/editor/inspector.py:75 pmachines/editor/start_items.py:79
 msgid "friction"
 msgstr "frizione"
 
-#: pmachines/editor/inspector.py:70 pmachines/editor/start_items.py:62
+#: pmachines/editor/inspector.py:75 pmachines/editor/start_items.py:79
 msgid "friction (default 0.5)"
 msgstr "frizione (default 0.5)"
 
-#: pmachines/editor/inspector.py:71 pmachines/editor/inspector.py:323
-#: pmachines/editor/inspector.py:451 pmachines/editor/start_items.py:63
+#: pmachines/editor/inspector.py:76 pmachines/editor/inspector.py:346
+#: pmachines/editor/inspector.py:477 pmachines/editor/start_items.py:80
 msgid "id"
 msgstr "id"
 
-#: pmachines/editor/inspector.py:71
+#: pmachines/editor/inspector.py:76 pmachines/editor/inspector.py:477
 msgid "id of the item (for the strategies)"
 msgstr "id dell'oggetto (per le strategie)"
 
-#: pmachines/editor/inspector.py:72 pmachines/editor/start_items.py:71
+#: pmachines/editor/inspector.py:86 pmachines/editor/start_items.py:88
 msgid "strategy"
 msgstr "strategia"
 
-#: pmachines/editor/inspector.py:72 pmachines/editor/start_items.py:71
+#: pmachines/editor/inspector.py:86 pmachines/editor/start_items.py:88
 msgid "the strategy of the item"
 msgstr "la strategia dell'oggetto"
 
-#: pmachines/editor/inspector.py:73 pmachines/editor/start_items.py:72
+#: pmachines/editor/inspector.py:101 pmachines/editor/start_items.py:103
 msgid "strategy_args"
 msgstr "argomenti strategia"
 
-#: pmachines/editor/inspector.py:73 pmachines/editor/start_items.py:72
+#: pmachines/editor/inspector.py:101 pmachines/editor/start_items.py:103
 msgid "the arguments of the strategy"
 msgstr "gli argomenti della strategia"
 
-#: pmachines/editor/inspector.py:99 pmachines/editor/start_items.py:99
+#: pmachines/editor/inspector.py:128 pmachines/editor/inspector.py:504
+#: pmachines/editor/start_items.py:131
 msgid "Close"
 msgstr "Chiudi"
 
-#: pmachines/editor/inspector.py:109
+#: pmachines/editor/inspector.py:139 pmachines/editor/inspector.py:515
 msgid "Delete the item"
 msgstr "Cancella l'oggetto"
 
-#: pmachines/editor/inspector.py:264 pmachines/editor/start_items.py:309
+#: pmachines/editor/inspector.py:286 pmachines/editor/start_items.py:365
 msgid "There are errors in the strategy args."
 msgstr "Ci sono errori negli argomenti della strategia."
 
-#: pmachines/editor/scene.py:71
+#: pmachines/editor/scene.py:76
 msgid "Filename"
 msgstr "Filename"
 
-#: pmachines/editor/scene.py:86
+#: pmachines/editor/scene.py:92
 msgid "The name of the file without the extension"
 msgstr "Il nome del file senza estensione"
 
-#: pmachines/editor/scene.py:88
+#: pmachines/editor/scene.py:95
 msgid "Name"
 msgstr "Nome"
 
-#: pmachines/editor/scene.py:103
+#: pmachines/editor/scene.py:110
 msgid "The title of the scene"
 msgstr "Il titolo della scena"
 
-#: pmachines/editor/scene.py:105
+#: pmachines/editor/scene.py:113
 msgid "Description"
 msgstr "Descrizione"
 
-#: pmachines/editor/scene.py:129
+#: pmachines/editor/scene.py:137
 msgid "The description of the scene"
 msgstr "La descrizione della scena"
 
-#: pmachines/editor/scene.py:152 pmachines/editor/scene_list.py:89
+#: pmachines/editor/scene.py:162 pmachines/editor/scene_list.py:93
 msgid "Close the scene editor"
 msgstr "Chiudi l'editor della scena"
 
-#: pmachines/editor/scene.py:161
+#: pmachines/editor/scene.py:172
 msgid "Save the scene"
 msgstr "Salva la scena"
 
-#: pmachines/editor/scene.py:170
+#: pmachines/editor/scene.py:182
 msgid "Set the sorting of the scenes"
 msgstr "Imposta l'ordinamento delle scene"
 
-#: pmachines/editor/scene.py:181 pmachines/editor/scene.py:188
+#: pmachines/editor/scene.py:193 pmachines/editor/scene.py:210
 msgid "new item"
 msgstr "nuovo oggetto"
 
-#: pmachines/editor/scene.py:200
+#: pmachines/editor/scene.py:223
 msgid "Create a new item"
 msgstr "Crea un nuovo oggetto"
 
-#: pmachines/editor/scene.py:209
+#: pmachines/editor/scene.py:234
 msgid "Initial items"
 msgstr "Oggetti iniziali"
 
-#: pmachines/editor/scene.py:218
+#: pmachines/editor/scene.py:244
 msgid "New scene"
 msgstr "Nuova scena"
 
-#: pmachines/editor/scene.py:263 pmachines/editor/scene_list.py:107
+#: pmachines/editor/scene.py:289 pmachines/editor/scene_list.py:112
 msgid "You have unsaved changes. Really quit?"
 msgstr "Hai modifiche non salvate, vuoi veramente uscire?"
 
-#: pmachines/editor/scene_list.py:39
+#: pmachines/editor/scene_list.py:41
 msgid "Write the file names (without the extension), one file for each line"
 msgstr "Scrivi i nomi dei file (senza estensione), uno per riga."
 
-#: pmachines/editor/scene_list.py:66
+#: pmachines/editor/scene_list.py:68
 msgid "the list of the scenes in the proper order"
 msgstr "la lista della scene nell'ordine"
 
-#: pmachines/editor/scene_list.py:98
+#: pmachines/editor/scene_list.py:103
 msgid "Save the scene list"
 msgstr "Salva la lista delle scene"
 
-#: pmachines/editor/start_items.py:57
+#: pmachines/editor/start_items.py:61
 msgid "class"
 msgstr "classe"
 
-#: pmachines/editor/start_items.py:57
+#: pmachines/editor/start_items.py:61
 msgid "class of the item"
 msgstr "classe dell'oggetto"
 
-#: pmachines/editor/start_items.py:58
+#: pmachines/editor/start_items.py:75
 msgid "count"
 msgstr "quantità"
 
-#: pmachines/editor/start_items.py:58
+#: pmachines/editor/start_items.py:75
 msgid "number of the items"
 msgstr "numero degli oggetti"
 
-#: pmachines/editor/start_items.py:60
+#: pmachines/editor/start_items.py:77
 msgid "mass (default 1)"
 msgstr "massa (default 1)"
 
-#: pmachines/editor/start_items.py:108
+#: pmachines/editor/start_items.py:141
 msgid "Save"
 msgstr "Salva"
 
-#: pmachines/editor/start_items.py:117
+#: pmachines/editor/start_items.py:151
 msgid "Delete"
 msgstr "Cancella"
 
-#: pmachines/editor/start_items.py:126
+#: pmachines/editor/start_items.py:161
 msgid "New"
 msgstr "Nuovo"
 
-#: pmachines/editor/start_items.py:135
+#: pmachines/editor/start_items.py:171
 msgid "Next"
 msgstr "Prossimo"
 
-#: pmachines/gui/main_page.py:19
-msgid "Play"
-msgstr "Gioca"
-
-#: pmachines/gui/main_page.py:24
-msgid "Options"
-msgstr "Opzioni"
-
-#: pmachines/gui/main_page.py:29
-msgid "Credits"
-msgstr "Riconoscimenti"
-
-#: pmachines/gui/main_page.py:39 pmachines/scene/scene.py:305
-msgid "Exit"
-msgstr "Esci"
-
-#: pmachines/gui/credits_page.py:25
+#: pmachines/gui/credits_page.py:11
 msgid ""
 "Code and gfx\n"
 "  \ 1scale\ 1Flavio Calva\ 2\n"
@@ -243,11 +228,11 @@ msgstr ""
 "Music\n"
 "  \ 1scale\ 1Stefan Grossmann\ 2"
 
-#: pmachines/gui/credits_page.py:30
+#: pmachines/gui/credits_page.py:17
 msgid "Website"
 msgstr "Sito web"
 
-#: pmachines/gui/credits_page.py:33
+#: pmachines/gui/credits_page.py:23
 msgid ""
 "Special thanks to:\n"
 "  \ 1scale\ 1rdb\ 2\n"
@@ -259,66 +244,82 @@ 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
+#: pmachines/gui/credits_page.py:29 pmachines/gui/options_page.py:132
+#: pmachines/gui/play_page.py:32
 msgid "Back"
 msgstr "Indietro"
 
-#: pmachines/gui/options_page.py:34 pmachines/gui/options_page.py:37
+#: pmachines/gui/main_page.py:11
+msgid "Play"
+msgstr "Gioca"
+
+#: pmachines/gui/main_page.py:18
+msgid "Options"
+msgstr "Opzioni"
+
+#: pmachines/gui/main_page.py:25
+msgid "Credits"
+msgstr "Riconoscimenti"
+
+#: pmachines/gui/main_page.py:32 pmachines/scene/scene.py:307
+msgid "Exit"
+msgstr "Esci"
+
+#: pmachines/gui/options_page.py:30 pmachines/gui/options_page.py:33
 #: 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:30 pmachines/gui/options_page.py:34
 #: pmachines/gui/options_page.py:146
 msgid "Italian"
 msgstr "Italiano"
 
-#: pmachines/gui/options_page.py:50
+#: pmachines/gui/options_page.py:47
 msgid "Language"
 msgstr "Linguaggio"
 
-#: pmachines/gui/options_page.py:60
+#: pmachines/gui/options_page.py:56
 msgid "Volume"
 msgstr "Volume"
 
-#: pmachines/gui/options_page.py:78
+#: pmachines/gui/options_page.py:70
 msgid "Fullscreen"
 msgstr "Schermo intero"
 
-#: pmachines/gui/options_page.py:109
+#: pmachines/gui/options_page.py:101
 msgid "Resolution"
 msgstr "Risoluzione"
 
-#: pmachines/gui/options_page.py:121
+#: pmachines/gui/options_page.py:114
 msgid "Antialiasing"
 msgstr "Antialiasing"
 
-#: pmachines/gui/options_page.py:129
+#: pmachines/gui/options_page.py:123
 msgid "Shadows"
 msgstr "Ombre"
 
-#: pmachines/scene/scene.py:106
+#: pmachines/scene/scene.py:108
 msgid "Scene: "
 msgstr "Scena: "
 
-#: pmachines/scene/scene.py:306
+#: pmachines/scene/scene.py:308
 msgid "Instructions"
 msgstr "Istruzioni"
 
-#: pmachines/scene/scene.py:307
+#: pmachines/scene/scene.py:309
 msgid "Run"
 msgstr "Vai"
 
-#: pmachines/scene/scene.py:313
+#: pmachines/scene/scene.py:315
 msgid "Editor"
 msgstr "Editor"
 
-#: pmachines/scene/scene.py:636
+#: pmachines/scene/scene.py:638
 msgid "You win!"
 msgstr "Hai vinto!"
 
-#: pmachines/scene/scene.py:716
+#: pmachines/scene/scene.py:718
 msgid "You have failed!"
 msgstr "Hai perso!"
 
diff --git a/main.py b/main.py
index cd2889eb748b8df9e9f26b117426b6bffe92b54e..f7a5c7954d1b14e54b0b1de5c1a8ee15d231af55 100644 (file)
--- a/main.py
+++ b/main.py
@@ -1,7 +1,7 @@
 from ya2.utils.log import LogManager
 LogManager.before_init_setup('pmachines')
 from sys import argv
-from ya2.utils.gui import GuiTools
+from ya2.utils.gui.gui import GuiTools
 if '--version' in argv: GuiTools.no_window()
 from os.path import exists
 from p3d_appimage import AppImageBuilder
index 0eb9593ff8799fa5667e2f2e3f722f500ea7ac79..77aa71148ee99df4d542daa449b7a20879a16766 100755 (executable)
@@ -26,6 +26,7 @@ from ya2.utils.log import WindowedLogManager
 from ya2.utils.functional import FunctionalTest
 from ya2.utils.asserts import Assert
 from ya2.utils.gfx import DirectGuiMixin
+from ya2.utils.gui.gui import GuiTools
 
 
 class MainFsm(FSM):
@@ -43,8 +44,8 @@ class MainFsm(FSM):
         self.__do_asserts()
         DirectGuiMixin.clear_tooltips()
 
-    def enterScene(self, cls):
-        self._pmachines.on_scene_enter(cls)
+    def enterScene(self, scene_name):
+        self._pmachines.on_scene_enter(scene_name)
 
     def exitScene(self):
         self._pmachines.on_scene_exit()
@@ -89,6 +90,7 @@ class Pmachines:
         self.log_mgr = WindowedLogManager.init_cls()
         self._pos_mgr = {}
         self._prepare_window(args)
+        GuiTools.init()
         self._fsm = MainFsm(self)
         self._fsm.demand('Start')  # otherwise it is Off and cleanup in tests won't work
         if args.update:
@@ -126,7 +128,8 @@ class Pmachines:
     def on_menu_enter(self):
         self._menu_bg = Background()
         self._menu = Menu(
-            self._fsm, self.lang_mgr, self._options,
+            lambda scene_name: self._fsm.demand('Scene', scene_name),
+            self.lang_mgr.set_language, self._options,
             self._pipeline, self.scenes(), self._args.functional_test or self._args.functional_ref,
             self._pos_mgr)
 
index d08961a38b41e5a75bf310df0aac7f46ad1b1e6d..f5f04f07e3504498b4f8a65e49e7510dc9792f95 100644 (file)
@@ -14,7 +14,7 @@ from pmachines.items.item import ItemStrategy, FixedStrategy, StillStrategy
 from pmachines.items.box import HitStrategy
 from pmachines.items.domino import DownStrategy, UpStrategy
 from pmachines.editor.augmented_frame import AugmentedDirectFrame
-from pmachines.gui.options_page import DirectOptionMenuTestable
+from ya2.utils.gui.base_page import DirectOptionMenuTestable
 from ya2.utils.gfx import DirectGuiMixin
 
 
index 942ab5356f759b4e9d86ee462c6791d711ee8dcb..84f4eaf9aae98e277e7e6f9b815701225cf29378 100644 (file)
@@ -20,7 +20,7 @@ from pmachines.editor.inspector import Inspector, PixelSpaceInspector, WorldSpac
 from pmachines.editor.start_items import StartItems
 from ya2.utils.gfx import Point, DirectGuiMixin
 from pmachines.editor.augmented_frame import AugmentedDirectFrame
-from pmachines.gui.options_page import DirectOptionMenuTestable
+from ya2.utils.gui.base_page import DirectOptionMenuTestable
 
 
 class SceneEditor(DirectObject):
@@ -87,6 +87,7 @@ class SceneEditor(DirectObject):
             initialText=json_name,
             parent=self._frm,
             text_fg=self._common['text_fg'])
+        self.__filenamename_entry['text_fg'] = (1, 0, 0, 1)
         self.__filenamename_entry.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
         self.__filenamename_entry.set_tooltip(_('The name of the file without the extension'), *tooltip_args)
         self.__pos_mgr['editor_scene_filename'] = self.__filenamename_entry.pos_pixel()
index a214410e60e4c3ae5ab414f0a3c3056ba3607217..a899452f2315a34b97e9ec130aa8cad2ab8891e4 100644 (file)
@@ -12,7 +12,7 @@ from direct.showbase.DirectObject import DirectObject
 from ya2.utils.gfx import DirectGuiMixin
 from pmachines.items.item import Item, ItemStrategy
 from pmachines.editor.augmented_frame import AugmentedDirectFrame
-from pmachines.gui.options_page import DirectOptionMenuTestable
+from ya2.utils.gui.base_page import DirectOptionMenuTestable
 
 
 class StartItems(DirectObject):
diff --git a/pmachines/gui/base_page.py b/pmachines/gui/base_page.py
deleted file mode 100644 (file)
index e26b726..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-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
index bf5eeb24e8ef7a5ab5d73fcf781d7c065d0de091..a0d617216b5b7491e366552c6cb749bc77407a87 100644 (file)
@@ -1,47 +1,38 @@
 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
+from ya2.utils.gui.base_page import BasePage, TextInfo, ButtonInfo
 
 
 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 _build_widgets(self):
+        text_dev = TextInfo(
+            text=_('Code and gfx\n  \1scale\1Flavio Calva\2\n\n\nMusic\n  \1scale\1Stefan Grossmann\2'),
+            pos=(-.9, .55),
+            align='left')
+        self._add_text(text_dev)
+        website_button = ButtonInfo(
+            id='website',
+            text=_('Website'),
+            pos=(-.6, .29),
+            command=self.__on_website,
+            scale=.08)
+        self._add_button(website_button)
+        thanks_text = TextInfo(
+            text=_('Special thanks to:\n  \1scale\1rdb\2\n  \1scale\1Luisa Tenuta\2\n  \1scale\1Damiana Ercolani\2'),
+            pos=(.1, .55),
+            align='left')
+        self._add_text(thanks_text)
+        back_button = ButtonInfo(
+            id='back',
+            text=_('Back'),
+            pos=(0, -.8),
+            command=self._set_page,
+            command_args=['main'])
+        self._add_button(back_button)
 
-    def on_website(self):
+    def __on_website(self):
         if platform.startswith('linux'):
             environ['LD_LIBRARY_PATH'] = ''
             system('xdg-open https://www.ya2.it')
index afde786acd74074649bae264ef7ce2230a79be1a..e2ad88073459b36b4d5c56c6b4186cf5b6638c2e 100644 (file)
@@ -1,67 +1,41 @@
 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
+from ya2.utils.gui.base_page import BasePage, ButtonInfo
 
 
 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]
+    def _build_widgets(self):
+        play_args = ButtonInfo(
+            id='play',
+            text=_('Play'),
+            pos=(0, .6),
+            command=self._set_page,
+            command_args=['play'])
+        self._add_button(play_args)
+        option_args = ButtonInfo(
+            id='options',
+            text=_('Options'),
+            pos=(0, .2),
+            command=self._set_page,
+            command_args=['options'])
+        self._add_button(option_args)
+        credits_args = ButtonInfo(
+            id='credits',
+            text=_('Credits'),
+            pos=(0, -.2),
+            command=self._set_page,
+            command_args=['credits'])
+        self._add_button(credits_args)
+        exit_args = ButtonInfo(
+            id='exit',
+            text=_('Exit'),
+            pos=(0, -.6),
+            command=self.__on_exit)
+        self._add_button(exit_args)
+        self._rearrange_buttons_width()
+
+    def __on_exit(self):
+        if self._page_info.running_functional_tests:
+            ServerProxy('http://localhost:7000').destroy()
+        exit()
index 85200ecc0c4439aa7ea5cc126d85ebc40885681a..c1bfd5a2bd1adbcadc35e07b46ecca4e3d2d7db5 100644 (file)
@@ -1,77 +1,34 @@
-from collections import namedtuple
-from panda3d.core import TextNode
-from direct.gui.DirectGuiGlobals import FLAT
-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
+from ya2.utils.gui.menu import BaseMenu
+from ya2.utils.gui.cursor import MouseCursorInfo
 
 
-class Menu:
+class Menu(BaseMenu):
 
-    def __init__(self, application_fsm, language_manager, option_file,
+    def __init__(self, start_scene, set_language, option_file,
                  gfx_pipeline, scenes,
                  running_functional_tests, test_positions):
-        super().__init__()
-        self.__application_fsm = application_fsm
-        self.__language_manager = language_manager
+        c = MouseCursorInfo(
+            'assets/images/buttons/arrowUpLeft.dds',
+            running_functional_tests,
+            (.04, 1, .04),
+            (.5, .5, .5, 1),
+            (.01, .01))
+        super().__init__(c, running_functional_tests, test_positions)
+        self.__start_scene = start_scene
+        self.__set_language = set_language
         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': GuiTools.load_font('assets/fonts/Hanken-Book.ttf'),
-            'text_fg': (.9, .9, .9, 1),
-            'relief': FLAT,
-            'frameColor': (.4, .4, .4, .14),
-            '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': h,
-            'text_align': TextNode.A_center,
-        } | 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 slider['rolloverSound']
-        del slider['clickSound']
-        GuiArgs = namedtuple('GuiArgs', 'button option slider')
-        gui_args = GuiArgs(button, option, slider)
-        return gui_args
-
-    def set_page(self, page_name):
-        self.destroy()
-        if self.__page: self.__page.destroy()
+    def _set_page(self, page_name):
         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):
-        if self.__page:
-            self.__page.destroy()
-        self.__page = None
+            case 'main': p = MainPage(self._page_info)
+            case 'credits': p = CreditsPage(self._page_info)
+            case 'options': p = OptionsPage(self._page_info, self.__option_file, self.__set_language, self.__gfx_pipeline)
+            case 'play': p = PlayPage(self._page_info, self.__scenes, self.__start_scene)
+        return p
index 29d8a7289456426fbe506f91928f4532c267e3a3..4f479a3c2622a65ecad42539ac9840b1d5970f7a 100644 (file)
@@ -1,88 +1,80 @@
 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 panda3d.core import WindowProperties, LVector2i
+from direct.showbase.DirectObject import DirectObject
+from direct.gui.DirectGui import DirectFrame
+from ya2.utils.gfx import DirectGuiMixin, pos_pixel
 from ya2.utils.audio import AudioTools
-from pmachines.gui.base_page import BasePage
+from ya2.utils.gui.base_page import BasePage, OptionMenuInfo, TextInfo, SliderInfo, CheckButtonInfo, ButtonInfo
 
 
-class DirectOptionMenuTestable(DirectOptionMenu):
+class OptionsPage(BasePage, DirectObject):
 
-    def __init__(self, parent=None, **kw):
-        DirectOptionMenu.__init__(self, parent, **kw)
-        self.initialiseoptions(DirectOptionMenuTestable)
+    def __init__(self, page_info, option_file, set_language, gfx_pipeline):
+        DirectObject.__init__(self)
+        self.__option_file = option_file
+        self.__set_language = set_language
+        self.__gfx_pipeline = gfx_pipeline
+        self.__enforced_resolution = ''
+        super().__init__(page_info)
 
-    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()])
+    def _build_widgets(self):
+        self.__build_language()
+        self.__build_volume()
+        self.__build_fullscreen()
+        self.__build_resolution()
+        self.__build_aa()
+        self.__build_shadows()
+        self.__build_back()
 
-
-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 = []
+    def __build_language(self):
         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']]
+        }[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]
+                if element in self._page_info.test_positions:
+                    del self._page_info.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']
+                self._page_info.test_positions[lng] = (pos[0] + 5, pos[1])
+        language_menu = OptionMenuInfo(
+            id='languages',
+            text=_('Language'),
+            items=items,
+            initial_item=inititem,
+            pos=(0, .8),
+            command=self.on_language)
+        self._add_option_menu(language_menu, lang_cb)
+
+    def __build_volume(self):
+        t = TextInfo(
+            text=_('Volume'),
+            pos=(-.56, .55),
+            align='left')
+        self._add_text(t)
+        s = SliderInfo(
+            pos=(.3, 1, .57),
+            value=self.__option_file['settings']['volume'],
+            scale=(.28, 1, .5),
+            command=self.on_volume)
+        self._slider = self._add_slider(s)
+
+    def __build_fullscreen(self):
+        c = CheckButtonInfo(
+            id='fullscreen',
+            text=_('Fullscreen'),
+            pos=(0, .3),
+            command=self.on_fullscreen,
+            value=self.__option_file['settings']['fullscreen'])
+        self._add_check_button(c)
+
+    def __build_resolution(self):
+        res = self.__option_file['settings']['resolution']
         d_i = base.pipe.get_display_information()
         def _res(idx):
             return d_i.get_display_mode_width(idx), \
@@ -95,94 +87,110 @@ class OptionsPage(BasePage):
         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]
+                if element in self._page_info.test_positions:
+                    del self._page_info.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._page_info.test_positions['res_' + tgt_res] = (pos[0] + 5, pos[1])
+        r = OptionMenuInfo(
+            id='resolutions',
+            text=_('Resolution'),
+            items=resolutions,
+            initial_item=res,
+            pos=(0, .05),
+            command=self.on_resolution)
+        o = self._add_option_menu(r, res_cb)
+        pos_res = o.pos_pixel()
+        self._page_info.test_positions['res_1440x900'] = [pos_res[0] + 320, pos_res[1] + 75]
+        self._page_info.test_positions['res_1360x768'] = [pos_res[0] + 430, pos_res[1] -285]
+
+    def __build_aa(self):
+        c = CheckButtonInfo(
+            id='aa',
+            text=_('Antialiasing'),
+            pos=(0, -.2),
+            command=self.on_aa,
+            value=self.__option_file['settings']['antialiasing'])
+        self._add_check_button(c)
+
+    def __build_shadows(self):
+        c = CheckButtonInfo(
+            id='shadows',
+            text=_('Shadows'),
+            pos=(0, -.45),
+            command=self.on_shadows,
+            value=self.__option_file['settings']['shadows'])
+        self._add_check_button(c)
+
+    def __build_back(self):
+        back_button = ButtonInfo(
+            id='back',
+            text=_('Back'),
+            pos=(0, -.8),
+            command=self._set_page,
+            command_args=['main'])
+        self._add_button(back_button)
         self.accept('enforce_resolution', self.enforce_resolution)
 
+    def enforce_resolution(self, resolution):
+        self.__enforced_resolution = resolution
+        info('enforced resolution: ' + 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')
+        self.__set_language(lang_code)
+        self.__option_file['settings']['language'] = lang_code[:2]
+        self.__option_file.store()
+        self._set_page('options')
 
     def on_volume(self):
-        self._option_file['settings']['volume'] = self._slider['value']
+        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:
+        if not self._page_info.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()
+        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('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()
+        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()
+        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()
+        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()
+
+    def on_back(self):
+        self.__option_file.store()
+        super().on_back()
+
+    def destroy(self):
+        super().destroy()
+        self.ignore('enforce_resolution')
index 11c6e48696259039bb380aff0f1d92828f2f2680..117b98db96b5ebc5788d1a5d23ef604d005cc486 100644 (file)
@@ -1,49 +1,36 @@
-from direct.gui.DirectGui import DirectButton
-from ya2.utils.gfx import DirectGuiMixin
-from pmachines.gui.base_page import BasePage
+from ya2.utils.gui.base_page import BasePage, ButtonInfo
 
 
 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
+    def __init__(self, page_info, scenes, start_scene):
+        self.__scenes = scenes
+        self.__start_scene = start_scene
+        super().__init__(page_info)
+
+    def _build_widgets(self):
+        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
+        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)
+            i = ButtonInfo(
+                id=scene_name.lower(),
+                text=Scene.name(scene_name),
+                pos=(left + dx * (i % 4), top - dx * row),
+                command=self.__start_scene,
+                command_args=[scene_name],
+                wordwrap=6,
+                texture='assets/images/scenes/%s.dds' % scene_name,
+                size=(-2.4, 2.4, -2.4, 2.4),
+                text_scale=.64,
+                color=(1, 1, 1, .4) if Scene.is_done(scene_name) else (1, 1, 1, .8),
+                text_color=(.9, .9, .9, .4) if Scene.is_done(scene_name) else (.9, .9, .9, 1))
+            self._add_button(i, True)
+        back_button = ButtonInfo(
+            id='back',
+            text=_('Back'),
+            pos=(0, -.8),
+            command=self._set_page,
+            command_args=['main'])
+        self._add_button(back_button)
index 5745a14338da6fb48c1b6feb93077585e2d3a121..e8172f9f85141c8913974303a5238179362bc66e 100644 (file)
@@ -22,9 +22,9 @@ from pmachines.items.shelf import Shelf
 from pmachines.items.teetertooter import TeeterTooter
 from pmachines.items.test_item import PixelSpaceTestItem, WorldSpaceTestItem
 from pmachines.editor.scene import SceneEditor
-from ya2.utils.cursor import MouseCursor
+from ya2.utils.gui.cursor import MouseCursor, MouseCursorInfo
 from ya2.utils.gfx import GfxTools, DirectGuiMixin
-from ya2.utils.gui import GuiTools
+from ya2.utils.gui.gui import GuiTools
 
 
 class Scene(DirectObject):
@@ -52,9 +52,10 @@ class Scene(DirectObject):
         self.json = {}
         self.accept('enforce_result', self.enforce_result)
         self._set_camera()
-        self._cursor = MouseCursor(
-            'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
-            (.01, .01), testing)
+        c = MouseCursorInfo(
+            'assets/images/buttons/arrowUpLeft.dds', testing, (.04, 1, .04), (.5, .5, .5, 1),
+            (.01, .01))
+        self._cursor = MouseCursor(c)
         self.__set_font()
         self._set_gui()
         self._set_lights()
diff --git a/prj.org b/prj.org
index 06bfb4bc425d064353a73bcef1faa2dfe29e85d9..061dc038caa45a1a18ad39fce6b17638222ea2fe 100644 (file)
--- a/prj.org
+++ b/prj.org
@@ -9,7 +9,7 @@
 * BACKLOG actions: rewind, prev, next
 * BACKLOG (python 3.11) manylinux2014_x86_64
 * BACKLOG (when panda3d provides it) android build (test with the emulator of android studio)
-* BACKLOG (when itch.io's client works with wine) functional tests for windows-itch.io
+* BACKLOG (test with wine 8.0 | when itch.io's client works with wine) functional tests for windows-itch.io
 * calendar                                                         :calendar:
 ** publish post q1; reschedule
 SCHEDULED: <2023-03-20 Mon +1y>
diff --git a/tests/ya2/utils/gui/__init__.py b/tests/ya2/utils/gui/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/ya2/utils/gui/test_cursor.py b/tests/ya2/utils/gui/test_cursor.py
new file mode 100644 (file)
index 0000000..8a8a011
--- /dev/null
@@ -0,0 +1,33 @@
+from panda3d.core import load_prc_file_data
+load_prc_file_data('', 'window-type none')
+from pathlib import Path
+import sys
+if '' in sys.path: sys.path.remove('')
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from unittest import TestCase
+from direct.showbase.ShowBase import ShowBase
+from ya2.utils.gui.cursor import MouseCursor, MouseCursorInfo
+
+
+class CursorTests(TestCase):
+
+    def setUp(self):
+        self.__app = ShowBase()
+
+    def tearDown(self):
+        self.__app.destroy()
+
+    def test_cursor(self):
+        i = MouseCursorInfo('tests/assets/images/icon16.png', False, (1, 1, 1), (1, 1, 1, 1), (0, 0))
+        c = MouseCursor(i)
+        c.set_image('tests/assets/images/icon32.png')
+        self.assertEqual('tests/assets/images/icon32.png', c._MouseCursor__image.get_texture().get_filename())
+        c.set_color((0, 1, 0, 1))
+        self.assertEqual((0, 1, 0, 1), c._MouseCursor__image.get_color())
+        c.show()
+        self.assertFalse(c._MouseCursor__image.is_hidden())
+        c.hide()
+        self.assertTrue(c._MouseCursor__image.is_hidden())
+        c.destroy()
+        self.assertNotIn('on_frame_cursor', [t.name for t in taskMgr.getTasks() + taskMgr.getDoLaters()])
+        self.assertTrue(c._MouseCursor__image.is_empty())
diff --git a/tests/ya2/utils/gui/test_gui.py b/tests/ya2/utils/gui/test_gui.py
new file mode 100644 (file)
index 0000000..1472a38
--- /dev/null
@@ -0,0 +1,42 @@
+from panda3d.core import load_prc_file_data
+load_prc_file_data('', 'window-type offscreen')
+from pathlib import Path
+import sys
+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, Texture
+from direct.showbase.ShowBase import ShowBase
+from ya2.utils.gui.gui import GuiTools
+import ya2
+
+
+class TestApp(ShowBase): pass
+
+
+class GraphicsToolsTests(TestCase):
+
+    def setUp(self):
+        self.__app = TestApp()
+        get_model_path().append_directory(str(Path(__file__).parent.parent.parent))
+
+    def tearDown(self):
+        self.__app.destroy()
+
+    @patch.object(ya2.utils.gui.gui, 'load_prc_file_data')
+    def test_no_window(self, l_mock):
+        GuiTools.no_window()
+        l_mock.assert_called_once()
+        l_args = l_mock.call_args_list[0].args
+        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)
diff --git a/tests/ya2/utils/test_cursor.py b/tests/ya2/utils/test_cursor.py
deleted file mode 100644 (file)
index b0f69e1..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-from panda3d.core import load_prc_file_data
-load_prc_file_data('', 'window-type none')
-from pathlib import Path
-import sys
-if '' in sys.path: sys.path.remove('')
-sys.path.append(str(Path(__file__).parent.parent.parent))
-from unittest import TestCase
-from direct.showbase.ShowBase import ShowBase
-from ya2.utils.cursor import MouseCursor
-
-
-class CursorTests(TestCase):
-
-    def setUp(self):
-        self.__app = ShowBase()
-
-    def tearDown(self):
-        self.__app.destroy()
-
-    def test_cursor(self):
-        c = MouseCursor('tests/assets/images/icon16.png', (1, 1, 1), (1, 1, 1, 1), (0, 0), False)
-        c.set_image('tests/assets/images/icon32.png')
-        self.assertEqual('tests/assets/images/icon32.png', c._MouseCursor__image.get_texture().get_filename())
-        c.set_color((0, 1, 0, 1))
-        self.assertEqual((0, 1, 0, 1), c._MouseCursor__image.get_color())
-        c.show()
-        self.assertFalse(c._MouseCursor__image.is_hidden())
-        c.hide()
-        self.assertTrue(c._MouseCursor__image.is_hidden())
-        c.destroy()
-        self.assertNotIn('on_frame_cursor', [t.name for t in taskMgr.getTasks() + taskMgr.getDoLaters()])
-        self.assertTrue(c._MouseCursor__image.is_empty())
diff --git a/tests/ya2/utils/test_gui.py b/tests/ya2/utils/test_gui.py
deleted file mode 100644 (file)
index da34e56..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-from panda3d.core import load_prc_file_data
-load_prc_file_data('', 'window-type offscreen')
-from pathlib import Path
-import sys
-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, Texture
-from direct.showbase.ShowBase import ShowBase
-from ya2.utils.gui import GuiTools
-import ya2
-
-
-class TestApp(ShowBase): pass
-
-
-class GraphicsToolsTests(TestCase):
-
-    def setUp(self):
-        self.__app = TestApp()
-        get_model_path().append_directory(str(Path(__file__).parent.parent.parent))
-
-    def tearDown(self):
-        self.__app.destroy()
-
-    @patch.object(ya2.utils.gui, 'load_prc_file_data')
-    def test_no_window(self, l_mock):
-        GuiTools.no_window()
-        l_mock.assert_called_once()
-        l_args = l_mock.call_args_list[0].args
-        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)
diff --git a/ya2/utils/cursor.py b/ya2/utils/cursor.py
deleted file mode 100644 (file)
index 3e004da..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-from panda3d.core import WindowProperties, Texture
-from direct.gui.OnscreenImage import OnscreenImage
-from ya2.utils.logics import LogicsTools
-
-
-class MouseCursor:
-
-    def __init__(self, image_path, scale, color, hotspot_position, functional_test):
-        self.__functional_test = functional_test
-        if not image_path: return
-        self.__hide_system_cursor()
-        self.__set_image(image_path, scale, color)
-        self.__set_hotspot(hotspot_position, scale)
-        self._tsk = taskMgr.add(self.__on_frame, 'on_frame_cursor')
-
-    def __hide_system_cursor(self):
-        p = WindowProperties()
-        p.set_cursor_hidden(True)
-        if LogicsTools.windowed: base.win.request_properties(p)
-
-    def __set_image(self, image_path, scale, color):
-        self.__image = OnscreenImage(image_path, scale=scale)
-        self.__image.set_color(color)
-        self.__image.set_bin('gui-popup', 50)
-        alpha_formats = [Texture.FRgba]
-        if self.__image.get_texture().get_format() in alpha_formats:
-            self.__image.set_transparency(True)
-
-    def __set_hotspot(self, hotspot_position, scale):
-        self.__hotspot_dx = scale[0] * (1 - 2 * hotspot_position[0])
-        self.__hotspot_dy = scale[2] * (1 - 2 * hotspot_position[1])
-
-    def __on_frame(self, task):
-        m = base.mouseWatcherNode
-        if not m or not m.hasMouse(): return task.again
-        mouse = m.get_mouse_x(), m.get_mouse_y()
-        h_x = mouse[0] * base.get_aspect_ratio() + self.__hotspot_dx
-        self.__image.set_pos((h_x, 1, mouse[1] - self.__hotspot_dy))
-        return task.again
-
-    def set_image(self, image):
-        self.__image.set_texture(loader.load_texture(image), 1)
-
-    def set_color(self, color):
-        self.__image.set_color(color)
-
-    def show(self):
-        if not self.__functional_test: return self.__image.show()
-
-    def hide(self):
-        return self.__image.hide()
-
-    def destroy(self):
-        taskMgr.remove(self._tsk)
-        self.__image.destroy()
diff --git a/ya2/utils/gui.py b/ya2/utils/gui.py
deleted file mode 100644 (file)
index aa68e9a..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-from panda3d.core import load_prc_file_data, Texture
-from ya2.utils.gfx import Point
-
-class GuiTools:
-
-    @staticmethod
-    def no_window():
-        load_prc_file_data('', 'window-type none')
-
-    @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
diff --git a/ya2/utils/gui/__init__.py b/ya2/utils/gui/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/ya2/utils/gui/base_page.py b/ya2/utils/gui/base_page.py
new file mode 100644 (file)
index 0000000..6b3a0df
--- /dev/null
@@ -0,0 +1,243 @@
+from dataclasses import dataclass
+from panda3d.core import TextNode
+from direct.gui.OnscreenText import OnscreenText
+from direct.gui.DirectGui import DirectButton, DirectOptionMenu, DirectSlider, DirectCheckButton
+from direct.gui.DirectGuiGlobals import FLAT
+from ya2.utils.audio import AudioTools
+from ya2.utils.gfx import DirectGuiMixin, GfxTools
+from ya2.utils.gui.gui import GuiTools
+
+
+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()])
+
+
+@dataclass
+class BaseInfo:
+    id: ... = ''
+    text: ... = None
+    pos: ... = None
+    scale: ... = None
+    wordwrap: ... = None
+    texture: ... = None
+    text_scale: ... = None
+    font: ... = None
+    color: ... = None
+
+    def to_p3d_args(self):
+        d = {}
+        d['text'] = self.text
+        d['pos'] = self.pos or (0, 1, 0)
+        d['scale'] = self.scale or .12
+        d['font'] = self.font or GuiTools.load_font('assets/fonts/Hanken-Book.ttf')
+        d['fg'] = self.color or (.9, .9, .9, 1)
+        return d
+
+
+@dataclass
+class ButtonInfo(BaseInfo):
+    command: ... = None
+    command_args: ... = None
+    size: ... = None
+    text_color: ... = None
+    relief: ... = None
+    rollover_sound: ... = None
+    click_sound: ... = None
+
+    def to_p3d_args(self):
+        d = super().to_p3d_args()
+        d['frameSize'] = self.size or (-4.8, 4.8, -.6, 1.2)
+        d['command'] = self.command
+        d['extraArgs'] = self.command_args or []
+        d['text_fg'] = self.text_color or d.pop('fg')
+        if 'fg' in d: del d['fg']
+        d['text_font'] = self.font or d.pop('font')
+        d['frameColor'] = self.color or (.4, .4, .4, .14)
+        d['relief'] = self.relief or FLAT
+        d['rolloverSound'] = self.rollover_sound or AudioTools.load_sfx('assets/audio/sfx/rollover.ogg')
+        d['clickSound'] = self.click_sound or AudioTools.load_sfx('assets/audio/sfx/click.ogg')
+        d['pos'] = (self.pos[0], 1, self.pos[1]) if len(self.pos) == 2 else self.pos
+        d['text_wordwrap'] = self.wordwrap
+        d['frameTexture'] = self.texture
+        d['text_scale'] = self.text_scale or 1
+        return d
+
+    def __build_gui_args(self):
+        base = {
+            'scale': .12,
+            'text_font': GuiTools.load_font('assets/fonts/Hanken-Book.ttf'),
+            'text_fg': (.9, .9, .9, 1),
+            'relief': FLAT,
+            'frameColor': (.4, .4, .4, .14),
+            'rolloverSound': AudioTools.load_sfx('assets/audio/sfx/rollover.ogg'),
+            'clickSound': AudioTools.load_sfx('assets/audio/sfx/click.ogg')}
+        button = {'size': (-4.8, 4.8, -.6, 1.2)} | base
+        return button
+
+
+@dataclass
+class SliderInfo(ButtonInfo):
+    range: ... = None
+    scale: ... = None
+    value: ... = None
+
+    def to_p3d_args(self):
+        d = super().to_p3d_args()
+        d['range'] = self.range or (0, 1)
+        d['thumb_frameColor'] = (.4, .4, .4, .4)
+        d['thumb_scale'] = 1.6
+        d['thumb_relief'] = d['relief']
+        d['scale'] = self.scale or .4
+        d['value'] = self.value or 0
+        del d['rolloverSound']
+        del d['clickSound']
+        del d['frameSize']
+        return d
+
+
+@dataclass
+class OptionMenuInfo(ButtonInfo):
+    items: ... = None
+    initial_item: ... = None
+    item_color: ... = None
+    popup_color: ... = None
+    item_relief: ... = None
+    item_text_font: ... = None
+    item_text_fg: ... = None
+    highlight_color: ... = None
+    text_align: ... = None
+
+    def to_p3d_args(self):
+        d = super().to_p3d_args()
+        d['items'] = self.items
+        d['initialitem'] = self.initial_item
+        d['item_frameColor'] = d['frameColor']
+        d['popupMarker_frameColor'] = d['frameColor']
+        d['popupMarker_relief'] = d['relief']
+        d['item_relief'] = d['relief']
+        d['item_text_font'] = d['text_font']
+        d['item_text_fg'] = d['text_fg']
+        d['textMayChange'] = 1
+        h = d['frameColor']
+        h = (h[0] + .2, h[1] + .2, h[2] + .2, h[3] + .2)
+        d['highlightColor'] = self.highlight_color or h
+        d['text_align'] = TextNode.A_center
+        d['frameSize'] = (f:=d['frameSize'])[0], f[1] - .56, f[2], f[3]
+        return d
+
+
+@dataclass
+class CheckButtonInfo(ButtonInfo):
+    value: ... = None
+
+    def to_p3d_args(self):
+        d = super().to_p3d_args()
+        h = d['frameColor']
+        h = (h[0] + .2, h[1] + .2, h[2] + .2, h[3] + .2)
+        d['indicator_frameColor'] = h
+        d['indicator_relief'] = d['relief']
+        d['indicatorValue'] = self.value or 0
+        return d
+
+
+@dataclass
+class TextInfo(BaseInfo):
+    align: ... = None
+
+    def to_p3d_args(self):
+        d = super().to_p3d_args()
+        if self.align:
+            a = {'left': TextNode.A_left,
+                 'right': TextNode.A_right,
+                 'center': TextNode.A_center}[self.align]
+            d['align'] = a
+        return d
+
+
+class BasePage:
+
+    def __init__(self, page_info):
+        self.__widgets = []
+        self._page_info = page_info
+        for k in list(self._page_info.test_positions.keys()):
+            del self._page_info.test_positions[k]
+        self._build_widgets()
+
+    def _add_text(self, text_info):
+        t = OnscreenText(
+            **text_info.to_p3d_args())
+        self.__widgets += [t]
+        return t
+
+    def _add_button(self, button_args, vertically_centered=False):
+        b = DirectButton(**button_args.to_p3d_args())
+        b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
+        self._page_info.test_positions[button_args.id] = b.pos_pixel()
+        self.__widgets += [b]
+        if vertically_centered:
+            for j in range(4):
+                tnode = b.component('text%s' % j).textNode
+                height = - tnode.getLineHeight() / 2
+                height += (tnode.get_height() - tnode.get_line_height()) / 2
+                b.component('text%s' % j).set_pos(0, 0, height)
+        return b
+
+    def _add_check_button(self, button_args):
+        b = DirectCheckButton(**button_args.to_p3d_args())
+        b.__class__ = type('DirectCheckButtonMixed', (DirectCheckButton, DirectGuiMixin), {})
+        self.__widgets += [b]
+        self._page_info.test_positions[button_args.id] = b.pos_pixel()
+        return b
+
+    def _add_option_menu(self, option_args, cb):
+        o = DirectOptionMenuTestable(**option_args.to_p3d_args())
+        o.__class__ = type('DirectOptionMenuMixed', (DirectOptionMenu, DirectGuiMixin), {})
+        o._show_cb = cb
+        o.popupMenu['frameColor'] = option_args.to_p3d_args()['frameColor']
+        o.popupMenu['relief'] = option_args.to_p3d_args()['relief']
+        self.__widgets += [o]
+        self._page_info.test_positions[option_args.id] = o.pos_pixel()
+        return o
+
+    def _add_slider(self, slider_args):
+        s = DirectSlider(**slider_args.to_p3d_args())
+        s.__class__ = type('DirectSliderMixed', (DirectSlider, DirectGuiMixin), {})
+        self._page_info.test_positions['volume'] = s.pos_pixel()
+        np_left = GfxTools.build_empty_node('left_slider')
+        np_left.set_pos(s.get_net_transform().get_pos())
+        np_left.set_x(np_left.get_x() - s.get_scale()[0])
+        lpos = np_left.pos_as_widget()  # try with pos2d_pixel and remove pos_as_widget if ok
+        self.__widgets += [s]
+        self._page_info.test_positions['volume_0'] = lpos
+        return s
+
+    def _set_page(self, page_name):
+        self._page_info.set_page(page_name)
+        self.destroy()
+
+    def on_back(self):
+        self._set_page('main')
+
+    def _rearrange_buttons_width(self):
+        max_width = 0
+        for w in self.__widgets:
+            t = w.component('text0')
+            ul = t.textNode.get_upper_left_3d()
+            lr = t.textNode.get_lower_right_3d()
+            max_width = max(lr[0] - ul[0], max_width)
+        for w in self.__widgets:
+            m_w = max_width / 2 + .8
+            w['frameSize'] = -m_w, m_w, w['frameSize'][2], w['frameSize'][3]
+
+    def destroy(self):
+        for w in self.__widgets: w.destroy()
+        self._page_info = None
+        self.__widgets = []
diff --git a/ya2/utils/gui/cursor.py b/ya2/utils/gui/cursor.py
new file mode 100644 (file)
index 0000000..69d4e9a
--- /dev/null
@@ -0,0 +1,65 @@
+from dataclasses import dataclass
+from panda3d.core import WindowProperties, Texture
+from direct.gui.OnscreenImage import OnscreenImage
+from ya2.utils.logics import LogicsTools
+
+
+@dataclass
+class MouseCursorInfo:
+    image_path: ...
+    functional_test: ...
+    scale: ... = 1
+    color: ... = (1, 1, 1, 1)
+    hotspot_position: ... = (0, 0)
+
+
+class MouseCursor:
+
+    def __init__(self, info):
+        self.__functional_test = info.functional_test
+        if not info.image_path: return
+        self.__hide_system_cursor()
+        self.__set_image(info.image_path, info.scale, info.color)
+        self.__set_hotspot(info.hotspot_position, info.scale)
+        self._tsk = taskMgr.add(self.__on_frame, 'on_frame_cursor')
+
+    def __hide_system_cursor(self):
+        p = WindowProperties()
+        p.set_cursor_hidden(True)
+        if LogicsTools.windowed: base.win.request_properties(p)
+
+    def __set_image(self, image_path, scale, color):
+        self.__image = OnscreenImage(image_path, scale=scale)
+        self.__image.set_color(color)
+        self.__image.set_bin('gui-popup', 50)
+        alpha_formats = [Texture.FRgba]
+        if self.__image.get_texture().get_format() in alpha_formats:
+            self.__image.set_transparency(True)
+
+    def __set_hotspot(self, hotspot_position, scale):
+        self.__hotspot_dx = scale[0] * (1 - 2 * hotspot_position[0])
+        self.__hotspot_dy = scale[2] * (1 - 2 * hotspot_position[1])
+
+    def __on_frame(self, task):
+        m = base.mouseWatcherNode
+        if not m or not m.hasMouse(): return task.again
+        mouse = m.get_mouse_x(), m.get_mouse_y()
+        h_x = mouse[0] * base.get_aspect_ratio() + self.__hotspot_dx
+        self.__image.set_pos((h_x, 1, mouse[1] - self.__hotspot_dy))
+        return task.again
+
+    def set_image(self, image):
+        self.__image.set_texture(loader.load_texture(image), 1)
+
+    def set_color(self, color):
+        self.__image.set_color(color)
+
+    def show(self):
+        if not self.__functional_test: return self.__image.show()
+
+    def hide(self):
+        return self.__image.hide()
+
+    def destroy(self):
+        taskMgr.remove(self._tsk)
+        self.__image.destroy()
diff --git a/ya2/utils/gui/gui.py b/ya2/utils/gui/gui.py
new file mode 100644 (file)
index 0000000..ff58709
--- /dev/null
@@ -0,0 +1,29 @@
+from panda3d.core import load_prc_file_data, Texture, TextProperties, TextPropertiesManager
+from ya2.utils.gfx import Point
+
+class GuiTools:
+
+    @staticmethod
+    def init():
+        GuiTools.__create_text_properties_scale()
+
+    def __create_text_properties_scale():
+        (t := TextProperties()).set_text_scale(.64)
+        TextPropertiesManager.get_global_ptr().set_properties('scale', t)
+
+    @staticmethod
+    def no_window():
+        load_prc_file_data('', 'window-type none')
+
+    @staticmethod
+    def get_mouse():
+        return Point(base.mouseWatcherNode.get_mouse())
+
+    @staticmethod
+    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
diff --git a/ya2/utils/gui/menu.py b/ya2/utils/gui/menu.py
new file mode 100644 (file)
index 0000000..59cf84e
--- /dev/null
@@ -0,0 +1,27 @@
+from dataclasses import dataclass
+from ya2.utils.gui.cursor import MouseCursor
+
+
+@dataclass
+class PageInfo:
+    set_page: ...
+    test_positions: ...
+    running_functional_tests: ...
+
+
+class BaseMenu:
+
+    def __init__(self, cursor_info, running_functional_tests, test_positions):
+        self._page_info = PageInfo(self.set_page, test_positions, running_functional_tests)
+        self._page = None
+        self._cursor = MouseCursor(cursor_info)
+        self.set_page('main')
+
+    def set_page(self, page_name):
+        if self._page: self._page.destroy()
+        p = self._set_page(page_name)
+        self._page = p
+
+    def destroy(self):
+        if self._page: self._page = self._page.destroy()
+        self._cursor.destroy()