def __init__(self): self.mainlistener = MainListener(self) self.windows = WindowManager() # temporary aliases for compatibility with rest of the code self.open_popup = self.windows.open_popup self.open_error_popup = self.windows.open_error_popup # Main menu background image setup. available_images = glob.glob('content/gui/images/background/mainmenu/bg_*.png') self.bg_images = deque(available_images) latest_bg = horizons.globals.fife.get_uh_setting("LatestBackground") try: # If we know the current background from an earlier session, # show all other available ones before picking that one again. self.bg_images.remove(latest_bg) self.bg_images.append(latest_bg) except ValueError: pass self._background = Icon(position_technique='center:center') self.rotate_background() self._background.show() # Initialize menu dialogs and widgets that are accessed from `gui`. self.singleplayermenu = SingleplayerMenu(self.windows) self.multiplayermenu = MultiplayerMenu(self, self.windows) self.help_dialog = HelpDialog(self.windows) self.loadingscreen = LoadingScreen() self.settings_dialog = SettingsDialog(self.windows) self.mainmenu = MainMenu(self, self.windows) self.fps_display = FPSDisplay()
def __init__(self): self.mainlistener = MainListener(self) self.current = None # currently active window self.widgets = LazyWidgetsDict( self.styles) # access widgets with their filenames without '.xml' self.session = None self.current_dialog = None self.dialog_executed = False self.__pause_displayed = False self._background_image = self._get_random_background() self.subscribe() self.singleplayermenu = SingleplayerMenu(self) self.multiplayermenu = MultiplayerMenu(self) self.help_dialog = HelpDialog(self)
def __init__(self): self.mainlistener = MainListener(self) self.current = None # currently active window self.widgets = LazyWidgetsDict(self.styles) # access widgets with their filenames without '.xml' self.session = None self.current_dialog = None self.dialog_executed = False self.__pause_displayed = False self._background_image = self._get_random_background() self.subscribe() self.singleplayermenu = SingleplayerMenu(self) self.multiplayermenu = MultiplayerMenu(self) self.help_dialog = HelpDialog(self)
def __init__(self): self.mainlistener = MainListener(self) self.windows = WindowManager() # temporary aliases for compatibility with rest of the code self.show_popup = self.windows.show_popup self.show_error_popup = self.windows.show_error_popup self._background = Icon(image=self._get_random_background(), position_technique='center:center') self._background.show() self.singleplayermenu = SingleplayerMenu(self.windows) self.multiplayermenu = MultiplayerMenu(self, self.windows) self.help_dialog = HelpDialog(self.windows) self.loadingscreen = LoadingScreen() self.settings_dialog = SettingsDialog(self.windows) self.mainmenu = MainMenu(self, self.windows) self.fps_display = FPSDisplay()
def __init__(self): self.mainlistener = MainListener(self) self.windows = WindowManager() # temporary aliases for compatibility with rest of the code self.open_popup = self.windows.open_popup self.open_error_popup = self.windows.open_error_popup self._background = Background() self._background.show() # Initialize menu dialogs and widgets that are accessed from `gui`. self.singleplayermenu = SingleplayerMenu(self.windows) self.multiplayermenu = MultiplayerMenu(self, self.windows) self.help_dialog = HelpDialog(self.windows) self.loadingscreen = LoadingScreen() self.settings_dialog = SettingsDialog(self.windows) self.mainmenu = MainMenu(self, self.windows) self.fps_display = FPSDisplay()
class Gui(object): """This class handles all the out of game menu, like the main and pause menu, etc. """ log = logging.getLogger("gui") # styles to apply to a widget styles = { "mainmenu": "menu", "requirerestart": "book", "ingamemenu": "headline", "help": "book", "singleplayermenu": "book", "sp_random": "book", "sp_scenario": "book", "sp_free_maps": "book", "multiplayermenu": "book", "multiplayer_creategame": "book", "multiplayer_gamelobby": "book", "playerdataselection": "book", "aidataselection": "book", "select_savegame": "book", "ingame_pause": "book", "game_settings": "book", "editor_pause_menu": "headline", } def __init__(self): self.mainlistener = MainListener(self) self.current = None # currently active window self.widgets = LazyWidgetsDict(self.styles) # access widgets with their filenames without '.xml' self.session = None self.current_dialog = None self.dialog_executed = False self.__pause_displayed = False self._background_image = self._get_random_background() self.subscribe() self.singleplayermenu = SingleplayerMenu(self) self.multiplayermenu = MultiplayerMenu(self) self.help_dialog = HelpDialog(self) self.selectsavegame_dialog = SelectSavegameDialog(self) self.show_select_savegame = self.selectsavegame_dialog.show_select_savegame def subscribe(self): """Subscribe to the necessary messages.""" GuiAction.subscribe(self._on_gui_action) def unsubscribe(self): GuiAction.unsubscribe(self._on_gui_action) # basic menu widgets def show_main(self): """Shows the main menu """ self._switch_current_widget( "mainmenu", center=True, show=True, event_map={ "startSingle": self.show_single, # first is the icon in menu "start": self.show_single, # second is the label in menu "startMulti": self.show_multi, "start_multi": self.show_multi, "settingsLink": self.show_settings, "settings": self.show_settings, "helpLink": self.on_help, "help": self.on_help, "editor_link": self.show_editor_start_menu, "editor": self.show_editor_start_menu, "closeButton": self.show_quit, "quit": self.show_quit, "creditsLink": self.show_credits, "credits": self.show_credits, "loadgameButton": self.load_game, "loadgame": self.load_game, "changeBackground": self.get_random_background_by_button, }, ) self.on_escape = self.show_quit def load_game(self): saved_game = self.show_select_savegame(mode="load") if saved_game is None: return False # user aborted dialog self.show_loading_screen() options = StartGameOptions(saved_game) horizons.main.start_singleplayer(options) return True def toggle_pause(self): """Shows in-game pause menu if the game is currently not paused. Else unpauses and hides the menu. Multiple layers of the 'paused' concept exist; if two widgets are opened which would both pause the game, we do not want to unpause after only one of them is closed. Uses PauseCommand and UnPauseCommand. """ # TODO: logically, this now belongs to the ingame_gui (it used to be different) # this manifests itself by the need for the __pause_displayed hack below # in the long run, this should be moved, therefore eliminating the hack, and # ensuring correct setup/teardown. if self.__pause_displayed: self.__pause_displayed = False self.hide() self.current = None UnPauseCommand(suggestion=True).execute(self.session) self.on_escape = self.toggle_pause else: self.__pause_displayed = True # reload the menu because caching creates spacing problems # see http://trac.unknown-horizons.org/t/ticket/1047 in_editor_mode = self.session.in_editor_mode() menu_name = "editor_pause_menu" if in_editor_mode else "ingamemenu" self.widgets.reload(menu_name) def do_load(): did_load = self.load_game() if did_load: self.__pause_displayed = False def do_load_map(): self.show_editor_start_menu(False) def do_quit(): did_quit = self.quit_session() if did_quit: self.__pause_displayed = False events = { # needed twice, save only once here "e_load": do_load_map if in_editor_mode else do_load, "e_save": self.session.ingame_gui.show_save_map_dialog if in_editor_mode else self.save_game, "e_sett": self.show_settings, "e_help": self.on_help, "e_start": self.toggle_pause, "e_quit": do_quit, } self._switch_current_widget( menu_name, center=True, show=False, event_map={ # icons "loadgameButton": events["e_load"], "savegameButton": events["e_save"], "settingsLink": events["e_sett"], "helpLink": events["e_help"], "startGame": events["e_start"], "closeButton": events["e_quit"], # labels "loadgame": events["e_load"], "savegame": events["e_save"], "settings": events["e_sett"], "help": events["e_help"], "start": events["e_start"], "quit": events["e_quit"], }, ) self.show_modal_background() self.current.show() PauseCommand(suggestion=True).execute(self.session) self.on_escape = self.toggle_pause # what happens on button clicks def save_game(self): """Wrapper for saving for separating gui messages from save logic """ success = self.session.save() if not success: # There was a problem during the 'save game' procedure. self.show_popup(_("Error"), _("Failed to save.")) def show_settings(self): """Displays settings gui derived from the FIFE settings module.""" horizons.globals.fife.show_settings() def on_help(self): self.help_dialog.toggle() def show_quit(self): """Shows the quit dialog. Closes the game unless the dialog is cancelled.""" message = _("Are you sure you want to quit Unknown Horizons?") if self.show_popup(_("Quit Game"), message, show_cancel_button=True): horizons.main.quit() def quit_session(self, force=False): """Quits the current session. Usually returns to main menu afterwards. @param force: whether to ask for confirmation""" message = _("Are you sure you want to abort the running session?") if force or self.show_popup(_("Quit Session"), message, show_cancel_button=True): if self.current is not None: # this can be None if not called from gui (e.g. scenario finished) self.hide() self.current = None if self.session is not None: self.session.end() self.session = None self.show_main() return True else: return False def show_credits(self, number=0): """Shows the credits dialog. """ widget = CreditsPickbeltWidget().get_widget() widget.position_technique = "automatic" # Overwrite a few style pieces for box in widget.findChildren(name="box"): box.margins = (30, 0) # to get some indentation box.padding = 3 for listbox in widget.findChildren(name="translators"): listbox.background_color = fife.Color(255, 255, 255, 0) self.show_dialog(widget, {OkButton.DEFAULT_NAME: True}) # display def on_escape(self): pass def show(self): self.log.debug("Gui: showing current: %s", self.current) if self.current is not None: self.current.show() def hide(self): self.log.debug("Gui: hiding current: %s", self.current) if self.current is not None: self.current.hide() self.hide_modal_background() def is_visible(self): return self.current is not None and self.current.isVisible() def show_dialog(self, dlg, bind, event_map=None, modal=False): """Shows any pychan dialog. @param dlg: dialog that is to be shown @param bind: events that make the dialog return + return values{ 'ok': callback, 'cancel': callback } @param event_map: dictionary with callbacks for buttons. See pychan docu: pychan.widget.mapEvents() @param modal: Whether to block user interaction while displaying the dialog """ self.current_dialog = dlg if event_map is not None: dlg.mapEvents(event_map) if modal: self.show_modal_background() # handle escape and enter keypresses def _on_keypress(event, dlg=dlg): # rebind to make sure this dlg is used from horizons.engine import pychan_util if event.getKey().getValue() == fife.Key.ESCAPE: # convention says use cancel action btn = dlg.findChild(name=CancelButton.DEFAULT_NAME) callback = pychan_util.get_button_event(btn) if btn else None if callback: pychan.tools.applyOnlySuitable(callback, event=event, widget=btn) else: # escape should hide the dialog default horizons.globals.fife.pychanmanager.breakFromMainLoop(returnValue=False) dlg.hide() elif event.getKey().getValue() == fife.Key.ENTER: # convention says use ok action btn = dlg.findChild(name=OkButton.DEFAULT_NAME) callback = pychan_util.get_button_event(btn) if btn else None if callback: pychan.tools.applyOnlySuitable(callback, event=event, widget=btn) # can't guess a default action here dlg.capture(_on_keypress, event_name="keyPressed") # show that a dialog is being executed, this can sometimes require changes in program logic elsewhere self.dialog_executed = True ret = dlg.execute(bind) self.dialog_executed = False if modal: self.hide_modal_background() return ret def show_popup(self, windowtitle, message, show_cancel_button=False, size=0, modal=True): """Displays a popup with the specified text @param windowtitle: the title of the popup @param message: the text displayed in the popup @param show_cancel_button: boolean, show cancel button or not @param size: 0, 1 or 2. Larger means bigger. @param modal: Whether to block user interaction while displaying the popup @return: True on ok, False on cancel (if no cancel button, always True) """ popup = self.build_popup(windowtitle, message, show_cancel_button, size=size) # ok should be triggered on enter, therefore we need to focus the button # pychan will only allow it after the widgets is shown def focus_ok_button(): popup.findChild(name=OkButton.DEFAULT_NAME).requestFocus() ExtScheduler().add_new_object(focus_ok_button, self, run_in=0) if show_cancel_button: return self.show_dialog(popup, {OkButton.DEFAULT_NAME: True, CancelButton.DEFAULT_NAME: False}, modal=modal) else: return self.show_dialog(popup, {OkButton.DEFAULT_NAME: True}, modal=modal) def show_error_popup(self, windowtitle, description, advice=None, details=None, _first=True): """Displays a popup containing an error message. @param windowtitle: title of popup, will be auto-prefixed with "Error: " @param description: string to tell the user what happened @param advice: how the user might be able to fix the problem @param details: technical details, relevant for debugging but not for the user @param _first: Don't touch this. Guide for writing good error messages: http://www.useit.com/alertbox/20010624.html """ msg = u"" msg += description + u"\n" if advice: msg += advice + u"\n" if details: msg += _("Details: {error_details}").format(error_details=details) try: self.show_popup( _("Error: {error_message}").format(error_message=windowtitle), msg, show_cancel_button=False ) except SystemExit: # user really wants us to die raise except: # could be another game error, try to be persistent in showing the error message # else the game would be gone without the user being able to read the message. if _first: traceback.print_exc() print "Exception while showing error, retrying once more" return self.show_error_popup(windowtitle, description, advice, details, _first=False) else: raise # it persists, we have to die. def build_popup(self, windowtitle, message, show_cancel_button=False, size=0): """ Creates a pychan popup widget with the specified properties. @param windowtitle: the title of the popup @param message: the text displayed in the popup @param show_cancel_button: boolean, include cancel button or not @param size: 0, 1 or 2 @return: Container(name='popup_window') with buttons 'okButton' and optionally 'cancelButton' """ if size == 0: wdg_name = "popup_230" elif size == 1: wdg_name = "popup_290" elif size == 2: wdg_name = "popup_350" else: assert False, "size should be 0 <= size <= 2, but is " + str(size) # NOTE: reusing popup dialogs can sometimes lead to exit(0) being called. # it is yet unknown why this happens, so let's be safe for now and reload the widgets. self.widgets.reload(wdg_name) popup = self.widgets[wdg_name] if not show_cancel_button: cancel_button = popup.findChild(name=CancelButton.DEFAULT_NAME) cancel_button.parent.removeChild(cancel_button) popup.headline = popup.findChild(name="headline") popup.headline.text = _(windowtitle) popup.message = popup.findChild(name="popup_message") popup.message.text = _(message) popup.adaptLayout() # recalculate widths return popup def show_modal_background(self): """ Loads transparent background that de facto prohibits access to other gui elements by eating all input events. Used for modal popups and our in-game menu. """ height = horizons.globals.fife.engine_settings.getScreenHeight() width = horizons.globals.fife.engine_settings.getScreenWidth() image = horizons.globals.fife.imagemanager.loadBlank(width, height) image = fife.GuiImage(image) self.additional_widget = pychan.Icon(image=image) self.additional_widget.position = (0, 0) self.additional_widget.show() def hide_modal_background(self): try: self.additional_widget.hide() del self.additional_widget except AttributeError: pass # only used for some widgets, e.g. pause def show_loading_screen(self): self._switch_current_widget("loadingscreen", center=True, show=True) # Add 'Quote of the Load' to loading screen: qotl_type_label = self.current.findChild(name="qotl_type_label") qotl_label = self.current.findChild(name="qotl_label") quote_type = int(horizons.globals.fife.get_uh_setting("QuotesType")) if quote_type == 2: quote_type = random.randint(0, 1) # choose a random type if quote_type == 0: name = GAMEPLAY_TIPS["name"] items = GAMEPLAY_TIPS["items"] elif quote_type == 1: name = FUN_QUOTES["name"] items = FUN_QUOTES["items"] qotl_type_label.text = unicode(name) qotl_label.text = unicode(random.choice(items)) # choose a random quote / gameplay tip # helper def _switch_current_widget(self, new_widget, center=False, event_map=None, show=False, hide_old=False): """Switches self.current to a new widget. @param new_widget: str, widget name @param center: bool, whether to center the new widget @param event_map: pychan event map to apply to new widget @param show: bool, if True old window gets hidden and new one shown @param hide_old: bool, if True old window gets hidden. Implied by show @return: instance of old widget""" old = self.current if (show or hide_old) and old is not None: self.log.debug("Gui: hiding %s", old) self.hide() self.log.debug("Gui: setting current to %s", new_widget) if isinstance(new_widget, str): self.current = self.widgets[new_widget] else: self.current = new_widget bg = self.current.findChild(name="background") if bg: # Set background image bg.image = self._background_image if center: self.current.position_technique = "automatic" # == "center:center" if event_map: self.current.mapEvents(event_map) if show: self.current.show() return old def get_random_background_by_button(self): """Randomly select a background image to use. This function is triggered by change background button from main menu.""" # we need to redraw screen to apply changes. self.hide() self._background_image = self._get_random_background() self.show_main() def _get_random_background(self): """Randomly select a background image to use through out the game menu.""" available_images = glob.glob("content/gui/images/background/mainmenu/bg_*.png") # get latest background latest_background = horizons.globals.fife.get_uh_setting("LatestBackground") # if there is a latest background then remove it from available list if latest_background is not None: available_images.remove(latest_background) background_choice = random.choice(available_images) # save current background choice horizons.globals.fife.set_uh_setting("LatestBackground", background_choice) horizons.globals.fife.save_settings() return background_choice def _on_gui_action(self, msg): AmbientSoundComponent.play_special("click") def show_editor_start_menu(self, from_main_menu=True): editor_start_menu = EditorStartMenu(self, from_main_menu) self._switch_current_widget(editor_start_menu, hide_old=True) return True def show_single(self): self.singleplayermenu.show_single() def show_multi(self): self.multiplayermenu.show_multi()
class Gui(object): """This class handles all the out of game menu, like the main and pause menu, etc. """ log = logging.getLogger("gui") # styles to apply to a widget styles = { 'mainmenu': 'menu', 'requirerestart': 'book', 'ingamemenu': 'headline', 'help': 'book', 'singleplayermenu': 'book', 'sp_random': 'book', 'sp_scenario': 'book', 'sp_free_maps': 'book', 'multiplayermenu' : 'book', 'multiplayer_creategame' : 'book', 'multiplayer_gamelobby' : 'book', 'playerdataselection' : 'book', 'aidataselection' : 'book', 'select_savegame': 'book', 'ingame_pause': 'book', 'game_settings' : 'book', # 'credits': 'book', 'editor_select_map': 'book', 'editor_pause_menu': 'headline', } def __init__(self): self.mainlistener = MainListener(self) self.current = None # currently active window self.widgets = LazyWidgetsDict(self.styles) # access widgets with their filenames without '.xml' self.session = None self.current_dialog = None self.dialog_executed = False self.__pause_displayed = False self._background_image = self._get_random_background() self.subscribe() self.singleplayermenu = SingleplayerMenu(self) self.multiplayermenu = MultiplayerMenu(self) self.help_dialog = HelpDialog(self) def subscribe(self): """Subscribe to the necessary messages.""" GuiAction.subscribe(self._on_gui_action) def unsubscribe(self): GuiAction.unsubscribe(self._on_gui_action) # basic menu widgets def show_main(self): """Shows the main menu """ self._switch_current_widget('mainmenu', center=True, show=True, event_map={ 'startSingle' : self.show_single, # first is the icon in menu 'start' : self.show_single, # second is the label in menu 'startMulti' : self.show_multi, 'start_multi' : self.show_multi, 'settingsLink' : self.show_settings, 'settings' : self.show_settings, 'helpLink' : self.on_help, 'help' : self.on_help, 'editor_link' : self.editor_load_map, 'editor' : self.editor_load_map, 'closeButton' : self.show_quit, 'quit' : self.show_quit, 'creditsLink' : self.show_credits, 'credits' : self.show_credits, 'loadgameButton' : self.load_game, 'loadgame' : self.load_game, 'changeBackground' : self.get_random_background_by_button, }) self.on_escape = self.show_quit def load_game(self): saved_game = self.show_select_savegame(mode='load') if saved_game is None: return False # user aborted dialog self.show_loading_screen() options = StartGameOptions(saved_game) horizons.main.start_singleplayer(options) return True def toggle_pause(self): """Shows in-game pause menu if the game is currently not paused. Else unpauses and hides the menu. Multiple layers of the 'paused' concept exist; if two widgets are opened which would both pause the game, we do not want to unpause after only one of them is closed. Uses PauseCommand and UnPauseCommand. """ # TODO: logically, this now belongs to the ingame_gui (it used to be different) # this manifests itself by the need for the __pause_displayed hack below # in the long run, this should be moved, therefore eliminating the hack, and # ensuring correct setup/teardown. if self.__pause_displayed: self.__pause_displayed = False self.hide() self.current = None UnPauseCommand(suggestion=True).execute(self.session) self.on_escape = self.toggle_pause else: self.__pause_displayed = True # reload the menu because caching creates spacing problems # see http://trac.unknown-horizons.org/t/ticket/1047 in_editor_mode = self.session.in_editor_mode() menu_name = 'editor_pause_menu' if in_editor_mode else 'ingamemenu' self.widgets.reload(menu_name) def do_load(): did_load = self.load_game() if did_load: self.__pause_displayed = False def do_load_map(): if self.editor_load_map(): self.__pause_displayed = False def do_quit(): did_quit = self.quit_session() if did_quit: self.__pause_displayed = False events = { # needed twice, save only once here 'e_load' : do_load_map if in_editor_mode else do_load, 'e_save' : self.session.ingame_gui.show_save_map_dialog if in_editor_mode else self.save_game, 'e_sett' : self.show_settings, 'e_help' : self.on_help, 'e_start': self.toggle_pause, 'e_quit' : do_quit, } self._switch_current_widget(menu_name, center=True, show=False, event_map={ # icons 'loadgameButton' : events['e_load'], 'savegameButton' : events['e_save'], 'settingsLink' : events['e_sett'], 'helpLink' : events['e_help'], 'startGame' : events['e_start'], 'closeButton' : events['e_quit'], # labels 'loadgame' : events['e_load'], 'savegame' : events['e_save'], 'settings' : events['e_sett'], 'help' : events['e_help'], 'start' : events['e_start'], 'quit' : events['e_quit'], }) self.show_modal_background() self.current.show() PauseCommand(suggestion=True).execute(self.session) self.on_escape = self.toggle_pause # what happens on button clicks def save_game(self): """Wrapper for saving for separating gui messages from save logic """ success = self.session.save() if not success: # There was a problem during the 'save game' procedure. self.show_popup(_('Error'), _('Failed to save.')) def show_settings(self): """Displays settings gui derived from the FIFE settings module.""" horizons.globals.fife.show_settings() def on_help(self): self.help_dialog.toggle() def show_quit(self): """Shows the quit dialog. Closes the game unless the dialog is cancelled.""" message = _("Are you sure you want to quit Unknown Horizons?") if self.show_popup(_("Quit Game"), message, show_cancel_button=True): horizons.main.quit() def quit_session(self, force=False): """Quits the current session. Usually returns to main menu afterwards. @param force: whether to ask for confirmation""" message = _("Are you sure you want to abort the running session?") if force or self.show_popup(_("Quit Session"), message, show_cancel_button=True): if self.current is not None: # this can be None if not called from gui (e.g. scenario finished) self.hide() self.current = None if self.session is not None: self.session.end() self.session = None self.show_main() return True else: return False def show_credits(self, number=0): """Shows the credits dialog. """ if self.current_dialog is not None: self.current_dialog.hide() credits_page = self.widgets['credits{number}'.format(number=number)] for box in credits_page.findChildren(name='box'): box.margins = (30, 0) # to get some indentation if number in [0, 2]: # #TODO fix these hardcoded page references box.padding = 1 box.parent.padding = 3 # further decrease if more entries labels = [credits_page.findChild(name=section+"_lbl") for section in ('team', 'patchers', 'translators', 'packagers', 'special_thanks')] for i in xrange(5): # add callbacks to each pickbelt labels[i].capture(Callback(self.show_credits, i), event_name="mouseClicked") self.show_dialog(credits_page, {OkButton.DEFAULT_NAME : True}) def show_select_savegame(self, mode, sanity_checker=None, sanity_criteria=None): """Shows menu to select a savegame. @param mode: Valid options are 'save', 'load', 'mp_load', 'mp_save' @param sanity_checker: only allow manually entered names that pass this test @param sanity_criteria: explain which names are allowed to the user @return: Path to savegamefile or None""" assert mode in ('save', 'load', 'mp_load', 'mp_save') map_files, map_file_display = None, None args = mode, sanity_checker, sanity_criteria # for reshow mp = mode.startswith('mp_') if mp: mode = mode[3:] # below this line, mp_load == load, mp_save == save if mode == 'load': if not mp: map_files, map_file_display = SavegameManager.get_saves() else: map_files, map_file_display = SavegameManager.get_multiplayersaves() if not map_files: self.show_popup(_("No saved games"), _("There are no saved games to load.")) return else: # don't show autosave and quicksave on save if not mp: map_files, map_file_display = SavegameManager.get_regular_saves() else: map_files, map_file_display = SavegameManager.get_multiplayersaves() # Prepare widget old_current = self._switch_current_widget('select_savegame') if mode == 'save': helptext = _('Save game') elif mode == 'load': helptext = _('Load game') # else: not a valid mode, so we can as well crash on the following self.current.findChild(name='headline').text = helptext self.current.findChild(name=OkButton.DEFAULT_NAME).helptext = helptext name_box = self.current.findChild(name="gamename_box") password_box = self.current.findChild(name="gamepassword_box") if mp and mode == 'load': # have gamename name_box.parent.showChild(name_box) password_box.parent.showChild(password_box) gamename_textfield = self.current.findChild(name="gamename") gamepassword_textfield = self.current.findChild(name="gamepassword") gamepassword_textfield.text = u"" def clear_gamedetails_textfields(): gamename_textfield.text = u"" gamepassword_textfield.text = u"" gamename_textfield.capture(clear_gamedetails_textfields, 'mouseReleased', 'default') else: if name_box not in name_box.parent.hidden_children: name_box.parent.hideChild(name_box) if password_box not in name_box.parent.hidden_children: password_box.parent.hideChild(password_box) self.current.show() if not hasattr(self, 'filename_hbox'): self.filename_hbox = self.current.findChild(name='enter_filename') self.filename_hbox_parent = self.filename_hbox.parent if mode == 'save': # only show enter_filename on save self.filename_hbox_parent.showChild(self.filename_hbox) elif self.filename_hbox not in self.filename_hbox_parent.hidden_children: self.filename_hbox_parent.hideChild(self.filename_hbox) def tmp_selected_changed(): """Fills in the name of the savegame in the textbox when selected in the list""" if mode != 'save': # set textbox only if we are in save mode return if self.current.collectData('savegamelist') == -1: # set blank if nothing is selected self.current.findChild(name="savegamefile").text = u"" else: savegamefile = map_file_display[self.current.collectData('savegamelist')] self.current.distributeData({'savegamefile': savegamefile}) self.current.distributeInitialData({'savegamelist': map_file_display}) # Select first item when loading, nothing when saving selected_item = -1 if mode == 'save' else 0 self.current.distributeData({'savegamelist': selected_item}) cb_details = Gui._create_show_savegame_details(self.current, map_files, 'savegamelist') cb = Callback.ChainedCallbacks(cb_details, tmp_selected_changed) cb() # Refresh data on start self.current.mapEvents({'savegamelist/action': cb}) self.current.findChild(name="savegamelist").capture(cb, event_name="keyPressed") bind = { OkButton.DEFAULT_NAME : True, CancelButton.DEFAULT_NAME : False, DeleteButton.DEFAULT_NAME : 'delete' } if mode == 'save': bind['savegamefile'] = True retval = self.show_dialog(self.current, bind) if not retval: # cancelled self.current = old_current return if retval == 'delete': # delete button was pressed. Apply delete and reshow dialog, delegating the return value delete_retval = self._delete_savegame(map_files) if delete_retval: self.current.distributeData({'savegamelist' : -1}) cb() self.current = old_current return self.show_select_savegame(*args) selected_savegame = None if mode == 'save': # return from textfield selected_savegame = self.current.collectData('savegamefile') if selected_savegame == "": self.show_error_popup(windowtitle=_("No filename given"), description=_("Please enter a valid filename.")) self.current = old_current return self.show_select_savegame(*args) # reshow dialog elif selected_savegame in map_file_display: # savegamename already exists #xgettext:python-format message = _("A savegame with the name '{name}' already exists.").format( name=selected_savegame) + u"\n" + _('Overwrite it?') # keep the pop-up non-modal because otherwise it is double-modal (#1876) if not self.show_popup(_("Confirmation for overwriting"), message, show_cancel_button=True, modal=False): self.current = old_current return self.show_select_savegame(*args) # reshow dialog elif sanity_checker and sanity_criteria: if not sanity_checker(selected_savegame): self.show_error_popup(windowtitle=_("Invalid filename given"), description=sanity_criteria) self.current = old_current return self.show_select_savegame(*args) # reshow dialog else: # return selected item from list selected_savegame = self.current.collectData('savegamelist') assert selected_savegame != -1, "No savegame selected in savegamelist" selected_savegame = map_files[selected_savegame] if mp and mode == 'load': # also name gamename_textfield = self.current.findChild(name="gamename") ret = selected_savegame, self.current.collectData('gamename'), self.current.collectData('gamepassword') else: ret = selected_savegame self.current = old_current # reuse old widget return ret # display def on_escape(self): pass def show(self): self.log.debug("Gui: showing current: %s", self.current) if self.current is not None: self.current.show() def hide(self): self.log.debug("Gui: hiding current: %s", self.current) if self.current is not None: self.current.hide() self.hide_modal_background() def is_visible(self): return self.current is not None and self.current.isVisible() def show_dialog(self, dlg, bind, event_map=None, modal=False): """Shows any pychan dialog. @param dlg: dialog that is to be shown @param bind: events that make the dialog return + return values{ 'ok': callback, 'cancel': callback } @param event_map: dictionary with callbacks for buttons. See pychan docu: pychan.widget.mapEvents() @param modal: Whether to block user interaction while displaying the dialog """ self.current_dialog = dlg if event_map is not None: dlg.mapEvents(event_map) if modal: self.show_modal_background() # handle escape and enter keypresses def _on_keypress(event, dlg=dlg): # rebind to make sure this dlg is used from horizons.engine import pychan_util if event.getKey().getValue() == fife.Key.ESCAPE: # convention says use cancel action btn = dlg.findChild(name=CancelButton.DEFAULT_NAME) callback = pychan_util.get_button_event(btn) if btn else None if callback: pychan.tools.applyOnlySuitable(callback, event=event, widget=btn) else: # escape should hide the dialog default horizons.globals.fife.pychanmanager.breakFromMainLoop(returnValue=False) dlg.hide() elif event.getKey().getValue() == fife.Key.ENTER: # convention says use ok action btn = dlg.findChild(name=OkButton.DEFAULT_NAME) callback = pychan_util.get_button_event(btn) if btn else None if callback: pychan.tools.applyOnlySuitable(callback, event=event, widget=btn) # can't guess a default action here dlg.capture(_on_keypress, event_name="keyPressed") # show that a dialog is being executed, this can sometimes require changes in program logic elsewhere self.dialog_executed = True ret = dlg.execute(bind) self.dialog_executed = False if modal: self.hide_modal_background() return ret def show_popup(self, windowtitle, message, show_cancel_button=False, size=0, modal=True): """Displays a popup with the specified text @param windowtitle: the title of the popup @param message: the text displayed in the popup @param show_cancel_button: boolean, show cancel button or not @param size: 0, 1 or 2. Larger means bigger. @param modal: Whether to block user interaction while displaying the popup @return: True on ok, False on cancel (if no cancel button, always True) """ popup = self.build_popup(windowtitle, message, show_cancel_button, size=size) # ok should be triggered on enter, therefore we need to focus the button # pychan will only allow it after the widgets is shown def focus_ok_button(): popup.findChild(name=OkButton.DEFAULT_NAME).requestFocus() ExtScheduler().add_new_object(focus_ok_button, self, run_in=0) if show_cancel_button: return self.show_dialog(popup, {OkButton.DEFAULT_NAME : True, CancelButton.DEFAULT_NAME : False}, modal=modal) else: return self.show_dialog(popup, {OkButton.DEFAULT_NAME : True}, modal=modal) def show_error_popup(self, windowtitle, description, advice=None, details=None, _first=True): """Displays a popup containing an error message. @param windowtitle: title of popup, will be auto-prefixed with "Error: " @param description: string to tell the user what happened @param advice: how the user might be able to fix the problem @param details: technical details, relevant for debugging but not for the user @param _first: Don't touch this. Guide for writing good error messages: http://www.useit.com/alertbox/20010624.html """ msg = u"" msg += description + u"\n" if advice: msg += advice + u"\n" if details: msg += _("Details: {error_details}").format(error_details=details) try: self.show_popup( _("Error: {error_message}").format(error_message=windowtitle), msg, show_cancel_button=False) except SystemExit: # user really wants us to die raise except: # could be another game error, try to be persistent in showing the error message # else the game would be gone without the user being able to read the message. if _first: traceback.print_exc() print 'Exception while showing error, retrying once more' return self.show_error_popup(windowtitle, description, advice, details, _first=False) else: raise # it persists, we have to die. def build_popup(self, windowtitle, message, show_cancel_button=False, size=0): """ Creates a pychan popup widget with the specified properties. @param windowtitle: the title of the popup @param message: the text displayed in the popup @param show_cancel_button: boolean, include cancel button or not @param size: 0, 1 or 2 @return: Container(name='popup_window') with buttons 'okButton' and optionally 'cancelButton' """ if size == 0: wdg_name = "popup_230" elif size == 1: wdg_name = "popup_290" elif size == 2: wdg_name = "popup_350" else: assert False, "size should be 0 <= size <= 2, but is "+str(size) # NOTE: reusing popup dialogs can sometimes lead to exit(0) being called. # it is yet unknown why this happens, so let's be safe for now and reload the widgets. self.widgets.reload(wdg_name) popup = self.widgets[wdg_name] if not show_cancel_button: cancel_button = popup.findChild(name=CancelButton.DEFAULT_NAME) cancel_button.parent.removeChild(cancel_button) popup.headline = popup.findChild(name='headline') popup.headline.text = _(windowtitle) popup.message = popup.findChild(name='popup_message') popup.message.text = _(message) popup.adaptLayout() # recalculate widths return popup def show_modal_background(self): """ Loads transparent background that de facto prohibits access to other gui elements by eating all input events. Used for modal popups and our in-game menu. """ height = horizons.globals.fife.engine_settings.getScreenHeight() width = horizons.globals.fife.engine_settings.getScreenWidth() image = horizons.globals.fife.imagemanager.loadBlank(width, height) image = fife.GuiImage(image) self.additional_widget = pychan.Icon(image=image) self.additional_widget.position = (0, 0) self.additional_widget.show() def hide_modal_background(self): try: self.additional_widget.hide() del self.additional_widget except AttributeError: pass # only used for some widgets, e.g. pause def show_loading_screen(self): self._switch_current_widget('loadingscreen', center=True, show=True) # Add 'Quote of the Load' to loading screen: qotl_type_label = self.current.findChild(name='qotl_type_label') qotl_label = self.current.findChild(name='qotl_label') quote_type = int(horizons.globals.fife.get_uh_setting("QuotesType")) if quote_type == 2: quote_type = random.randint(0, 1) # choose a random type if quote_type == 0: name = GAMEPLAY_TIPS["name"] items = GAMEPLAY_TIPS["items"] elif quote_type == 1: name = FUN_QUOTES["name"] items = FUN_QUOTES["items"] qotl_type_label.text = unicode(name) qotl_label.text = unicode(random.choice(items)) # choose a random quote / gameplay tip # helper def _switch_current_widget(self, new_widget, center=False, event_map=None, show=False, hide_old=False): """Switches self.current to a new widget. @param new_widget: str, widget name @param center: bool, whether to center the new widget @param event_map: pychan event map to apply to new widget @param show: bool, if True old window gets hidden and new one shown @param hide_old: bool, if True old window gets hidden. Implied by show @return: instance of old widget""" old = self.current if (show or hide_old) and old is not None: self.log.debug("Gui: hiding %s", old) self.hide() self.log.debug("Gui: setting current to %s", new_widget) self.current = self.widgets[new_widget] bg = self.current.findChild(name='background') if bg: # Set background image bg.image = self._background_image if center: self.current.position_technique = "automatic" # == "center:center" if event_map: self.current.mapEvents(event_map) if show: self.current.show() return old @staticmethod def _create_show_savegame_details(gui, map_files, savegamelist): """Creates a function that displays details of a savegame in gui""" def tmp_show_details(): """Fetches details of selected savegame and displays it""" gui.findChild(name="screenshot").image = None map_file = None map_file_index = gui.collectData(savegamelist) if map_file_index == -1: gui.findChild(name="savegame_details").hide() return else: gui.findChild(name="savegame_details").show() try: map_file = map_files[map_file_index] except IndexError: # this was a click in the savegame list, but not on an element # it happens when the savegame list is empty return savegame_info = SavegameManager.get_metadata(map_file) if savegame_info.get('screenshot'): # try to find a writable location, that is accessible via relative paths # (required by fife) fd, filename = tempfile.mkstemp() try: path_rel = os.path.relpath(filename) except ValueError: # the relative path sometimes doesn't exist on win os.close(fd) os.unlink(filename) # try again in the current dir, it's often writable fd, filename = tempfile.mkstemp(dir=os.curdir) try: path_rel = os.path.relpath(filename) except ValueError: fd, filename = None, None if fd: with os.fdopen(fd, "w") as f: f.write(savegame_info['screenshot']) # fife only supports relative paths gui.findChild(name="screenshot").image = path_rel os.unlink(filename) # savegamedetails details_label = gui.findChild(name="savegamedetails_lbl") details_label.text = u"" if savegame_info['timestamp'] == -1: details_label.text += _("Unknown savedate") else: savetime = time.strftime("%c", time.localtime(savegame_info['timestamp'])) #xgettext:python-format details_label.text += _("Saved at {time}").format(time=savetime.decode('utf-8')) details_label.text += u'\n' counter = savegame_info['savecounter'] # N_ takes care of plural forms for different languages #xgettext:python-format details_label.text += N_("Saved {amount} time", "Saved {amount} times", counter).format(amount=counter) details_label.text += u'\n' details_label.stylize('book') from horizons.constants import VERSION try: #xgettext:python-format details_label.text += _("Savegame version {version}").format( version=savegame_info['savegamerev']) if savegame_info['savegamerev'] != VERSION.SAVEGAMEREVISION: if not SavegameUpgrader.can_upgrade(savegame_info['savegamerev']): details_label.text += u" " + _("(probably incompatible)") except KeyError: # this should only happen for very old savegames, so having this unfriendly # error is ok (savegame is quite certainly fully unusable). details_label.text += u" " + _("Incompatible version") gui.adaptLayout() return tmp_show_details def _delete_savegame(self, map_files): """Deletes the selected savegame if the user confirms self.current has to contain the widget "savegamelist" @param map_files: list of files that corresponds to the entries of 'savegamelist' @return: True if something was deleted, else False """ selected_item = self.current.collectData("savegamelist") if selected_item == -1 or selected_item >= len(map_files): self.show_popup(_("No file selected"), _("You need to select a savegame to delete.")) return False selected_file = map_files[selected_item] #xgettext:python-format message = _("Do you really want to delete the savegame '{name}'?").format( name=SavegameManager.get_savegamename_from_filename(selected_file)) if self.show_popup(_("Confirm deletion"), message, show_cancel_button=True): try: os.unlink(selected_file) return True except: self.show_popup(_("Error!"), _("Failed to delete savefile!")) return False else: # player cancelled deletion return False def get_random_background_by_button(self): """Randomly select a background image to use. This function is triggered by change background button from main menu.""" #we need to redraw screen to apply changes. self.hide() self._background_image = self._get_random_background() self.show_main() def _get_random_background(self): """Randomly select a background image to use through out the game menu.""" available_images = glob.glob('content/gui/images/background/mainmenu/bg_*.png') #get latest background latest_background = horizons.globals.fife.get_uh_setting("LatestBackground") #if there is a latest background then remove it from available list if latest_background is not None: available_images.remove(latest_background) background_choice = random.choice(available_images) #save current background choice horizons.globals.fife.set_uh_setting("LatestBackground", background_choice) horizons.globals.fife.save_settings() return background_choice def _on_gui_action(self, msg): AmbientSoundComponent.play_special('click') def editor_load_map(self): """Show a dialog for the user to select a map to edit.""" old_current = self._switch_current_widget('editor_select_map') self.current.show() map_files, map_file_display = SavegameManager.get_maps() self.current.distributeInitialData({'maplist': map_file_display}) bind = { OkButton.DEFAULT_NAME : True, CancelButton.DEFAULT_NAME : False, } retval = self.show_dialog(self.current, bind) if not retval: # Dialog cancelled self.current = old_current return False selected_map_index = self.current.collectData('maplist') if selected_map_index == -1: # No map selected yet => select first available one self.current.distributeData({'maplist': 0}) self.current = old_current self.show_loading_screen() horizons.main.edit_map(map_files[selected_map_index]) return True def show_single(self): self.singleplayermenu.show_single() def show_multi(self): self.multiplayermenu.show_multi()
class Gui(object): """This class handles all the out of game menu, like the main and pause menu, etc. """ log = logging.getLogger("gui") # styles to apply to a widget styles = { 'mainmenu': 'menu', 'requirerestart': 'book', 'ingamemenu': 'headline', 'help': 'book', 'singleplayermenu': 'book', 'sp_random': 'book', 'sp_scenario': 'book', 'sp_free_maps': 'book', 'multiplayermenu': 'book', 'multiplayer_creategame': 'book', 'multiplayer_gamelobby': 'book', 'playerdataselection': 'book', 'aidataselection': 'book', 'select_savegame': 'book', 'ingame_pause': 'book', 'game_settings': 'book', # 'credits': 'book', 'editor_select_map': 'book', 'editor_pause_menu': 'headline', } def __init__(self): self.mainlistener = MainListener(self) self.current = None # currently active window self.widgets = LazyWidgetsDict( self.styles) # access widgets with their filenames without '.xml' self.session = None self.current_dialog = None self.dialog_executed = False self.__pause_displayed = False self._background_image = self._get_random_background() self.subscribe() self.singleplayermenu = SingleplayerMenu(self) self.multiplayermenu = MultiplayerMenu(self) self.help_dialog = HelpDialog(self) def subscribe(self): """Subscribe to the necessary messages.""" GuiAction.subscribe(self._on_gui_action) def unsubscribe(self): GuiAction.unsubscribe(self._on_gui_action) # basic menu widgets def show_main(self): """Shows the main menu """ self._switch_current_widget( 'mainmenu', center=True, show=True, event_map={ 'startSingle': self.show_single, # first is the icon in menu 'start': self.show_single, # second is the label in menu 'startMulti': self.show_multi, 'start_multi': self.show_multi, 'settingsLink': self.show_settings, 'settings': self.show_settings, 'helpLink': self.on_help, 'help': self.on_help, 'editor_link': self.editor_load_map, 'editor': self.editor_load_map, 'closeButton': self.show_quit, 'quit': self.show_quit, 'creditsLink': self.show_credits, 'credits': self.show_credits, 'loadgameButton': self.load_game, 'loadgame': self.load_game, 'changeBackground': self.get_random_background_by_button, }) self.on_escape = self.show_quit def load_game(self): saved_game = self.show_select_savegame(mode='load') if saved_game is None: return False # user aborted dialog self.show_loading_screen() options = StartGameOptions(saved_game) horizons.main.start_singleplayer(options) return True def toggle_pause(self): """Shows in-game pause menu if the game is currently not paused. Else unpauses and hides the menu. Multiple layers of the 'paused' concept exist; if two widgets are opened which would both pause the game, we do not want to unpause after only one of them is closed. Uses PauseCommand and UnPauseCommand. """ # TODO: logically, this now belongs to the ingame_gui (it used to be different) # this manifests itself by the need for the __pause_displayed hack below # in the long run, this should be moved, therefore eliminating the hack, and # ensuring correct setup/teardown. if self.__pause_displayed: self.__pause_displayed = False self.hide() self.current = None UnPauseCommand(suggestion=True).execute(self.session) self.on_escape = self.toggle_pause else: self.__pause_displayed = True # reload the menu because caching creates spacing problems # see http://trac.unknown-horizons.org/t/ticket/1047 in_editor_mode = self.session.in_editor_mode() menu_name = 'editor_pause_menu' if in_editor_mode else 'ingamemenu' self.widgets.reload(menu_name) def do_load(): did_load = self.load_game() if did_load: self.__pause_displayed = False def do_load_map(): if self.editor_load_map(): self.__pause_displayed = False def do_quit(): did_quit = self.quit_session() if did_quit: self.__pause_displayed = False events = { # needed twice, save only once here 'e_load' : do_load_map if in_editor_mode else do_load, 'e_save' : self.session.ingame_gui.show_save_map_dialog if in_editor_mode else self.save_game, 'e_sett' : self.show_settings, 'e_help' : self.on_help, 'e_start': self.toggle_pause, 'e_quit' : do_quit, } self._switch_current_widget( menu_name, center=True, show=False, event_map={ # icons 'loadgameButton': events['e_load'], 'savegameButton': events['e_save'], 'settingsLink': events['e_sett'], 'helpLink': events['e_help'], 'startGame': events['e_start'], 'closeButton': events['e_quit'], # labels 'loadgame': events['e_load'], 'savegame': events['e_save'], 'settings': events['e_sett'], 'help': events['e_help'], 'start': events['e_start'], 'quit': events['e_quit'], }) self.show_modal_background() self.current.show() PauseCommand(suggestion=True).execute(self.session) self.on_escape = self.toggle_pause # what happens on button clicks def save_game(self): """Wrapper for saving for separating gui messages from save logic """ success = self.session.save() if not success: # There was a problem during the 'save game' procedure. self.show_popup(_('Error'), _('Failed to save.')) def show_settings(self): """Displays settings gui derived from the FIFE settings module.""" horizons.globals.fife.show_settings() def on_help(self): self.help_dialog.toggle() def show_quit(self): """Shows the quit dialog. Closes the game unless the dialog is cancelled.""" message = _("Are you sure you want to quit Unknown Horizons?") if self.show_popup(_("Quit Game"), message, show_cancel_button=True): horizons.main.quit() def quit_session(self, force=False): """Quits the current session. Usually returns to main menu afterwards. @param force: whether to ask for confirmation""" message = _("Are you sure you want to abort the running session?") if force or self.show_popup( _("Quit Session"), message, show_cancel_button=True): if self.current is not None: # this can be None if not called from gui (e.g. scenario finished) self.hide() self.current = None if self.session is not None: self.session.end() self.session = None self.show_main() return True else: return False def show_credits(self, number=0): """Shows the credits dialog. """ if self.current_dialog is not None: self.current_dialog.hide() credits_page = self.widgets['credits{number}'.format(number=number)] for box in credits_page.findChildren(name='box'): box.margins = (30, 0) # to get some indentation if number in [0, 2]: # #TODO fix these hardcoded page references box.padding = 1 box.parent.padding = 3 # further decrease if more entries labels = [ credits_page.findChild(name=section + "_lbl") for section in ('team', 'patchers', 'translators', 'packagers', 'special_thanks') ] for i in xrange(5): # add callbacks to each pickbelt labels[i].capture(Callback(self.show_credits, i), event_name="mouseClicked") self.show_dialog(credits_page, {OkButton.DEFAULT_NAME: True}) def show_select_savegame(self, mode, sanity_checker=None, sanity_criteria=None): """Shows menu to select a savegame. @param mode: Valid options are 'save', 'load', 'mp_load', 'mp_save' @param sanity_checker: only allow manually entered names that pass this test @param sanity_criteria: explain which names are allowed to the user @return: Path to savegamefile or None""" assert mode in ('save', 'load', 'mp_load', 'mp_save') map_files, map_file_display = None, None args = mode, sanity_checker, sanity_criteria # for reshow mp = mode.startswith('mp_') if mp: mode = mode[3:] # below this line, mp_load == load, mp_save == save if mode == 'load': if not mp: map_files, map_file_display = SavegameManager.get_saves() else: map_files, map_file_display = SavegameManager.get_multiplayersaves( ) if not map_files: self.show_popup(_("No saved games"), _("There are no saved games to load.")) return else: # don't show autosave and quicksave on save if not mp: map_files, map_file_display = SavegameManager.get_regular_saves( ) else: map_files, map_file_display = SavegameManager.get_multiplayersaves( ) # Prepare widget old_current = self._switch_current_widget('select_savegame') if mode == 'save': helptext = _('Save game') elif mode == 'load': helptext = _('Load game') # else: not a valid mode, so we can as well crash on the following self.current.findChild(name='headline').text = helptext self.current.findChild(name=OkButton.DEFAULT_NAME).helptext = helptext name_box = self.current.findChild(name="gamename_box") password_box = self.current.findChild(name="gamepassword_box") if mp and mode == 'load': # have gamename name_box.parent.showChild(name_box) password_box.parent.showChild(password_box) gamename_textfield = self.current.findChild(name="gamename") gamepassword_textfield = self.current.findChild( name="gamepassword") gamepassword_textfield.text = u"" def clear_gamedetails_textfields(): gamename_textfield.text = u"" gamepassword_textfield.text = u"" gamename_textfield.capture(clear_gamedetails_textfields, 'mouseReleased', 'default') else: if name_box not in name_box.parent.hidden_children: name_box.parent.hideChild(name_box) if password_box not in name_box.parent.hidden_children: password_box.parent.hideChild(password_box) self.current.show() if not hasattr(self, 'filename_hbox'): self.filename_hbox = self.current.findChild(name='enter_filename') self.filename_hbox_parent = self.filename_hbox.parent if mode == 'save': # only show enter_filename on save self.filename_hbox_parent.showChild(self.filename_hbox) elif self.filename_hbox not in self.filename_hbox_parent.hidden_children: self.filename_hbox_parent.hideChild(self.filename_hbox) def tmp_selected_changed(): """Fills in the name of the savegame in the textbox when selected in the list""" if mode != 'save': # set textbox only if we are in save mode return if self.current.collectData( 'savegamelist') == -1: # set blank if nothing is selected self.current.findChild(name="savegamefile").text = u"" else: savegamefile = map_file_display[self.current.collectData( 'savegamelist')] self.current.distributeData({'savegamefile': savegamefile}) self.current.distributeInitialData({'savegamelist': map_file_display}) # Select first item when loading, nothing when saving selected_item = -1 if mode == 'save' else 0 self.current.distributeData({'savegamelist': selected_item}) cb_details = Gui._create_show_savegame_details(self.current, map_files, 'savegamelist') cb = Callback.ChainedCallbacks(cb_details, tmp_selected_changed) cb() # Refresh data on start self.current.mapEvents({'savegamelist/action': cb}) self.current.findChild(name="savegamelist").capture( cb, event_name="keyPressed") bind = { OkButton.DEFAULT_NAME: True, CancelButton.DEFAULT_NAME: False, DeleteButton.DEFAULT_NAME: 'delete' } if mode == 'save': bind['savegamefile'] = True retval = self.show_dialog(self.current, bind) if not retval: # cancelled self.current = old_current return if retval == 'delete': # delete button was pressed. Apply delete and reshow dialog, delegating the return value delete_retval = self._delete_savegame(map_files) if delete_retval: self.current.distributeData({'savegamelist': -1}) cb() self.current = old_current return self.show_select_savegame(*args) selected_savegame = None if mode == 'save': # return from textfield selected_savegame = self.current.collectData('savegamefile') if selected_savegame == "": self.show_error_popup( windowtitle=_("No filename given"), description=_("Please enter a valid filename.")) self.current = old_current return self.show_select_savegame(*args) # reshow dialog elif selected_savegame in map_file_display: # savegamename already exists #xgettext:python-format message = _("A savegame with the name '{name}' already exists." ).format(name=selected_savegame) + u"\n" + _( 'Overwrite it?') # keep the pop-up non-modal because otherwise it is double-modal (#1876) if not self.show_popup(_("Confirmation for overwriting"), message, show_cancel_button=True, modal=False): self.current = old_current return self.show_select_savegame(*args) # reshow dialog elif sanity_checker and sanity_criteria: if not sanity_checker(selected_savegame): self.show_error_popup( windowtitle=_("Invalid filename given"), description=sanity_criteria) self.current = old_current return self.show_select_savegame(*args) # reshow dialog else: # return selected item from list selected_savegame = self.current.collectData('savegamelist') assert selected_savegame != -1, "No savegame selected in savegamelist" selected_savegame = map_files[selected_savegame] if mp and mode == 'load': # also name gamename_textfield = self.current.findChild(name="gamename") ret = selected_savegame, self.current.collectData( 'gamename'), self.current.collectData('gamepassword') else: ret = selected_savegame self.current = old_current # reuse old widget return ret # display def on_escape(self): pass def show(self): self.log.debug("Gui: showing current: %s", self.current) if self.current is not None: self.current.show() def hide(self): self.log.debug("Gui: hiding current: %s", self.current) if self.current is not None: self.current.hide() self.hide_modal_background() def is_visible(self): return self.current is not None and self.current.isVisible() def show_dialog(self, dlg, bind, event_map=None, modal=False): """Shows any pychan dialog. @param dlg: dialog that is to be shown @param bind: events that make the dialog return + return values{ 'ok': callback, 'cancel': callback } @param event_map: dictionary with callbacks for buttons. See pychan docu: pychan.widget.mapEvents() @param modal: Whether to block user interaction while displaying the dialog """ self.current_dialog = dlg if event_map is not None: dlg.mapEvents(event_map) if modal: self.show_modal_background() # handle escape and enter keypresses def _on_keypress(event, dlg=dlg): # rebind to make sure this dlg is used from horizons.engine import pychan_util if event.getKey().getValue( ) == fife.Key.ESCAPE: # convention says use cancel action btn = dlg.findChild(name=CancelButton.DEFAULT_NAME) callback = pychan_util.get_button_event(btn) if btn else None if callback: pychan.tools.applyOnlySuitable(callback, event=event, widget=btn) else: # escape should hide the dialog default horizons.globals.fife.pychanmanager.breakFromMainLoop( returnValue=False) dlg.hide() elif event.getKey().getValue( ) == fife.Key.ENTER: # convention says use ok action btn = dlg.findChild(name=OkButton.DEFAULT_NAME) callback = pychan_util.get_button_event(btn) if btn else None if callback: pychan.tools.applyOnlySuitable(callback, event=event, widget=btn) # can't guess a default action here dlg.capture(_on_keypress, event_name="keyPressed") # show that a dialog is being executed, this can sometimes require changes in program logic elsewhere self.dialog_executed = True ret = dlg.execute(bind) self.dialog_executed = False if modal: self.hide_modal_background() return ret def show_popup(self, windowtitle, message, show_cancel_button=False, size=0, modal=True): """Displays a popup with the specified text @param windowtitle: the title of the popup @param message: the text displayed in the popup @param show_cancel_button: boolean, show cancel button or not @param size: 0, 1 or 2. Larger means bigger. @param modal: Whether to block user interaction while displaying the popup @return: True on ok, False on cancel (if no cancel button, always True) """ popup = self.build_popup(windowtitle, message, show_cancel_button, size=size) # ok should be triggered on enter, therefore we need to focus the button # pychan will only allow it after the widgets is shown def focus_ok_button(): popup.findChild(name=OkButton.DEFAULT_NAME).requestFocus() ExtScheduler().add_new_object(focus_ok_button, self, run_in=0) if show_cancel_button: return self.show_dialog(popup, { OkButton.DEFAULT_NAME: True, CancelButton.DEFAULT_NAME: False }, modal=modal) else: return self.show_dialog(popup, {OkButton.DEFAULT_NAME: True}, modal=modal) def show_error_popup(self, windowtitle, description, advice=None, details=None, _first=True): """Displays a popup containing an error message. @param windowtitle: title of popup, will be auto-prefixed with "Error: " @param description: string to tell the user what happened @param advice: how the user might be able to fix the problem @param details: technical details, relevant for debugging but not for the user @param _first: Don't touch this. Guide for writing good error messages: http://www.useit.com/alertbox/20010624.html """ msg = u"" msg += description + u"\n" if advice: msg += advice + u"\n" if details: msg += _("Details: {error_details}").format(error_details=details) try: self.show_popup( _("Error: {error_message}").format(error_message=windowtitle), msg, show_cancel_button=False) except SystemExit: # user really wants us to die raise except: # could be another game error, try to be persistent in showing the error message # else the game would be gone without the user being able to read the message. if _first: traceback.print_exc() print 'Exception while showing error, retrying once more' return self.show_error_popup(windowtitle, description, advice, details, _first=False) else: raise # it persists, we have to die. def build_popup(self, windowtitle, message, show_cancel_button=False, size=0): """ Creates a pychan popup widget with the specified properties. @param windowtitle: the title of the popup @param message: the text displayed in the popup @param show_cancel_button: boolean, include cancel button or not @param size: 0, 1 or 2 @return: Container(name='popup_window') with buttons 'okButton' and optionally 'cancelButton' """ if size == 0: wdg_name = "popup_230" elif size == 1: wdg_name = "popup_290" elif size == 2: wdg_name = "popup_350" else: assert False, "size should be 0 <= size <= 2, but is " + str(size) # NOTE: reusing popup dialogs can sometimes lead to exit(0) being called. # it is yet unknown why this happens, so let's be safe for now and reload the widgets. self.widgets.reload(wdg_name) popup = self.widgets[wdg_name] if not show_cancel_button: cancel_button = popup.findChild(name=CancelButton.DEFAULT_NAME) cancel_button.parent.removeChild(cancel_button) popup.headline = popup.findChild(name='headline') popup.headline.text = _(windowtitle) popup.message = popup.findChild(name='popup_message') popup.message.text = _(message) popup.adaptLayout() # recalculate widths return popup def show_modal_background(self): """ Loads transparent background that de facto prohibits access to other gui elements by eating all input events. Used for modal popups and our in-game menu. """ height = horizons.globals.fife.engine_settings.getScreenHeight() width = horizons.globals.fife.engine_settings.getScreenWidth() image = horizons.globals.fife.imagemanager.loadBlank(width, height) image = fife.GuiImage(image) self.additional_widget = pychan.Icon(image=image) self.additional_widget.position = (0, 0) self.additional_widget.show() def hide_modal_background(self): try: self.additional_widget.hide() del self.additional_widget except AttributeError: pass # only used for some widgets, e.g. pause def show_loading_screen(self): self._switch_current_widget('loadingscreen', center=True, show=True) # Add 'Quote of the Load' to loading screen: qotl_type_label = self.current.findChild(name='qotl_type_label') qotl_label = self.current.findChild(name='qotl_label') quote_type = int(horizons.globals.fife.get_uh_setting("QuotesType")) if quote_type == 2: quote_type = random.randint(0, 1) # choose a random type if quote_type == 0: name = GAMEPLAY_TIPS["name"] items = GAMEPLAY_TIPS["items"] elif quote_type == 1: name = FUN_QUOTES["name"] items = FUN_QUOTES["items"] qotl_type_label.text = unicode(name) qotl_label.text = unicode( random.choice(items)) # choose a random quote / gameplay tip # helper def _switch_current_widget(self, new_widget, center=False, event_map=None, show=False, hide_old=False): """Switches self.current to a new widget. @param new_widget: str, widget name @param center: bool, whether to center the new widget @param event_map: pychan event map to apply to new widget @param show: bool, if True old window gets hidden and new one shown @param hide_old: bool, if True old window gets hidden. Implied by show @return: instance of old widget""" old = self.current if (show or hide_old) and old is not None: self.log.debug("Gui: hiding %s", old) self.hide() self.log.debug("Gui: setting current to %s", new_widget) self.current = self.widgets[new_widget] bg = self.current.findChild(name='background') if bg: # Set background image bg.image = self._background_image if center: self.current.position_technique = "automatic" # == "center:center" if event_map: self.current.mapEvents(event_map) if show: self.current.show() return old @staticmethod def _create_show_savegame_details(gui, map_files, savegamelist): """Creates a function that displays details of a savegame in gui""" def tmp_show_details(): """Fetches details of selected savegame and displays it""" gui.findChild(name="screenshot").image = None map_file = None map_file_index = gui.collectData(savegamelist) if map_file_index == -1: gui.findChild(name="savegame_details").hide() return else: gui.findChild(name="savegame_details").show() try: map_file = map_files[map_file_index] except IndexError: # this was a click in the savegame list, but not on an element # it happens when the savegame list is empty return savegame_info = SavegameManager.get_metadata(map_file) if savegame_info.get('screenshot'): # try to find a writable location, that is accessible via relative paths # (required by fife) fd, filename = tempfile.mkstemp() try: path_rel = os.path.relpath(filename) except ValueError: # the relative path sometimes doesn't exist on win os.close(fd) os.unlink(filename) # try again in the current dir, it's often writable fd, filename = tempfile.mkstemp(dir=os.curdir) try: path_rel = os.path.relpath(filename) except ValueError: fd, filename = None, None if fd: with os.fdopen(fd, "w") as f: f.write(savegame_info['screenshot']) # fife only supports relative paths gui.findChild(name="screenshot").image = path_rel os.unlink(filename) # savegamedetails details_label = gui.findChild(name="savegamedetails_lbl") details_label.text = u"" if savegame_info['timestamp'] == -1: details_label.text += _("Unknown savedate") else: savetime = time.strftime( "%c", time.localtime(savegame_info['timestamp'])) #xgettext:python-format details_label.text += _("Saved at {time}").format( time=savetime.decode('utf-8')) details_label.text += u'\n' counter = savegame_info['savecounter'] # N_ takes care of plural forms for different languages #xgettext:python-format details_label.text += N_("Saved {amount} time", "Saved {amount} times", counter).format(amount=counter) details_label.text += u'\n' details_label.stylize('book') from horizons.constants import VERSION try: #xgettext:python-format details_label.text += _("Savegame version {version}").format( version=savegame_info['savegamerev']) if savegame_info['savegamerev'] != VERSION.SAVEGAMEREVISION: if not SavegameUpgrader.can_upgrade( savegame_info['savegamerev']): details_label.text += u" " + _( "(probably incompatible)") except KeyError: # this should only happen for very old savegames, so having this unfriendly # error is ok (savegame is quite certainly fully unusable). details_label.text += u" " + _("Incompatible version") gui.adaptLayout() return tmp_show_details def _delete_savegame(self, map_files): """Deletes the selected savegame if the user confirms self.current has to contain the widget "savegamelist" @param map_files: list of files that corresponds to the entries of 'savegamelist' @return: True if something was deleted, else False """ selected_item = self.current.collectData("savegamelist") if selected_item == -1 or selected_item >= len(map_files): self.show_popup(_("No file selected"), _("You need to select a savegame to delete.")) return False selected_file = map_files[selected_item] #xgettext:python-format message = _( "Do you really want to delete the savegame '{name}'?").format( name=SavegameManager.get_savegamename_from_filename( selected_file)) if self.show_popup(_("Confirm deletion"), message, show_cancel_button=True): try: os.unlink(selected_file) return True except: self.show_popup(_("Error!"), _("Failed to delete savefile!")) return False else: # player cancelled deletion return False def get_random_background_by_button(self): """Randomly select a background image to use. This function is triggered by change background button from main menu.""" #we need to redraw screen to apply changes. self.hide() self._background_image = self._get_random_background() self.show_main() def _get_random_background(self): """Randomly select a background image to use through out the game menu.""" available_images = glob.glob( 'content/gui/images/background/mainmenu/bg_*.png') #get latest background latest_background = horizons.globals.fife.get_uh_setting( "LatestBackground") #if there is a latest background then remove it from available list if latest_background is not None: available_images.remove(latest_background) background_choice = random.choice(available_images) #save current background choice horizons.globals.fife.set_uh_setting("LatestBackground", background_choice) horizons.globals.fife.save_settings() return background_choice def _on_gui_action(self, msg): AmbientSoundComponent.play_special('click') def editor_load_map(self): """Show a dialog for the user to select a map to edit.""" old_current = self._switch_current_widget('editor_select_map') self.current.show() map_files, map_file_display = SavegameManager.get_maps() self.current.distributeInitialData({'maplist': map_file_display}) bind = { OkButton.DEFAULT_NAME: True, CancelButton.DEFAULT_NAME: False, } retval = self.show_dialog(self.current, bind) if not retval: # Dialog cancelled self.current = old_current return False selected_map_index = self.current.collectData('maplist') if selected_map_index == -1: # No map selected yet => select first available one self.current.distributeData({'maplist': 0}) self.current = old_current self.show_loading_screen() horizons.main.edit_map(map_files[selected_map_index]) return True def show_single(self): self.singleplayermenu.show_single() def show_multi(self): self.multiplayermenu.show_multi()