class MainMenu: """The main game menu. Includes starting a game, loading, saving and exiting GUI. """ def __init__(self): self._save_but = None self._chosen_crew = None self._load_msg = None self._load_screen = None self._is_first_pause = True self._save_blocked_lab = None self._menu_music = loader.loadSfx( # noqa: F821 "sounds/music/Among Madness - Fever.mp3") self._menu_music.setVolume(0.19) self.tactics_wids = [] self.save_wids = [] self.conf_wids = [] self.cred_wids = [] self._hover_snd = loader.loadSfx("sounds/GUI/menu1.ogg") # noqa: F821 self._hover_snd.setVolume(0.1) self.click_snd = loader.loadSfx("sounds/GUI/menu2.ogg") # noqa: F821 self.click_snd.setVolume(0.1) self.new_enemy_snd = loader.loadSfx( "sounds/new_enemy.ogg") # noqa: F821 self.new_enemy_snd.setPlayRate(0.95) self._main_fr = DirectFrame( frameSize=(-2, 2, -1, 1), frameColor=(0.15, 0.15, 0.15, 1), state=DGG.NORMAL, ) wids = self._show_authors_word() taskMgr.doMethodLater( # noqa: F821 5, self._hide_authors_word, "stop_splash_screens", extraArgs=[wids]) def _build(self): """Build the main menu.""" but_params = { "text_scale": 0.05, "relief": None, "parent": self._main_fr, "clickSound": self.click_snd, "text_font": base.main_font, # noqa: F821 } self._new_game_but = DirectButton( # New game pos=(-1.12, 0, 0.5), text_fg=RUST_COL, text=base.labels.MAIN_MENU[0], # noqa: F821 command=self._choose_tactics, text_align=TextNode.ALeft, **but_params, ) self.bind_button(self._new_game_but) is_save_exists = save_exists(1) or save_exists(2) or save_exists(3) self._load_but = DirectButton( # Load game pos=(-1.12, 0, 0.4), text_fg=RUST_COL if is_save_exists else (0.5, 0.5, 0.5, 1), text=base.labels.MAIN_MENU[1], # noqa: F821 command=self._show_slots if is_save_exists else None, extraArgs=[True], text_align=TextNode.ALeft, **but_params, ) self.bind_button(self._load_but) self.bind_button( DirectButton( # Options pos=(-1.12, 0, 0.2), text_fg=RUST_COL, text=base.labels.MAIN_MENU[2], # noqa: F821 text_align=TextNode.ALeft, command=self._show_conf, **but_params, )) self.bind_button( DirectButton( # Credits pos=(-1.12, 0, 0.1), text_fg=RUST_COL, text=base.labels.MAIN_MENU[30], # noqa: F821 command=self._show_credits, text_align=TextNode.ALeft, **but_params, )) self.bind_button( DirectButton( # Exit pos=(-1.12, 0, -0.1), text_fg=RUST_COL, text=base.labels.MAIN_MENU[3], # noqa: F821 command=sys.exit, extraArgs=[1], text_align=TextNode.ALeft, **but_params, )) self._alpha_disclaimer = DirectLabel( parent=self._main_fr, pos=(0, 0, -0.9), text_scale=0.03, text_fg=SILVER_COL, frameColor=(0, 0, 0, 0), text_font=base.main_font, # noqa: F821 text=base.labels.MAIN_MENU[4], # noqa: F821 ) def _check_can_save(self, task): """Check if the game can be saved right now. If the game can't be saved, the cause will be shown under the "Save game" button. """ if base.world.is_on_et: # noqa: F821 cause = base.labels.MAIN_MENU[16] # noqa: F821 elif base.world.is_in_city: # noqa: F821 cause = base.labels.MAIN_MENU[17] # noqa: F821 elif base.world.is_near_fork: # noqa: F821 cause = base.labels.MAIN_MENU[18] # noqa: F821 elif base.train.ctrl.critical_damage: # noqa: F821 cause = base.labels.MAIN_MENU[19] # noqa: F821 elif base.current_block.id < 4: # noqa: F821 cause = base.labels.MAIN_MENU[20] # noqa: F821 else: cause = None if cause: if self._save_blocked_lab is None: self._save_blocked_lab = DirectLabel( parent=self._main_fr, pos=(-1.12, 0, 0.26), frameColor=(0, 0, 0, 0), text_scale=0.026, text_fg=SILVER_COL, text=cause, text_font=base.main_font, # noqa: F821 text_align=TextNode.ALeft, ) self._save_blocked_lab["text"] = cause self._save_but["text_fg"] = SILVER_COL self._save_but["command"] = None clear_wids(self.save_wids) else: self._save_but["text_fg"] = RUST_COL self._save_but["command"] = self._show_slots if self._save_blocked_lab is not None: self._save_blocked_lab.destroy() self._save_blocked_lab = None return task.again def _choose_tactics(self): """Choose initial tactics before new game start.""" if self.tactics_wids: return clear_wids(self.save_wids) clear_wids(self.conf_wids) clear_wids(self.cred_wids) self.tactics_wids.append( DirectLabel( # Choose your crew parent=self._main_fr, text=base.labels.MAIN_MENU[5], # noqa: F821 text_fg=RUST_COL, text_scale=0.04, text_font=base.main_font, # noqa: F821 frameColor=(0, 0, 0, 0), pos=(0.7, 0, 0.75), )) # an image with a crew representer self._crew_preview = DirectFrame( parent=self._main_fr, frameSize=(-0.61, 0.61, -0.35, 0.35), pos=(0.7, 0, 0.3), ) self._crew_preview.setTransparency(TransparencyAttrib.MAlpha) self.tactics_wids.append(self._crew_preview) but_params = { "parent": self._main_fr, "text_scale": 0.035, "text_fg": RUST_COL, "text_font": base.main_font, # noqa: F821 "relief": None, "clickSound": self.click_snd, "command": self._show_crew, } self._crew_buts = { "soldiers": DirectButton( text=base.labels.MAIN_MENU[6], # noqa: F821 extraArgs=["soldiers"], pos=(0.5, 0, -0.15), **but_params, ), "raiders": DirectButton( text=base.labels.MAIN_MENU[7], # noqa: F821 extraArgs=["raiders"], pos=(0.7, 0, -0.15), **but_params, ), "anarchists": DirectButton( text=base.labels.MAIN_MENU[8], # noqa: F821 extraArgs=["anarchists"], pos=(0.925, 0, -0.15), **but_params, ), } self.tactics_wids += self._crew_buts.values() for but in self._crew_buts.values(): self.bind_button(but) self._team_description = DirectLabel( # Crew description parent=self._main_fr, text=base.labels.MAIN_MENU[9], # noqa: F821 text_fg=SILVER_COL, text_scale=0.03, text_font=base.main_font, # noqa: F821 frameColor=(0, 0, 0, 0), pos=(0.7, 0, -0.26), ) self.tactics_wids.append(self._team_description) start_but = DirectButton( # Start parent=self._main_fr, text_scale=0.045, text_fg=RUST_COL, text=base.labels.MAIN_MENU[10], # noqa: F821 text_font=base.main_font, # noqa: F821 relief=None, command=self._start_new_game, pos=(0.7, 0, -0.5), clickSound=self.click_snd, ) self.bind_button(start_but) self.tactics_wids.append(start_but) self._show_crew("soldiers") def _clear_temp_wids(self, task): """Destroy widgets from the first game screen.""" clear_wids(self.tactics_wids) self._alpha_disclaimer.destroy() return task.done def _dehighlight_but(self, button, _): """Dehighlight the button, when mouse pointer leaved it. Args: button (panda3d.gui.DirectGui.DirectButton): Button to dehighlight. """ if button["command"] is not None: button["text_scale"] = ( button["text_scale"][0] - 0.002, button["text_scale"][1] - 0.003, ) def _hide_authors_word(self, wids): """Hide author's word splashscreen and build the main menu. Args: wids (list): Widgets to destroy. """ for wid in wids: wid.destroy() self._menu_music.play() self._build() def _highlight_but(self, button, _): """Highlight the button pointed by mouse. Args: button (panda3d.gui.DirectGui.DirectButton): Button to highlight. """ if button["command"] is not None: button["text_scale"] = ( button["text_scale"][0] + 0.002, button["text_scale"][1] + 0.003, ) self._hover_snd.play() def _load_game(self, num): """Load previously saved game. Args: num (int): The slot number. """ save_file = "saves/save{}.dat".format(num) if not os.path.exists(save_file): return self.show_loading() taskMgr.doMethodLater( # noqa: F821 4, self._clear_temp_wids, "clear_main_menu_temp_wids") base.doMethodLater( # noqa: F821 0.25, base.load_game, "load_game", extraArgs=[num] # noqa: F821 ) def _read_saves(self): """Read all the game save slots. Returns: list: Dicts, each of which describes a save. """ saves = [] for num in range(1, 4): if save_exists(num): save = shelve.open("saves/save{}".format(str(num))) classes = [char["class"] for char in save["team"]] saves.append({ "save_time": save["save_time"], "miles": save["train"]["miles"], "chars": ( classes.count("soldier"), classes.count("raider"), classes.count("anarchist"), ), }) save.close() else: saves.append({}) return saves def _save_conf_and_restart( self, res_chooser, tutorial_check, lang_chooser, fps_chooser, fps_meter, multi_threading, ): """Save configurations and restart the game program. Args: res_chooser (GUI.widgets.ListChooser): Widget to choose a screen resolution. tutorial_check (direct.gui.DirectCheckButton): Tutorial enabling check button. lang_chooser (GUI.widgets.ListChooser): Widget to choose GUI language. fps_chooser (GUI.widgets.ListChooser): Framerate chooser. fps_meter (direct.gui.DirectCheckButton): FPS meter enabling check button. multi_threading (direct.gui.DirectCheckButton): Multi threading mode enabling check button. """ base.game_config.update( # noqa: F821 res_chooser.chosen_item, lang_chooser.chosen_item, str(bool(tutorial_check["indicatorValue"])), fps_chooser.chosen_item, str(bool(fps_meter["indicatorValue"])), str(bool(multi_threading["indicatorValue"])), ) base.restart_game() # noqa: F821 def _show_authors_word(self): """Show author's word splashscreen. Returns: list: Splash screen widgets. """ title = DirectLabel( pos=(0, 0, 0.2), text_scale=0.045, text_fg=SILVER_COL, frameColor=(0, 0, 0, 0), text=base.labels.SPLASHES[0], # noqa: F821 state=DGG.NORMAL, text_font=base.main_font, # noqa: F821 ) msg = DirectLabel( pos=(0, 0, 0.05), text_scale=0.035, text_fg=SILVER_COL, frameColor=(0, 0, 0, 0), text=base.labels.SPLASHES[1], # noqa: F821, state=DGG.NORMAL, text_font=base.main_font, # noqa: F821 ) return [title, msg] def _show_conf(self): """Show game configurations GUI.""" clear_wids(self.save_wids) clear_wids(self.tactics_wids) clear_wids(self.cred_wids) self.conf_wids.append( DirectLabel( # Resolution: parent=self._main_fr, text=base.labels.MAIN_MENU[22], # noqa: F821, text_fg=RUST_COL, text_scale=0.04, text_font=base.main_font, # noqa: F821 pos=(-0.3, 0, 0.5), frameColor=(0, 0, 0, 0), )) if base.game_config.resolution in RESOLUTIONS: # noqa: F821 res_ind = RESOLUTIONS.index( base.game_config.resolution) # noqa: F821 else: res_ind = len(RESOLUTIONS) RESOLUTIONS.append(base.game_config.resolution) # noqa: F821 res_chooser = ListChooser() res_chooser.prepare(self._main_fr, (0.1, 0, 0.51), RESOLUTIONS, res_ind) self.conf_wids.append(res_chooser) self.conf_wids.append( DirectLabel( # Tutorial: parent=self._main_fr, text=base.labels.MAIN_MENU[23], # noqa: F821, text_fg=RUST_COL, text_scale=0.04, text_font=base.main_font, # noqa: F821 pos=(-0.3, 0, 0.4), frameColor=(0, 0, 0, 0), )) tutorial_check = DirectCheckButton( parent=self._main_fr, indicatorValue=base.game_config.tutorial_enabled, # noqa: F821 clickSound=self.click_snd, scale=0.02, pos=(0.12, 0, 0.41), boxBorder=0, boxImage=(GUI_PIC + "no_check.png", GUI_PIC + "check.png", None), boxRelief=None, relief="flat", frameColor=RUST_COL, ) tutorial_check.setTransparency(TransparencyAttrib.MAlpha) self.conf_wids.append(tutorial_check) self.conf_wids.append( DirectLabel( # Language parent=self._main_fr, text=base.labels.MAIN_MENU[24], # noqa: F821, text_fg=RUST_COL, text_scale=0.04, text_font=base.main_font, # noqa: F821 pos=(-0.3, 0, 0.3), frameColor=(0, 0, 0, 0), )) lang_chooser = ListChooser() lang_chooser.prepare( self._main_fr, (0.1, 0, 0.31), LANGUAGES, LANGUAGES.index(base.game_config.language), # noqa: F821, ) self.conf_wids.append(lang_chooser) self.conf_wids.append( DirectLabel( # Framerate limit parent=self._main_fr, text=base.labels.MAIN_MENU[29], # noqa: F821, text_fg=RUST_COL, text_scale=0.04, text_font=base.main_font, # noqa: F821 pos=(-0.3, 0, 0.21), frameColor=(0, 0, 0, 0), )) fps_chooser = ListChooser() fps_chooser.prepare( self._main_fr, (0.1, 0, 0.22), FPS, FPS.index(str(base.game_config.fps_limit)), # noqa: F821 ) self.conf_wids.append(fps_chooser) self.conf_wids.append( DirectLabel( # FPS meter: parent=self._main_fr, text=base.labels.MAIN_MENU[36], # noqa: F821, text_fg=RUST_COL, text_scale=0.04, text_font=base.main_font, # noqa: F821 pos=(-0.3, 0, 0.11), frameColor=(0, 0, 0, 0), )) fps_meter = DirectCheckButton( parent=self._main_fr, indicatorValue=base.game_config.fps_meter, # noqa: F821 clickSound=self.click_snd, scale=0.02, pos=(0.12, 0, 0.12), boxBorder=0, boxImage=(GUI_PIC + "no_check.png", GUI_PIC + "check.png", None), boxRelief=None, relief="flat", frameColor=RUST_COL, ) fps_meter.setTransparency(TransparencyAttrib.MAlpha) self.conf_wids.append(fps_meter) self.conf_wids.append( DirectLabel( # Multi threading: parent=self._main_fr, text=base.labels.MAIN_MENU[37], # noqa: F821, text_fg=RUST_COL, text_scale=0.04, text_font=base.main_font, # noqa: F821 pos=(-0.3, 0, 0.01), frameColor=(0, 0, 0, 0), )) multi_threading = DirectCheckButton( parent=self._main_fr, indicatorValue=base.game_config.multi_threading, # noqa: F821 clickSound=self.click_snd, scale=0.02, pos=(0.12, 0, 0.02), boxBorder=0, boxImage=(GUI_PIC + "no_check.png", GUI_PIC + "check.png", None), boxRelief=None, relief="flat", frameColor=RUST_COL, ) multi_threading.setTransparency(TransparencyAttrib.MAlpha) self.conf_wids.append(multi_threading) self.conf_wids.append( DirectLabel( # Multi threading: parent=self._main_fr, text=base.labels.MAIN_MENU[38], # noqa: F821, text_fg=SILVER_COL, text_scale=0.025, text_font=base.main_font, # noqa: F821 pos=(-0.3, 0, -0.03), frameColor=(0, 0, 0, 0), )) but = DirectButton( # Save and restart parent=self._main_fr, text_scale=0.045, text_fg=RUST_COL, text=base.labels.MAIN_MENU[25], # noqa: F821, text_font=base.main_font, # noqa: F821 relief=None, command=self._save_conf_and_restart, extraArgs=[ res_chooser, tutorial_check, lang_chooser, fps_chooser, fps_meter, multi_threading, ], pos=(0.1, 0, -0.2), clickSound=self.click_snd, ) self.bind_button(but) self.conf_wids.append(but) def _show_credits(self): """Show the game credits.""" clear_wids(self.save_wids) clear_wids(self.conf_wids) clear_wids(self.tactics_wids) center = 0.25 self.cred_wids.append( DirectLabel( parent=self._main_fr, pos=(center, 0, 0.6), text_scale=0.04, text_fg=SILVER_COL, text_font=base.main_font, # noqa: F821 frameColor=(0, 0, 0, 0), text=base.labels.MAIN_MENU[31], # noqa: F821 )) self.cred_wids.append( DirectLabel( # Project source code parent=self._main_fr, pos=(center, 0, 0.5), text_scale=0.04, text_fg=SILVER_COL, text_font=base.main_font, # noqa: F821 frameColor=(0, 0, 0, 0), text=base.labels.MAIN_MENU[32], # noqa: F821 )) self.cred_wids.append( DirectButton( parent=self._main_fr, pos=(center, 0, 0.45), frameColor=(0, 0, 0, 0), text_scale=0.04, text_fg=RUST_COL, text="github.com/IlyaFaer/ForwardOnlyGame", text_font=base.main_font, # noqa: F821 relief=None, command=webbrowser.open, extraArgs=["https://github.com/IlyaFaer/ForwardOnlyGame"], clickSound=self.click_snd, )) self.cred_wids.append( DirectLabel( # Subscribe parent=self._main_fr, pos=(center, 0, 0.35), text_scale=0.04, text_fg=SILVER_COL, text_font=base.main_font, # noqa: F821 frameColor=(0, 0, 0, 0), text=base.labels.MAIN_MENU[33], # noqa: F821 )) self.cred_wids.append( DirectButton( parent=self._main_fr, pos=(0.11, 0, 0.29), frameTexture="credits/youtube.png", frameSize=(-0.056, 0.056, -0.029, 0.029), relief="flat", command=webbrowser.open, extraArgs=[ "https://www.youtube.com/channel/UCKmtk9K6VkcQdOMiE7H-W9w" ], clickSound=self.click_snd, )) self.cred_wids.append( DirectButton( parent=self._main_fr, pos=(center, 0, 0.29), frameTexture="credits/indie_db.png", frameSize=(-0.058, 0.058, -0.029, 0.029), relief="flat", command=webbrowser.open, extraArgs=["https://www.indiedb.com/games/forward-only"], clickSound=self.click_snd, )) but = DirectButton( parent=self._main_fr, pos=(0.38, 0, 0.29), frameTexture="credits/discord.png", frameSize=(-0.045, 0.045, -0.045, 0.045), relief="flat", command=webbrowser.open, extraArgs=["https://discord.gg/8UgFJAWsFx"], clickSound=self.click_snd, ) but.setTransparency(TransparencyAttrib.MAlpha) self.cred_wids.append(but) self.cred_wids.append( DirectLabel( # Stack parent=self._main_fr, pos=(center, 0, 0.18), text_scale=0.04, text_fg=SILVER_COL, text_font=base.main_font, # noqa: F821 frameColor=(0, 0, 0, 0), text=base.labels.MAIN_MENU[34], # noqa: F821 )) but = DirectButton( parent=self._main_fr, pos=(0.05, 0, 0.11), frameTexture="credits/python.png", frameSize=(-0.05, 0.05, -0.05, 0.05), relief="flat", command=webbrowser.open, extraArgs=["https://www.python.org/"], clickSound=self.click_snd, ) but.setTransparency(TransparencyAttrib.MAlpha) self.cred_wids.append(but) but = DirectButton( parent=self._main_fr, pos=(0.185, 0, 0.11), frameTexture="credits/panda3d.png", frameSize=(-0.05, 0.05, -0.05, 0.05), relief="flat", command=webbrowser.open, extraArgs=["https://www.panda3d.org/"], clickSound=self.click_snd, ) but.setTransparency(TransparencyAttrib.MAlpha) self.cred_wids.append(but) but = DirectButton( parent=self._main_fr, pos=(0.315, 0, 0.11), frameTexture="credits/blender.png", frameSize=(-0.05, 0.05, -0.05, 0.05), relief="flat", command=webbrowser.open, extraArgs=["https://www.blender.org/"], clickSound=self.click_snd, ) but.setTransparency(TransparencyAttrib.MAlpha) self.cred_wids.append(but) but = DirectButton( parent=self._main_fr, pos=(0.45, 0, 0.11), frameTexture="credits/make_human.png", frameSize=(-0.05, 0.05, -0.05, 0.05), relief="flat", command=webbrowser.open, extraArgs=["http://www.makehumancommunity.org/"], clickSound=self.click_snd, ) but.setTransparency(TransparencyAttrib.MAlpha) self.cred_wids.append(but) self.cred_wids.append( DirectLabel( # Tools parent=self._main_fr, pos=(center, 0, -0.02), text_scale=0.04, text_fg=SILVER_COL, text_font=base.main_font, # noqa: F821 frameColor=(0, 0, 0, 0), text=base.labels.MAIN_MENU[35], # noqa: F821 )) but = DirectButton( parent=self._main_fr, pos=(center - 0.12, 0, -0.09), frameTexture="credits/free_sound.png", frameSize=(-0.057, 0.057, -0.029, 0.029), relief="flat", command=webbrowser.open, extraArgs=["https://freesound.org/"], clickSound=self.click_snd, ) but.setTransparency(TransparencyAttrib.MAlpha) self.cred_wids.append(but) but = DirectButton( parent=self._main_fr, pos=(center, 0, -0.09), frameTexture="credits/photopea.png", frameSize=(-0.03, 0.03, -0.03, 0.03), relief="flat", command=webbrowser.open, extraArgs=["https://www.photopea.com/"], clickSound=self.click_snd, ) but.setTransparency(TransparencyAttrib.MAlpha) self.cred_wids.append(but) but = DirectButton( parent=self._main_fr, pos=(center + 0.09, 0, -0.09), frameTexture="credits/online_convert.png", frameSize=(-0.03, 0.03, -0.03, 0.03), relief="flat", command=webbrowser.open, extraArgs=["https://audio.online-convert.com/"], clickSound=self.click_snd, ) but.setTransparency(TransparencyAttrib.MAlpha) self.cred_wids.append(but) self.cred_wids.append( DirectLabel( # Music parent=self._main_fr, pos=(center, 0, -0.24), text_scale=0.042, text_fg=SILVER_COL, text_font=base.main_font, # noqa: F821 frameColor=(0, 0, 0, 0), text=base.labels.MAIN_MENU[39], # noqa: F821 )) but = DirectButton( parent=self._main_fr, pos=(-0.15, 0, -0.45), frameTexture="credits/among_madness_logo.png", frameSize=(-0.15, 0.15, -0.15, 0.15), relief="flat", command=webbrowser.open, extraArgs=[ "https://open.spotify.com/artist/3uy4tvaLvBAsKdV52Kc2TI" ], clickSound=self.click_snd, ) but.setTransparency(TransparencyAttrib.MAlpha) self.cred_wids.append(but) self.cred_wids.append( DirectLabel( parent=self._main_fr, pos=(-0.15, 0, -0.65), text_scale=0.033, text_fg=SILVER_COL, text_font=base.main_font, # noqa: F821 frameColor=(0, 0, 0, 0), text="Among Madness", )) but = DirectButton( parent=self._main_fr, pos=(0.25, 0, -0.45), frameTexture="credits/qualia_logo.png", frameSize=(-0.15, 0.15, -0.15, 0.15), relief="flat", command=webbrowser.open, extraArgs=["https://kvalia.net/"], clickSound=self.click_snd, ) but.setTransparency(TransparencyAttrib.MAlpha) self.cred_wids.append(but) self.cred_wids.append( DirectLabel( parent=self._main_fr, pos=(0.25, 0, -0.65), text_scale=0.033, text_fg=SILVER_COL, text_font=base.main_font, # noqa: F821 frameColor=(0, 0, 0, 0), text="Квалиа", )) but = DirectButton( parent=self._main_fr, pos=(0.65, 0, -0.45), frameTexture="credits/moloken_logo.png", frameSize=(-0.15, 0.15, -0.15, 0.15), relief="flat", command=webbrowser.open, extraArgs=[ "https://open.spotify.com/artist/3LZzdqKCEcBwhh6vd6y6Q5" ], clickSound=self.click_snd, ) but.setTransparency(TransparencyAttrib.MAlpha) self.cred_wids.append(but) self.cred_wids.append( DirectLabel( parent=self._main_fr, pos=(0.65, 0, -0.65), text_scale=0.033, text_fg=SILVER_COL, text_font=base.main_font, # noqa: F821 frameColor=(0, 0, 0, 0), text="Moloken", )) def _show_crew(self, crew): """Show the description of the chosen tactics. Args: crew (str): The chosen tactics name. """ self._chosen_crew = crew self._crew_preview["frameTexture"] = "gui/tex/preview/{}.png".format( crew) for key, but in self._crew_buts.items(): but["text_fg"] = SILVER_COL if key == crew else RUST_COL descs = { "soldiers": base.labels.MAIN_MENU[11], # noqa: F821 "raiders": base.labels.MAIN_MENU[12], # noqa: F821 "anarchists": base.labels.MAIN_MENU[13], # noqa: F821 } self._team_description["text"] = descs[crew] def _show_slots(self, for_loading=False): """Show save slots GUI. Args: for_loading (bool): Optional. True if slots must be shown for loading. """ if self.save_wids: return clear_wids(self.tactics_wids) clear_wids(self.conf_wids) clear_wids(self.cred_wids) if for_loading: is_active = True command = self._load_game else: is_active = not (base.train.ctrl.critical_damage # noqa: F821 or base.world.is_in_city # noqa: F821 or base.world.is_on_et # noqa: F821 or base.current_block.id < 4 # noqa: F821 or base.world.is_near_fork # noqa: F821 ) command = base.save_game # noqa: F821 saves = self._read_saves() z_shift = 0.5 x_shift = -0.2 for num in range(1, 4): but = DirectButton( parent=self._main_fr, text="Slot {}".format(str(num)), pos=(x_shift - 0.103, 0, z_shift), text_scale=0.04, text_fg=RUST_COL, relief=None, command=command if is_active else None, extraArgs=[num], clickSound=self.click_snd, ) self.bind_button(but) self.save_wids.append(but) self.save_wids.append( DirectLabel( parent=self._main_fr, pos=(x_shift, 0, z_shift - 0.05), text_scale=0.03, text_fg=SILVER_COL, frameColor=(0, 0, 0, 0), text=saves[num - 1].get("save_time", "-"), )) self.save_wids.append( DirectLabel( parent=self._main_fr, pos=(x_shift - 0.1, 0, z_shift - 0.1), text_scale=0.03, text_fg=SILVER_COL, frameColor=(0, 0, 0, 0), text=(str(saves[num - 1].get("miles") or "- ")) + " mi", )) self.save_wids.append( DirectLabel( parent=self._main_fr, pos=(x_shift + 0.35, 0, z_shift - 0.1), text_scale=0.03, text_fg=SILVER_COL, frameColor=(0, 0, 0, 0), text="{0} soldiers, {1} raiders, {2} anarchists".format( *saves[num - 1].get("chars", ("-", "-", "-"))), )) z_shift -= 0.2 def _start_new_game(self): """Start a new game.""" taskMgr.doMethodLater( # noqa: F821 4, self._clear_temp_wids, "clear_main_menu_temp_wids") self.show_loading(is_game_start=True) taskMgr.doMethodLater( # noqa: F821 0.25, base.start_new_game, # noqa: F821 "start_new_game", extraArgs=[self._chosen_crew], ) def bind_button(self, button): """Bind the given button to visual effects. Args: button (panda3d.gui.DirectGui.DirectButton): Button to bind. """ button.bind(DGG.ENTER, self._highlight_but, extraArgs=[button]) button.bind(DGG.EXIT, self._dehighlight_but, extraArgs=[button]) def hide(self): """Hide the main menu.""" self._main_fr.hide() clear_wids(self.save_wids) clear_wids(self.conf_wids) clear_wids(self.cred_wids) if self._load_screen is not None: self._load_screen.destroy() self._load_screen = None base.accept("escape", self.show) # noqa: F821 taskMgr.remove("check_can_save") # noqa: F821 def hide_loading_msg(self): """Hide the "Loading..." message widget.""" self._load_msg.destroy() def show_credits(self, task): """Show the game end credits.""" mus = loader.loadSfx("sounds/music/Moloken - 11''12.mp3") # noqa: F821 mus.setVolume(0.2) mus.play() goodness = 0 inserts = {} for key, value in base.decisions.items(): # noqa: F821 inserts[key] = value["decision"] goodness += value["goodness"] if goodness <= 15: inserts["leader_desc"] = base.labels.ROUGH_LEADER # noqa: F821 elif goodness <= 30: inserts[ "leader_desc"] = base.labels.OPPORTUNIST_LEADER # noqa: F821 else: inserts["leader_desc"] = base.labels.EMPATHIC_LEADER # noqa: F821 frame = DirectFrame( frameSize=(-2, 2, -1, 1), frameColor=(0.15, 0.15, 0.15, 1), state=DGG.NORMAL, ) lab = DirectLabel( parent=frame, text=base.labels.CREDITS.format(**inserts), # noqa: F821 text_font=base.main_font, # noqa: F821 text_scale=0.042, text_fg=SILVER_COL, frameColor=(0, 0, 0, 0), text_align=TextNode.ACenter, pos=(0, 0, -1.5), ) LerpPosInterval(lab, 100, (0, 0, 10)).start() return task.done def show_loading(self, is_game_start=False): """Show game loading screen. Args: is_game_start (bool): Flag, indicating if the player just started a new game. """ if is_game_start: self.hide() self._load_screen = DirectFrame( frameSize=(-2, 2, -1, 1), frameColor=(0.15, 0.15, 0.15, 1), state=DGG.NORMAL, ) DirectLabel( parent=self._load_screen, text=base.labels.SCENARIO_LABELS[2], # noqa: F821 text_font=base.main_font, # noqa: F821 text_scale=0.056, text_fg=RUST_COL, frameColor=(0, 0, 0, 0), text_align=TextNode.ALeft, pos=(-1, 0, 0.5), ) DirectLabel( parent=self._load_screen, text=base.labels.PREAMBULA, # noqa: F821 text_font=base.main_font, # noqa: F821 text_scale=0.033, text_fg=SILVER_COL, frameColor=(0, 0, 0, 0), text_align=TextNode.ALeft, pos=(-1, 0, 0.4), ) self._load_msg = DirectLabel( # Loading... parent=self._load_screen or self._main_fr, text=base.labels.MAIN_MENU[26], # noqa: F821, text_font=base.main_font, # noqa: F821 text_fg=RUST_COL, frameSize=(1, 1, 1, 1), text_scale=0.04, pos=(0, 0, -0.75), ) def show(self, is_game_over=False): """Show the main menu. Args: is_game_over (bool): True, if the main menu is shown on game over. """ if not getattr(base, "traits_gui", None): # noqa: F821 return if base.world.rails_scheme.is_shown: # noqa: F821 base.world.rails_scheme.show() # noqa: F821 if base.traits_gui.is_shown: # noqa: F821 base.traits_gui.hide() # noqa: F821 self._main_fr.show() if is_game_over: self._new_game_but["command"] = None self._new_game_but["text_fg"] = SILVER_COL DirectLabel( parent=self._main_fr, pos=(0.85, 0, 0.3), frameColor=(0, 0, 0, 0), text_scale=0.055, text_fg=SILVER_COL, text_font=base.main_font, # noqa: F821 text=base.labels.MAIN_MENU[28], # noqa: F821 ) base.ignore("escape") # noqa: F821 else: base.accept("escape", self.hide) # noqa: F821 taskMgr.doMethodLater( # noqa: F821 0.25, self._check_can_save, "check_can_save") if not self._is_first_pause: return # it is the first pause self._main_fr["frameColor"] = (0, 0, 0, 0.6) self._new_game_but["text"] = base.labels.MAIN_MENU[21] # noqa: F821 if not is_game_over: self._new_game_but["command"] = self.hide self._load_but.destroy() can_save = not (base.train.ctrl.critical_damage # noqa: F821 or base.world.is_in_city # noqa: F821 or base.world.is_on_et # noqa: F821 or base.current_block.id < 4 # noqa: F821 or base.world.is_near_fork # noqa: F821 ) self._save_but = DirectButton( parent=self._main_fr, pos=(-1.12, 0, 0.3), text_scale=0.05, text_fg=RUST_COL if can_save else SILVER_COL, text=base.labels.MAIN_MENU[15], # noqa: F821 text_font=base.main_font, # noqa: F821 relief=None, text_align=TextNode.ALeft, command=self._show_slots if can_save else None, # noqa: F821 clickSound=self.click_snd, ) self.bind_button(self._save_but) self.bind_button( DirectButton( # Main menu parent=self._main_fr, pos=(-1.12, 0, 0), text_scale=0.05, text_fg=RUST_COL, text=base.labels.MAIN_MENU[14], # noqa: F821 text_font=base.main_font, # noqa: F821 relief=None, command=base.restart_game, # noqa: F821 text_align=TextNode.ALeft, clickSound=self.click_snd, )) self._is_first_pause = False def show_start_button(self): """Show a button to start a game on the loading screen.""" parent = self._load_msg.getParent() self._load_msg.destroy() self._load_msg = DirectButton( parent=parent, text=base.labels.MAIN_MENU[27], # noqa: F821, text_font=base.main_font, # noqa: F821 text_fg=RUST_COL, text_scale=0.04, pos=(0, 0, -0.75), relief=None, clickSound=self.click_snd, command=base.start_game, # noqa: F821 ) self.bind_button(self._load_msg) def stop_music(self): """Stop the main menu music.""" taskMgr.doMethodLater( # noqa: F821 0.3, drown_snd, "drown_menu_music", extraArgs=[self._menu_music], appendTask=True, ) taskMgr.doMethodLater( # noqa: F821 3, loader.unloadSfx, # noqa: F821 "unload_menu_music", extraArgs=[self._menu_music], appendTask=False, )
def __init__(self, items, parent=None, sidePad=.0, edgePos=PTop, align=ALeft, effect=ENone, buttonThrower=None, font=None, baselineOffset=.0, scale=.05, itemHeight=1., leftPad=.0, separatorHeight=.5, underscoreThickness=1, BGColor=(0, 0, 0, .7), BGBorderColor=(1, .85, .4, 1), separatorColor=(1, 1, 1, 1), frameColorHover=(1, .85, .4, 1), frameColorPress=(0, 1, 0, 1), textColorReady=(1, 1, 1, 1), textColorHover=(0, 0, 0, 1), textColorPress=(0, 0, 0, 1), textColorDisabled=(.5, .5, .5, 1), draggable=False, onMove=None): ''' sidePad : additional space on the left and right of the text item edgePos : menu bar position on the screen, use DropDownMenu.PLeft, PRight, PBottom, or PTop align : menu items alignment on menu bar, use DropDownMenu.ALeft, ACenter, or ARight effect : the drop down appearance effect, use DropDownMenu.ENone, EFade, ESlide, or EStretch draggable : menu bar's draggability status onMove : a function which will be called after changing edge position Read the remaining options documentation in PopupMenu class. ''' self.parent = parent if parent else getattr( base, DropDownMenu.parents[edgePos][align]) self.BT = buttonThrower if buttonThrower else base.buttonThrowers[ 0].node() self.menu = self.parent.attachNewNode('dropdownmenu-%s' % id(self)) self.font = font if font else TextNode.getDefaultFont() self.baselineOffset = baselineOffset self.scale = scale self.itemHeight = itemHeight self.sidePad = sidePad self.edgePos = edgePos self.alignment = align self.effect = effect self.leftPad = leftPad self.underscoreThickness = underscoreThickness self.separatorHeight = separatorHeight self.BGColor = BGColor self.BGBorderColor = BGBorderColor self.separatorColor = separatorColor self.frameColorHover = frameColorHover self.frameColorPress = frameColorPress self.textColorReady = textColorReady self.textColorHover = textColorHover self.textColorPress = textColorPress self.textColorDisabled = textColorDisabled self.draggable = draggable self.onMove = onMove self.dropDownMenu = self.whoseDropDownMenu = None self.gapFromEdge = gapFromEdge = .008 texMargin = self.font.getTextureMargin() * self.scale * .25 b = DirectButton(parent=NodePath(''), text='^|g_', text_font=self.font, scale=self.scale) fr = b.node().getFrame() b.getParent().removeNode() baselineToCenter = (fr[2] + fr[3]) * self.scale LH = (fr[3] - fr[2]) * self.itemHeight * self.scale baselineToTop = (fr[3] * self.itemHeight * self.scale / LH) / (1. + self.baselineOffset) baselineToBot = LH / self.scale - baselineToTop self.height = LH + .01 l, r, b, t = 0, 5, -self.height, 0 self.menuBG = DirectFrame(parent=self.menu, frameColor=BGColor, frameSize=(l, r, b, t), state=DGG.NORMAL, suppressMouse=1) if self.draggable: self.setDraggable(1) LSborder = LineSegs() LSborder.setThickness(2) LSborder.setColor(0, 0, 0, 1) LSborder.moveTo(l, 0, b) LSborder.drawTo(r, 0, b) self.menuBG.attachNewNode(LSborder.create()) self.itemsParent = self.menu.attachNewNode('menu items parent') x = sidePad * self.scale + gapFromEdge for t, menuItemsGenerator in items: underlinePos = t.find('_') t = t.replace('_', '') b = DirectButton( parent=self.itemsParent, text=t, text_font=self.font, pad=(sidePad, 0), scale=self.scale, pos=(x, 0, -baselineToTop * self.scale - gapFromEdge), text_fg=textColorReady, # text color when mouse over text2_fg=textColorHover, # text color when pressed text1_fg=textColorPress, # framecolor when pressed frameColor=frameColorPress, command=self.__createMenu, extraArgs=[True, menuItemsGenerator], text_align=TextNode.ALeft, relief=DGG.FLAT, rolloverSound=0, clickSound=0) b['extraArgs'] += [b.getName()] b.stateNodePath[2].setColor( *frameColorHover) # framecolor when mouse over b.stateNodePath[0].setColor(0, 0, 0, 0) # framecolor when ready fr = b.node().getFrame() b['frameSize'] = (fr[0], fr[1], -baselineToBot, baselineToTop) self.accept(DGG.ENTER + b.guiId, self.__createMenu, [False, menuItemsGenerator, b.getName()]) if underlinePos > -1: tn = TextNode('') tn.setFont(self.font) tn.setText(t[:underlinePos + 1]) tnp = NodePath(tn.getInternalGeom()) underlineXend = tnp.getTightBounds()[1][0] tnp.removeNode() tn.setText(t[underlinePos]) tnp = NodePath(tn.getInternalGeom()) b3 = tnp.getTightBounds() underlineXstart = underlineXend - (b3[1] - b3[0])[0] tnp.removeNode() LSunder = LineSegs() LSunder.setThickness(underscoreThickness) LSunder.moveTo(underlineXstart + texMargin, 0, -.7 * baselineToBot) LSunder.drawTo(underlineXend - texMargin, 0, -.7 * baselineToBot) underline = b.stateNodePath[0].attachNewNode(LSunder.create()) underline.setColor(Vec4(*textColorReady), 1) underline.copyTo(b.stateNodePath[1], 10).setColor(Vec4(*textColorPress), 1) underline.copyTo(b.stateNodePath[2], 10).setColor(Vec4(*textColorHover), 1) self.accept('alt-' + t[underlinePos].lower(), self.__createMenu, [True, menuItemsGenerator, b.getName()]) x += (fr[1] - fr[0]) * self.scale self.width = x - 2 * gapFromEdge self.align(align) self.setEdgePos(edgePos) self.minZ = base.a2dBottom + self.height if edgePos == DropDownMenu.PBottom else None viewPlaneNode = PlaneNode('cut menu') viewPlaneNode.setPlane(Plane(Vec3(0, 0, -1), Point3(0, 0, -LH))) self.clipPlane = self.menuBG.attachNewNode(viewPlaneNode)
class ScrolledButtonsList(DirectObject): """ A class to display a list of selectable buttons. It is displayed using scrollable window (DirectScrolledFrame). """ def __init__(self, parent=None, frameSize=(.8,1.2), buttonTextColor=(1,1,1,1), font=None, itemScale=.045, itemTextScale=0.85, itemTextZ=0, command=None, contextMenu=None, autoFocus=0, colorChange=1, colorChangeDuration=1, newItemColor=globals.colors['guiblue1'], rolloverColor=globals.colors['guiyellow'], suppressMouseWheel=1, modifier='control'): self.mode = None self.focusButton=None self.command=command self.contextMenu=contextMenu self.autoFocus=autoFocus self.colorChange=colorChange self.colorChangeDuration=colorChangeDuration*.5 self.newItemColor=newItemColor self.rolloverColor=rolloverColor self.rightClickTextColors=(Vec4(0,1,0,1),Vec4(0,35,100,1)) self.font=font if font: self.fontHeight=font.getLineHeight() else: self.fontHeight=TextNode.getDefaultFont().getLineHeight() self.fontHeight*=1.2 # let's enlarge font height a little self.xtraSideSpace=.2*self.fontHeight self.itemTextScale=itemTextScale self.itemTextZ=itemTextZ self.buttonTextColor=buttonTextColor self.suppressMouseWheel=suppressMouseWheel self.modifier=modifier self.buttonsList=[] self.numItems=0 self.__eventReceivers={} # DirectScrolledFrame to hold items self.itemScale=itemScale self.itemVertSpacing=self.fontHeight*self.itemScale self.frameWidth,self.frameHeight=frameSize # I set canvas' Z size smaller than the frame to avoid the auto-generated vertical slider bar self.childrenFrame = DirectScrolledFrame( parent=parent,pos=(-self.frameWidth*.5,0,.5*self.frameHeight), relief=DGG.GROOVE, state=DGG.NORMAL, # to create a mouse watcher region frameSize=(0, self.frameWidth, -self.frameHeight, 0), frameColor=(0,0,0,.7), canvasSize=(0, 0, -self.frameHeight*.5, 0), borderWidth=(0.01,0.01), manageScrollBars=0, enableEdit=0, suppressMouse=0, sortOrder=1000 ) # the real canvas is "self.childrenFrame.getCanvas()", # but if the frame is hidden since the beginning, # no matter how I set the canvas Z pos, the transform would be resistant, # so just create a new node under the canvas to be my canvas self.canvas=self.childrenFrame.getCanvas().attachNewNode('myCanvas') # slider background SliderBG=DirectFrame( parent=self.childrenFrame,frameSize=(-.025,.025,-self.frameHeight,0), frameColor=(0,0,0,.7), pos=(-.03,0,0),enableEdit=0, suppressMouse=0) # slider thumb track sliderTrack = DirectFrame( parent=SliderBG, relief=DGG.FLAT, #state=DGG.NORMAL, frameColor=(1,1,1,.2), frameSize=(-.015,.015,-self.frameHeight+.01,-.01), enableEdit=0, suppressMouse=0) # page up self.pageUpRegion=DirectFrame( parent=SliderBG, relief=DGG.FLAT, state=DGG.NORMAL, frameColor=(1,.8,.2,.1), frameSize=(-.015,.015,0,0), enableEdit=0, suppressMouse=0) self.pageUpRegion.setAlphaScale(0) self.pageUpRegion.bind(DGG.B1PRESS,self.__startScrollPage,[-1]) self.pageUpRegion.bind(DGG.WITHIN,self.__continueScrollUp) self.pageUpRegion.bind(DGG.WITHOUT,self.__suspendScrollUp) # page down self.pageDnRegion=DirectFrame( parent=SliderBG, relief=DGG.FLAT, state=DGG.NORMAL, frameColor=(1,.8,.2,.1), frameSize=(-.015,.015,0,0), enableEdit=0, suppressMouse=0) self.pageDnRegion.setAlphaScale(0) self.pageDnRegion.bind(DGG.B1PRESS,self.__startScrollPage,[1]) self.pageDnRegion.bind(DGG.WITHIN,self.__continueScrollDn) self.pageDnRegion.bind(DGG.WITHOUT,self.__suspendScrollDn) self.pageUpDnSuspended=[0,0] # slider thumb self.vertSliderThumb=DirectButton(parent=SliderBG, relief=DGG.FLAT, frameColor=(1,1,1,.6), frameSize=(-.015,.015,0,0), enableEdit=0, suppressMouse=0, rolloverSound=None, clickSound=None) self.vertSliderThumb.bind(DGG.B1PRESS,self.__startdragSliderThumb) self.vertSliderThumb.bind(DGG.WITHIN,self.__enteringThumb) self.vertSliderThumb.bind(DGG.WITHOUT,self.__exitingThumb) self.oldPrefix=base.buttonThrowers[0].node().getPrefix() self.sliderThumbDragPrefix='draggingSliderThumb-' # GOD & I DAMN IT !!! # These things below don't work well if the canvas has a lot of buttons. # So I end up checking the mouse region every frame by myself using a continuous task. # self.accept(DGG.WITHIN+self.childrenFrame.guiId,self.__enteringFrame) # self.accept(DGG.WITHOUT+self.childrenFrame.guiId,self.__exitingFrame) self.isMouseInRegion=False self.mouseOutInRegionCommand=(self.__exitingFrame,self.__enteringFrame) taskMgr.doMethodLater(.2,self.__getFrameRegion,'getFrameRegion') def __getFrameRegion(self,t): for g in range(base.mouseWatcherNode.getNumGroups()): region=base.mouseWatcherNode.getGroup(g).findRegion(self.childrenFrame.guiId) if region!=None: self.frameRegion=region taskMgr.add(self.__mouseInRegionCheck,'mouseInRegionCheck') break def __mouseInRegionCheck(self,t): """ check if the mouse is within or without the scrollable frame, and upon within or without, run the provided command """ if not base.mouseWatcherNode.hasMouse(): return Task.cont m=base.mouseWatcherNode.getMouse() bounds=self.frameRegion.getFrame() inRegion=bounds[0]<m[0]<bounds[1] and bounds[2]<m[1]<bounds[3] if self.isMouseInRegion==inRegion: return Task.cont self.isMouseInRegion=inRegion self.mouseOutInRegionCommand[inRegion]() return Task.cont def __startdragSliderThumb(self,m=None): if self.mode != None: if hasattr(self.mode, 'enableMouseCamControl') == 1: if self.mode.enableMouseCamControl == 1: self.mode.game.app.disableMouseCamControl() mpos=base.mouseWatcherNode.getMouse() parentZ=self.vertSliderThumb.getParent().getZ(render2d) sliderDragTask=taskMgr.add(self.__dragSliderThumb,'dragSliderThumb') sliderDragTask.ZposNoffset=mpos[1]-self.vertSliderThumb.getZ(render2d)+parentZ # sliderDragTask.mouseX=base.winList[0].getPointer(0).getX() self.oldPrefix=base.buttonThrowers[0].node().getPrefix() base.buttonThrowers[0].node().setPrefix(self.sliderThumbDragPrefix) self.acceptOnce(self.sliderThumbDragPrefix+'mouse1-up',self.__stopdragSliderThumb) def __dragSliderThumb(self,t): if not base.mouseWatcherNode.hasMouse(): return mpos=base.mouseWatcherNode.getMouse() # newY=base.winList[0].getPointer(0).getY() self.__updateCanvasZpos((t.ZposNoffset-mpos[1])/self.canvasRatio) # base.winList[0].movePointer(0, t.mouseX, newY) return Task.cont def __stopdragSliderThumb(self,m=None): if self.mode != None: if hasattr(self.mode, 'enableMouseCamControl') == 1: if self.mode.enableMouseCamControl == 1: self.mode.game.app.enableMouseCamControl() taskMgr.remove('dragSliderThumb') self.__stopScrollPage() base.buttonThrowers[0].node().setPrefix(self.oldPrefix) if self.isMouseInRegion: self.mouseOutInRegionCommand[self.isMouseInRegion]() def __startScrollPage(self,dir,m): self.oldPrefix=base.buttonThrowers[0].node().getPrefix() base.buttonThrowers[0].node().setPrefix(self.sliderThumbDragPrefix) self.acceptOnce(self.sliderThumbDragPrefix+'mouse1-up',self.__stopdragSliderThumb) t=taskMgr.add(self.__scrollPage,'scrollPage',extraArgs=[int((dir+1)*.5),dir*.01/self.canvasRatio]) self.pageUpDnSuspended=[0,0] def __scrollPage(self,dir,scroll): if not self.pageUpDnSuspended[dir]: self.__scrollCanvas(scroll) return Task.cont def __stopScrollPage(self,m=None): taskMgr.remove('scrollPage') def __suspendScrollUp(self,m=None): self.pageUpRegion.setAlphaScale(0) self.pageUpDnSuspended[0]=1 def __continueScrollUp(self,m=None): if taskMgr.hasTaskNamed('dragSliderThumb'): return self.pageUpRegion.setAlphaScale(1) self.pageUpDnSuspended[0]=0 def __suspendScrollDn(self,m=None): self.pageDnRegion.setAlphaScale(0) self.pageUpDnSuspended[1]=1 def __continueScrollDn(self,m=None): if taskMgr.hasTaskNamed('dragSliderThumb'): return self.pageDnRegion.setAlphaScale(1) self.pageUpDnSuspended[1]=0 def __suspendScrollPage(self,m=None): self.__suspendScrollUp() self.__suspendScrollDn() def __enteringThumb(self,m=None): self.vertSliderThumb['frameColor']=(1,1,1,1) self.__suspendScrollPage() def __exitingThumb(self,m=None): self.vertSliderThumb['frameColor']=(1,1,1,.6) def __scrollCanvas(self,scroll): if self.vertSliderThumb.isHidden() or self.buttonsList == []: return self.__updateCanvasZpos(self.canvas.getZ()+scroll) def __updateCanvasZpos(self,Zpos): newZ=clampScalar(Zpos, .0, self.canvasLen-self.frameHeight+.015) self.canvas.setZ(newZ) thumbZ=-newZ*self.canvasRatio self.vertSliderThumb.setZ(thumbZ) self.pageUpRegion['frameSize']=(-.015,.015,thumbZ-.01,-.01) self.pageDnRegion['frameSize']=(-.015,.015,-self.frameHeight+.01,thumbZ+self.vertSliderThumb['frameSize'][2]) def __adjustCanvasLength(self,numItem): self.canvasLen=float(numItem)*self.itemVertSpacing self.canvasRatio=(self.frameHeight-.015)/(self.canvasLen+.01) if self.canvasLen<=self.frameHeight-.015: canvasZ=.0 self.vertSliderThumb.hide() self.pageUpRegion.hide() self.pageDnRegion.hide() self.canvasLen=self.frameHeight-.015 else: canvasZ=self.canvas.getZ() self.vertSliderThumb.show() self.pageUpRegion.show() self.pageDnRegion.show() self.__updateCanvasZpos(canvasZ) self.vertSliderThumb['frameSize']=(-.015,.015,-self.frameHeight*self.canvasRatio,-.01) thumbZ=self.vertSliderThumb.getZ() self.pageUpRegion['frameSize']=(-.015,.015,thumbZ-.01,-.01) self.pageDnRegion['frameSize']=(-.015,.015,-self.frameHeight+.01,thumbZ+self.vertSliderThumb['frameSize'][2]) def __acceptAndIgnoreWorldEvent(self,event,command,extraArgs=[]): receivers=messenger.whoAccepts(event) if receivers is None: self.__eventReceivers[event]={} else: self.__eventReceivers[event]=receivers.copy() for r in self.__eventReceivers[event].keys(): if type(r) != types.TupleType: r.ignore(event) self.accept(event,command,extraArgs) def __ignoreAndReAcceptWorldEvent(self,events): for event in events: self.ignore(event) if self.__eventReceivers.has_key(event): for r, method_xtraArgs_persist in self.__eventReceivers[event].items(): if type(r) != types.TupleType: messenger.accept(event,r,*method_xtraArgs_persist) self.__eventReceivers[event]={} def __enteringFrame(self,m=None): # sometimes the WITHOUT event for page down region doesn't fired, # so directly suspend the page scrolling here self.__suspendScrollPage() BTprefix=base.buttonThrowers[0].node().getPrefix() if BTprefix==self.sliderThumbDragPrefix: return self.inOutBTprefix=BTprefix if self.suppressMouseWheel: self.__acceptAndIgnoreWorldEvent(self.inOutBTprefix+'wheel_up', command=self.__scrollCanvas, extraArgs=[-.07]) self.__acceptAndIgnoreWorldEvent(self.inOutBTprefix+'wheel_down', command=self.__scrollCanvas, extraArgs=[.07]) else: self.accept(self.inOutBTprefix+self.modifier+'-wheel_up',self.__scrollCanvas, [-.07]) self.accept(self.inOutBTprefix+self.modifier+'-wheel_down',self.__scrollCanvas, [.07]) def __exitingFrame(self,m=None): if not hasattr(self,'inOutBTprefix'): return if self.suppressMouseWheel: self.__ignoreAndReAcceptWorldEvent( ( self.inOutBTprefix+'wheel_up', self.inOutBTprefix+'wheel_down', ) ) else: self.ignore(self.inOutBTprefix+self.modifier+'-wheel_up') self.ignore(self.inOutBTprefix+self.modifier+'-wheel_down') def __setFocusButton(self,button,item): if self.focusButton: self.restoreNodeButton2Normal() self.focusButton=button self.highlightNodeButton() if callable(self.command) and button in self.buttonsList: # run user command and pass the selected item, it's index, and the button self.command(item,self.buttonsList.index(button),button) def __rightPressed(self,button,m): self.__isRightIn=True # text0 : normal # text1 : pressed # text2 : rollover # text3 : disabled button._DirectGuiBase__componentInfo['text2'][0].setColorScale(self.rightClickTextColors[self.focusButton==button]) button.bind(DGG.B3RELEASE,self.__rightReleased,[button]) button.bind(DGG.WITHIN,self.__rightIn,[button]) button.bind(DGG.WITHOUT,self.__rightOut,[button]) def __rightIn(self,button,m): self.__isRightIn=True button._DirectGuiBase__componentInfo['text2'][0].setColorScale(self.rightClickTextColors[self.focusButton==button]) def __rightOut(self,button,m): self.__isRightIn=False button._DirectGuiBase__componentInfo['text2'][0].setColorScale(Vec4(1,1,1,1)) def __rightReleased(self,button,m): button.unbind(DGG.B3RELEASE) button.unbind(DGG.WITHIN) button.unbind(DGG.WITHOUT) button._DirectGuiBase__componentInfo['text2'][0].setColorScale(self.rolloverColor) if not self.__isRightIn: return if callable(self.contextMenu): # run user command and pass the selected item, it's index, and the button self.contextMenu(button['extraArgs'][1],self.buttonsList.index(button),button) def scrollToBottom(self): ##for i in range(0,self.numItems): self.__scrollCanvas(1) def selectButton(self, button, item): self.__setFocusButton(button, item) def restoreNodeButton2Normal(self): """ stop highlighting item """ if self.focusButton != None: #self.focusButton['text_fg']=(1,1,1,1) self.focusButton['frameColor']=(0,0,0,0) def highlightNodeButton(self,idx=None): """ highlight the item """ if idx is not None: self.focusButton=self.buttonsList[idx] #self.focusButton['text_fg']=(.01,.01,.01,1) # nice dark blue. don't mess with the text fg color though! we want it custom self.focusButton['frameColor']=(0,.3,.8,1) def clear(self): """ clear the list """ for c in self.buttonsList: c.remove() self.buttonsList=[] self.focusButton=None self.numItems=0 def addItem(self,text,extraArgs=None,atIndex=None,textColorName=None): """ add item to the list text : text for the button extraArgs : the object which will be passed to user command(s) (both command and contextMenu) when the button get clicked atIndex : where to add the item <None> : put item at the end of list <integer> : put item at index <integer> <button> : put item at <button>'s index textColorName : the color name eg. 'yellow' """ textColor = self.buttonTextColor if textColorName != None: textColor = globals.colors[textColorName] button = DirectButton(parent=self.canvas, scale=self.itemScale, relief=DGG.FLAT, frameColor=(0,0,0,0),text_scale=self.itemTextScale, text=text, text_pos=(0,self.itemTextZ),text_fg=textColor, text_font=self.font, text_align=TextNode.ALeft, command=self.__setFocusButton, enableEdit=0, suppressMouse=0, rolloverSound=None,clickSound=None) #button.setMyMode(self.mode) l,r,b,t=button.getBounds() # top & bottom are blindly set without knowing where exactly the baseline is, # but this ratio fits most fonts baseline=-self.fontHeight*.25 #button['saved_color'] = textColor button['frameSize']=(l-self.xtraSideSpace,r+self.xtraSideSpace,baseline,baseline+self.fontHeight) # Zc=NodePath(button).getBounds().getCenter()[1]-self.fontHeight*.5+.25 # # Zc=button.getCenter()[1]-self.fontHeight*.5+.25 # button['frameSize']=(l-self.xtraSideSpace,r+self.xtraSideSpace,Zc,Zc+self.fontHeight) button['extraArgs']=[button,extraArgs] button._DirectGuiBase__componentInfo['text2'][0].setColorScale(self.rolloverColor) button.bind(DGG.B3PRESS,self.__rightPressed,[button]) if isinstance(atIndex,DirectButton): if atIndex.isEmpty(): atIndex=None else: index=self.buttonsList.index(atIndex) self.buttonsList.insert(index,button) if atIndex==None: self.buttonsList.append(button) index=self.numItems elif type(atIndex)==IntType: index=atIndex self.buttonsList.insert(index,button) Zpos=(-.7-index)*self.itemVertSpacing button.setPos(.02,0,Zpos) if index!=self.numItems: for i in range(index+1,self.numItems+1): self.buttonsList[i].setZ(self.buttonsList[i],-self.fontHeight) self.numItems+=1 self.__adjustCanvasLength(self.numItems) if self.autoFocus: self.focusViewOnItem(index) if self.colorChange: Sequence( button.colorScaleInterval(self.colorChangeDuration,self.newItemColor,globals.colors['guiblue3']), button.colorScaleInterval(self.colorChangeDuration,Vec4(1,1,1,1),self.newItemColor) ).start() def focusViewOnItem(self,idx): """ Scroll the window so the newly added item will be displayed in the middle of the window, if possible. """ Zpos=(idx+.7)*self.itemVertSpacing-self.frameHeight*.5 self.__updateCanvasZpos(Zpos) def setAutoFocus(self,b): """ set auto-view-focus state of newly added item """ self.autoFocus=b def index(self,button): """ get the index of button """ if not button in self.buttonsList: return None return self.buttonsList.index(button) def getNumItems(self): """ get the current number of items on the list """ return self.numItems def disableItem(self,i): if not 0<=i<self.numItems: print 'DISABLING : invalid index (%s)' %i return self.buttonsList[i]['state']=DGG.DISABLED self.buttonsList[i].setColorScale(.3,.3,.3,1) def enableItem(self,i): if not 0<=i<self.numItems: print 'ENABLING : invalid index (%s)' %i return self.buttonsList[i]['state']=DGG.NORMAL self.buttonsList[i].setColorScale(1,1,1,1) def removeItem(self,index): if not 0<=index<self.numItems: print 'REMOVAL : invalid index (%s)' %index return if self.numItems==0: return if self.focusButton==self.buttonsList[index]: self.focusButton=None self.buttonsList[index].removeNode() del self.buttonsList[index] self.numItems-=1 for i in range(index,self.numItems): self.buttonsList[i].setZ(self.buttonsList[i],self.fontHeight) self.__adjustCanvasLength(self.numItems) def destroy(self): self.clear() self.__exitingFrame() self.ignoreAll() self.childrenFrame.removeNode() taskMgr.remove('mouseInRegionCheck') def hide(self): self.childrenFrame.hide() self.isMouseInRegion=False self.__exitingFrame() taskMgr.remove('mouseInRegionCheck') def show(self): self.childrenFrame.show() if not hasattr(self,'frameRegion'): taskMgr.doMethodLater(.2,self.__getFrameRegion,'getFrameRegion') elif not taskMgr.hasTaskNamed('mouseInRegionCheck'): taskMgr.add(self.__mouseInRegionCheck,'mouseInRegionCheck') def toggleVisibility(self): if self.childrenFrame.isHidden(): self.show() else: self.hide() def setMyMode(self, myMode): self.mode = myMode
def __init__(self, items, parent=None, buttonThrower=None, onDestroy=None, font=None, baselineOffset=.0, scale=.05, itemHeight=1., leftPad=.0, separatorHeight=.5, underscoreThickness=1, BGColor=(0, 0, 0, .7), BGBorderColor=(1, .85, .4, 1), separatorColor=(1, 1, 1, 1), frameColorHover=(1, .85, .4, 1), frameColorPress=(0, 1, 0, 1), textColorReady=(1, 1, 1, 1), textColorHover=(0, 0, 0, 1), textColorPress=(0, 0, 0, 1), textColorDisabled=(.5, .5, .5, 1), minZ=None, useMouseZ=True): ''' items : a collection of menu items Item format : ( 'Item text', 'path/to/image', command ) OR ( 'Item text', 'path/to/image', command, arg1,arg2,.... ) If you don't want to use an image, pass 0. To create disabled item, pass 0 for the command : ( 'Item text', 'path/to/image', 0 ) so, you can easily switch between enabled or disabled : ( 'Item text', 'path/to/image', command if commandEnabled else 0 ) OR ( 'Item text', 'path/to/image', (0,command)[commandEnabled] ) To create submenu, pass a sequence of submenu items for the command. To create disabled submenu, pass an empty sequence for the command. To enable hotkey, insert an underscore before the character, e.g. hotkey of 'Item te_xt' is 'x' key. To add shortcut key text at the right side of the item, append it at the end of the item text, separated by "more than" sign, e.g. 'Item text>Ctrl-T'. To insert separator line, pass 0 for the whole item. parent : where to attach the menu, defaults to aspect2d buttonThrower : button thrower whose thrown events are blocked temporarily when the menu is displayed. If not given, the default button thrower is used onDestroy : user function which will be called after the menu is fully destroyed font : text font baselineOffset : text's baseline Z offset scale : text scale itemHeight : spacing between items, defaults to 1 leftPad : blank space width before text separatorHeight : separator line height, relative to itemHeight underscoreThickness : underscore line thickness BGColor, BGBorderColor, separatorColor, frameColorHover, frameColorPress, textColorReady, textColorHover, textColorPress, textColorDisabled are some of the menu components' color minZ : minimum Z position to restrain menu's bottom from going offscreen (-1..1). If it's None, it will be set a little above the screen's bottom. ''' self.parent = parent if parent else aspect2d self.onDestroy = onDestroy self.BT = buttonThrower if buttonThrower else base.buttonThrowers[ 0].node() self.menu = NodePath('menu-%s' % id(self)) self.parentMenu = None self.submenu = None self.BTprefix = self.menu.getName() + '>' self.submenuCreationTaskName = 'createSubMenu-' + self.BTprefix self.submenuRemovalTaskName = 'removeSubMenu-' + self.BTprefix self.font = font if font else TextNode.getDefaultFont() self.baselineOffset = baselineOffset self.scale = scale self.itemHeight = itemHeight self.leftPad = leftPad self.separatorHeight = separatorHeight self.underscoreThickness = underscoreThickness self.BGColor = BGColor self.BGBorderColor = BGBorderColor self.separatorColor = separatorColor self.frameColorHover = frameColorHover self.frameColorPress = frameColorPress self.textColorReady = textColorReady self.textColorHover = textColorHover self.textColorPress = textColorPress self.textColorDisabled = textColorDisabled self.minZ = minZ self.mpos = Point2(base.mouseWatcherNode.getMouse()) self.itemCommand = [] self.hotkeys = {} self.numItems = 0 self.sel = -1 self.selByKey = False bgPad = self.bgPad = .0125 texMargin = self.font.getTextureMargin() * self.scale * .25 b = DirectButton(parent=NodePath(''), text='^|g_', text_font=self.font, scale=self.scale) fr = b.node().getFrame() b.getParent().removeNode() baselineToCenter = (fr[2] + fr[3]) * self.scale LH = (fr[3] - fr[2]) * self.itemHeight * self.scale imageHalfHeight = .5 * (fr[3] - fr[2]) * self.itemHeight * .85 arrowHalfHeight = .5 * (fr[3] - fr[2]) * self.itemHeight * .5 baselineToTop = (fr[3] * self.itemHeight * self.scale / LH) / (1. + self.baselineOffset) baselineToBot = LH / self.scale - baselineToTop itemZcenter = (baselineToTop - baselineToBot) * .5 separatorHalfHeight = .5 * separatorHeight * LH LSseparator = LineSegs() LSseparator.setColor(.5, .5, .5, .2) arrowVtx = [ (0, itemZcenter), (-2 * arrowHalfHeight, itemZcenter + arrowHalfHeight), (-arrowHalfHeight, itemZcenter), (-2 * arrowHalfHeight, itemZcenter - arrowHalfHeight), ] tri = Triangulator() vdata = GeomVertexData('trig', GeomVertexFormat.getV3(), Geom.UHStatic) vwriter = GeomVertexWriter(vdata, 'vertex') for x, z in arrowVtx: vi = tri.addVertex(x, z) vwriter.addData3f(x, 0, z) tri.addPolygonVertex(vi) tri.triangulate() prim = GeomTriangles(Geom.UHStatic) for i in range(tri.getNumTriangles()): prim.addVertices(tri.getTriangleV0(i), tri.getTriangleV1(i), tri.getTriangleV2(i)) prim.closePrimitive() geom = Geom(vdata) geom.addPrimitive(prim) geomNode = GeomNode('arrow') geomNode.addGeom(geom) realArrow = NodePath(geomNode) z = -baselineToTop * self.scale - bgPad maxWidth = .1 / self.scale shortcutTextMaxWidth = 0 anyImage = False anyArrow = False anyShortcut = False arrows = [] shortcutTexts = [] loadPrcFileData('', 'text-flatten 0') for item in items: if item: t, imgPath, f = item[:3] haveSubmenu = type(f) in SEQUENCE_TYPES anyArrow |= haveSubmenu anyImage |= bool(imgPath) disabled = not len(f) if haveSubmenu else not callable(f) args = item[3:] underlinePos = t.find('_') t = t.replace('_', '') shortcutSepPos = t.find('>') if shortcutSepPos > -1: if haveSubmenu: print( "\nA SHORTCUT KEY POINTING TO A SUBMENU IS NON-SENSE, DON'T YOU AGREE ?" ) else: shortcutText = NodePath( OnscreenText( parent=self.menu, text=t[shortcutSepPos + 1:], font=self.font, scale=1, fg=(1, 1, 1, 1), align=TextNode.ARight, )) shortcutTextMaxWidth = max( shortcutTextMaxWidth, abs(shortcutText.getTightBounds()[0][0])) anyShortcut = True t = t[:shortcutSepPos] else: shortcutText = '' EoLcount = t.count('\n') arrowZpos = -self.font.getLineHeight() * EoLcount * .5 if disabled: b = NodePath( OnscreenText( parent=self.menu, text=t, font=self.font, scale=1, fg=textColorDisabled, align=TextNode.ALeft, )) # don't pass the scale and position to OnscreenText constructor, # to maintain correctness between the OnscreenText and DirectButton items # due to the new text generation implementation b.setScale(self.scale) b.setZ(z) maxWidth = max(maxWidth, b.getTightBounds()[1][0] / self.scale) if shortcutText: shortcutText.reparentTo(b) shortcutText.setColor(Vec4(*textColorDisabled), 1) shortcutText.setZ(arrowZpos) shortcutTexts.append(shortcutText) else: b = DirectButton( parent=self.menu, text=t, text_font=self.font, scale=self.scale, pos=(0, 0, z), text_fg=textColorReady, # text color when mouse over text2_fg=textColorHover, # text color when pressed text1_fg=textColorHover if haveSubmenu else textColorPress, # framecolor when pressed frameColor=frameColorHover if haveSubmenu else frameColorPress, command=(lambda: 0) if haveSubmenu else self.__runCommand, extraArgs=[] if haveSubmenu else [f, args], text_align=TextNode.ALeft, relief=DGG.FLAT, rolloverSound=0, clickSound=0, pressEffect=0) b.stateNodePath[2].setColor( *frameColorHover) # framecolor when mouse over b.stateNodePath[0].setColor(0, 0, 0, 0) # framecolor when ready bframe = Vec4(b.node().getFrame()) if EoLcount: bframe.setZ(EoLcount * 10) b['frameSize'] = bframe maxWidth = max(maxWidth, bframe[1]) if shortcutText: for snpi, col in ((0, textColorReady), (1, textColorPress), (2, textColorHover)): sct = shortcutText.copyTo(b.stateNodePath[snpi], sort=10) sct.setColor(Vec4(*col), 1) sct.setZ(arrowZpos) shortcutTexts.append(sct) shortcutText.removeNode() if imgPath: img = loader.loadTexture(imgPath) if disabled: if imgPath in PopupMenu.grayImages: img = PopupMenu.grayImages[imgPath] else: pnm = PNMImage() img.store(pnm) pnm.makeGrayscale(.2, .2, .2) img = Texture() img.load(pnm) PopupMenu.grayImages[imgPath] = img img.setMinfilter(Texture.FTLinearMipmapLinear) img.setWrapU(Texture.WMClamp) img.setWrapV(Texture.WMClamp) CM = CardMaker('') CM.setFrame(-2 * imageHalfHeight - leftPad, -leftPad, itemZcenter - imageHalfHeight, itemZcenter + imageHalfHeight) imgCard = b.attachNewNode(CM.generate()) imgCard.setTexture(img) if underlinePos > -1: oneLineText = t[:underlinePos + 1] oneLineText = oneLineText[oneLineText.rfind('\n') + 1:] tn = TextNode('') tn.setFont(self.font) tn.setText(oneLineText) tnp = NodePath(tn.getInternalGeom()) underlineXend = tnp.getTightBounds()[1][0] tnp.removeNode() tn.setText(t[underlinePos]) tnp = NodePath(tn.getInternalGeom()) b3 = tnp.getTightBounds() underlineXstart = underlineXend - (b3[1] - b3[0])[0] tnp.removeNode() underlineZpos = -.7 * baselineToBot - self.font.getLineHeight( ) * t[:underlinePos].count('\n') LSunder = LineSegs() LSunder.setThickness(underscoreThickness) LSunder.moveTo(underlineXstart + texMargin, 0, underlineZpos) LSunder.drawTo(underlineXend - texMargin, 0, underlineZpos) if disabled: underline = b.attachNewNode(LSunder.create()) underline.setColor(Vec4(*textColorDisabled), 1) else: underline = b.stateNodePath[0].attachNewNode( LSunder.create()) underline.setColor(Vec4(*textColorReady), 1) underline.copyTo(b.stateNodePath[1], 10).setColor( Vec4(*textColorHover if haveSubmenu else textColorPress), 1) underline.copyTo(b.stateNodePath[2], 10).setColor(Vec4(*textColorHover), 1) hotkey = t[underlinePos].lower() if hotkey in self.hotkeys: self.hotkeys[hotkey].append(self.numItems) else: self.hotkeys[hotkey] = [self.numItems] self.accept(self.BTprefix + hotkey, self.__processHotkey, [hotkey]) self.accept(self.BTprefix + 'alt-' + hotkey, self.__processHotkey, [hotkey]) if haveSubmenu: if disabled: arrow = realArrow.instanceUnderNode(b, '') arrow.setColor(Vec4(*textColorDisabled), 1) arrow.setZ(arrowZpos) else: arrow = realArrow.instanceUnderNode( b.stateNodePath[0], 'r') arrow.setColor(Vec4(*textColorReady), 1) arrow.setZ(arrowZpos) arrPress = realArrow.instanceUnderNode( b.stateNodePath[1], 'p') arrPress.setColor(Vec4(*textColorHover), 1) arrPress.setZ(arrowZpos) arrHover = realArrow.instanceUnderNode( b.stateNodePath[2], 'h') arrHover.setColor(Vec4(*textColorHover), 1) arrHover.setZ(arrowZpos) # weird, if sort order is 0, it's obscured by the frame for a in (arrPress, arrHover): a.reparentTo(a.getParent(), sort=10) if not disabled: extraArgs = [self.numItems, f if haveSubmenu else 0] self.accept(DGG.ENTER + b.guiId, self.__hoverOnItem, extraArgs) self.accept(DGG.EXIT + b.guiId, self.__offItem) #~ self.itemCommand.append((None,0) if haveSubmenu else (f,args)) self.itemCommand.append((f, args)) if self.numItems == 0: self.firstButtonIdx = int(b.guiId[2:]) self.numItems += 1 z -= LH + self.font.getLineHeight() * self.scale * EoLcount else: # SEPARATOR LINE z += LH - separatorHalfHeight - baselineToBot * self.scale LSseparator.moveTo(0, 0, z) LSseparator.drawTo(self.scale * .5, 0, z) LSseparator.drawTo(self.scale, 0, z) z -= separatorHalfHeight + baselineToTop * self.scale maxWidth += 7 * arrowHalfHeight * ( anyArrow or anyShortcut) + .2 + shortcutTextMaxWidth arrowXpos = maxWidth - arrowHalfHeight realArrow.setX(arrowXpos) if anyImage: leftPad += 2 * imageHalfHeight + leftPad for sct in shortcutTexts: sct.setX(maxWidth - 2 * (arrowHalfHeight * anyArrow + .2)) for c in asList(self.menu.findAllMatches('**/DirectButton*')): numLines = c.node().getFrame()[2] c.node().setFrame( Vec4( -leftPad, maxWidth, -baselineToBot - (numLines * .1 * self.itemHeight if numLines >= 10 else 0), baselineToTop)) loadPrcFileData('', 'text-flatten 1') try: minZ = self.menu.getChild(0).getRelativePoint( b, Point3(0, 0, b.node().getFrame()[2]))[2] except: minZ = self.menu.getChild(0).getRelativePoint( self.menu, Point3( 0, 0, b.getTightBounds()[0][2]))[2] - baselineToBot * .5 try: top = self.menu.getChild(0).node().getFrame()[3] except: top = self.menu.getChild(0).getZ() + baselineToTop l, r, b, t = -leftPad - bgPad / self.scale, maxWidth + bgPad / self.scale, minZ - bgPad / self.scale, top + bgPad / self.scale menuBG = DirectFrame(parent=self.menu.getChild(0), frameSize=(l, r, b, t), frameColor=BGColor, state=DGG.NORMAL, suppressMouse=1) menuBorder = self.menu.getChild(0).attachNewNode('border') borderVtx = ( (l, 0, b), (l, 0, .5 * (b + t)), (l, 0, t), (.5 * (l + r), 0, t), (r, 0, t), (r, 0, .5 * (b + t)), (r, 0, b), (.5 * (l + r), 0, b), (l, 0, b), ) LSborderBG = LineSegs() LSborderBG.setThickness(4) LSborderBG.setColor(0, 0, 0, .7) LSborderBG.moveTo(*(borderVtx[0])) for v in borderVtx[1:]: LSborderBG.drawTo(*v) # fills the gap at corners for v in range(0, 7, 2): LSborderBG.moveTo(*(borderVtx[v])) menuBorder.attachNewNode(LSborderBG.create()) LSborder = LineSegs() LSborder.setThickness(2) LSborder.setColor(*BGBorderColor) LSborder.moveTo(*(borderVtx[0])) for v in borderVtx[1:]: LSborder.drawTo(*v) menuBorder.attachNewNode(LSborder.create()) for v in range(1, 8, 2): LSborderBG.setVertexColor(v, Vec4(0, 0, 0, .1)) LSborder.setVertexColor(v, Vec4(.3, .3, .3, .5)) menuBorderB3 = menuBorder.getTightBounds() menuBorderDims = menuBorderB3[1] - menuBorderB3[0] menuBG.wrtReparentTo(self.menu, sort=-1) self.menu.reparentTo(self.parent) x = -menuBorderB3[0][0] * self.scale for c in asList(self.menu.getChildren()): c.setX(x) self.maxWidth = maxWidth = menuBorderDims[0] self.height = menuBorderDims[2] maxWidthR2D = maxWidth * self.menu.getChild(0).getSx(render2d) separatorLines = self.menu.attachNewNode(LSseparator.create(), 10) separatorLines.setSx(maxWidth) for v in range(1, LSseparator.getNumVertices(), 3): LSseparator.setVertexColor(v, Vec4(*separatorColor)) x = clampScalar(-.98, .98 - maxWidthR2D, self.mpos[0] - maxWidthR2D * .5) minZ = (-.98 if self.minZ is None else self.minZ) z = clampScalar( minZ + menuBorderDims[2] * self.scale * self.parent.getSz(render2d), .98, self.mpos[1] if useMouseZ else -1000) self.menu.setPos(render2d, x, 0, z) self.menu.setTransparency(1) self.origBTprefix = self.BT.getPrefix() self.BT.setPrefix(self.BTprefix) self.accept(self.BTprefix + 'escape', self.destroy) for e in ('mouse1', 'mouse3'): self.accept(self.BTprefix + e, self.destroy, [True]) self.accept(self.BTprefix + 'arrow_down', self.__nextItem) self.accept(self.BTprefix + 'arrow_down-repeat', self.__nextItem) self.accept(self.BTprefix + 'arrow_up', self.__prevItem) self.accept(self.BTprefix + 'arrow_up-repeat', self.__prevItem) self.accept(self.BTprefix + 'enter', self.__runSelItemCommand) self.accept(self.BTprefix + 'space', self.__runSelItemCommand)