class HelpDialog(object): def __init__(self, mainmenu): self.mainmenu = mainmenu self.widgets = mainmenu.widgets #i18n this defines how each line in our help looks like. Default: '[C] = Chat' self.HELPSTRING_LAYOUT = _('[{key}] = {text}') #xgettext:python-format self.keyconf = KeyConfig() # before _build_strings self._build_strings() self._is_displayed = False LanguageChanged.subscribe(lambda msg: self._build_strings()) def _build_strings(self): """ Loads the help strings from pychan object widgets (containing no key definitions) and adds the keys defined in the keyconfig configuration object in front of them. The layout is defined through HELPSTRING_LAYOUT and translated. """ # retranslate the layout self.HELPSTRING_LAYOUT = _('[{key}] = {text}') #xgettext:python-format widgets = self.widgets['help'] labels = widgets.getNamedChildren() # filter misc labels that do not describe key functions labels = dict((name[4:], lbl[0]) for (name, lbl) in labels.iteritems() if name.startswith('lbl_')) # now prepend the actual keys to the function strings defined in xml actionmap = self.keyconf.get_actionname_to_keyname_map() for (name, lbl) in labels.items(): keyname = actionmap.get(name, 'SHIFT') #TODO #HACK hardcoded shift key lbl.explanation = _(lbl.text) lbl.text = self.HELPSTRING_LAYOUT.format(text=lbl.explanation, key=keyname) lbl.capture( Callback(self.show_hotkey_change_popup, name, lbl, keyname)) def show_hotkey_change_popup(self, action, lbl, keyname): def apply_new_key(newkey=None): if not newkey: newkey = free_keys[listbox.selected] else: listbox.selected = listbox.items.index(newkey) self.keyconf.save_new_key(action, newkey=newkey) update_hotkey_info(action, newkey) lbl.text = self.HELPSTRING_LAYOUT.format(text=lbl.explanation, key=newkey) lbl.capture( Callback(self.show_hotkey_change_popup, action, lbl, newkey)) lbl.adaptLayout() def update_hotkey_info(action, keyname): default = self.keyconf.get_default_key_for_action(action) popup.message.text = ( lbl.explanation + #xgettext:python-format u'\n' + _('Current key: [{key}]').format(key=keyname) + #xgettext:python-format u'\t' + _('Default key: [{key}]').format(key=default)) popup.message.helptext = _('Click to reset to default key') reset_to_default = Callback(apply_new_key, default) popup.message.capture(reset_to_default) #xgettext:python-format headline = _('Change hotkey for {action}').format(action=action) message = '' if keyname in ('SHIFT', 'ESCAPE'): message = _('This key can not be reassigned at the moment.') self.mainmenu.show_popup(headline, message, {OkButton.DEFAULT_NAME: True}) return popup = self.mainmenu.build_popup(headline, message, size=2, show_cancel_button=True) update_hotkey_info(action, keyname) keybox = ScrollArea() listbox = ListBox(is_focusable=False, name="available_keys") keybox.max_size = listbox.max_size = \ keybox.min_size = listbox.min_size = \ keybox.size = listbox.size = (200, 200) keybox.position = listbox.position = (90, 110) prefer_short = lambda k: (len(k) > 1, len(k) > 3, k) is_valid, default_key = self.keyconf.is_valid_and_get_default_key( keyname, action) if not is_valid: headline = _('Invalid key') message = _('The default key for this action has been selected.') self.mainmenu.show_popup(headline, message, {OkButton.DEFAULT_NAME: True}) valid_key = keyname if is_valid else default_key free_key_dict = self.keyconf.get_keys_by_name( only_free_keys=True, force_include=[valid_key]) free_keys = sorted(free_key_dict.keys(), key=prefer_short) listbox.items = free_keys listbox.selected = listbox.items.index(valid_key) #TODO backwards replace key names in keyconfig.get_fife_key_names in the list # (currently this stores PLUS and PERIOD instead of + and . in the settings) keybox.addChild(listbox) popup.addChild(keybox) if not is_valid: apply_new_key() listbox.capture(apply_new_key) button_cbs = { OkButton.DEFAULT_NAME: True, CancelButton.DEFAULT_NAME: False } self.mainmenu.show_dialog(popup, button_cbs, modal=True) def toggle(self): """Called on help action. Toggles help screen via static variable *help_is_displayed*. Can be called both from main menu and in-game interface. """ help_dlg = self.widgets['help'] if not self._is_displayed: self._is_displayed = True # make game pause if there is a game and we're not in the main menu if self.mainmenu.session is not None and self.mainmenu.current != self.widgets[ 'ingamemenu']: PauseCommand().execute(self.mainmenu.session) if self.mainmenu.session is not None: self.mainmenu.session.ingame_gui.on_escape( ) # close dialogs that might be open self.mainmenu.show_dialog(help_dlg, {OkButton.DEFAULT_NAME: True}) self.toggle() # toggle state else: self._is_displayed = False if self.mainmenu.session is not None and self.mainmenu.current != self.widgets[ 'ingamemenu']: UnPauseCommand().execute(self.mainmenu.session) help_dlg.hide()
class HelpDialog(object): def __init__(self, mainmenu): self.mainmenu = mainmenu self.widgets = mainmenu.widgets #i18n this defines how each line in our help looks like. Default: '[C] = Chat' self.HELPSTRING_LAYOUT = _('[{key}] = {text}') #xgettext:python-format self.keyconf = KeyConfig() # before _build_strings self._build_strings() self._is_displayed = False LanguageChanged.subscribe(lambda msg: self._build_strings()) def _build_strings(self): """ Loads the help strings from pychan object widgets (containing no key definitions) and adds the keys defined in the keyconfig configuration object in front of them. The layout is defined through HELPSTRING_LAYOUT and translated. """ # retranslate the layout self.HELPSTRING_LAYOUT = _('[{key}] = {text}') #xgettext:python-format widgets = self.widgets['help'] labels = widgets.getNamedChildren() # filter misc labels that do not describe key functions labels = dict( (name[4:], lbl[0]) for (name, lbl) in labels.iteritems() if name.startswith('lbl_') ) # now prepend the actual keys to the function strings defined in xml actionmap = self.keyconf.get_actionname_to_keyname_map() for (name, lbl) in labels.items(): keyname = actionmap.get(name, 'SHIFT') #TODO #HACK hardcoded shift key lbl.explanation = _(lbl.text) lbl.text = self.HELPSTRING_LAYOUT.format(text=lbl.explanation, key=keyname) lbl.capture(Callback(self.show_hotkey_change_popup, name, lbl, keyname)) def show_hotkey_change_popup(self, action, lbl, keyname): def apply_new_key(newkey=None): if not newkey: newkey = free_keys[listbox.selected] else: listbox.selected = listbox.items.index(newkey) self.keyconf.save_new_key(action, newkey=newkey) update_hotkey_info(action, newkey) lbl.text = self.HELPSTRING_LAYOUT.format(text=lbl.explanation, key=newkey) lbl.capture(Callback(self.show_hotkey_change_popup, action, lbl, newkey)) lbl.adaptLayout() def update_hotkey_info(action, keyname): default = self.keyconf.get_default_key_for_action(action) popup.message.text = (lbl.explanation + #xgettext:python-format u'\n' + _('Current key: [{key}]').format(key=keyname) + #xgettext:python-format u'\t' + _('Default key: [{key}]').format(key=default)) popup.message.helptext = _('Click to reset to default key') reset_to_default = Callback(apply_new_key, default) popup.message.capture(reset_to_default) #xgettext:python-format headline = _('Change hotkey for {action}').format(action=action) message = '' if keyname in ('SHIFT', 'ESCAPE'): message = _('This key can not be reassigned at the moment.') self.mainmenu.show_popup(headline, message, {OkButton.DEFAULT_NAME: True}) return popup = self.mainmenu.build_popup(headline, message, size=2, show_cancel_button=True) update_hotkey_info(action, keyname) keybox = ScrollArea() listbox = ListBox(is_focusable=False, name="available_keys") keybox.max_size = listbox.max_size = \ keybox.min_size = listbox.min_size = \ keybox.size = listbox.size = (200, 200) keybox.position = listbox.position = (90, 110) prefer_short = lambda k: (len(k) > 1, len(k) > 3, k) is_valid, default_key = self.keyconf.is_valid_and_get_default_key(keyname, action) if not is_valid: headline = _('Invalid key') message = _('The default key for this action has been selected.') self.mainmenu.show_popup(headline, message, {OkButton.DEFAULT_NAME: True}) valid_key = keyname if is_valid else default_key free_key_dict = self.keyconf.get_keys_by_name(only_free_keys=True, force_include=[valid_key]) free_keys = sorted(free_key_dict.keys(), key=prefer_short) listbox.items = free_keys listbox.selected = listbox.items.index(valid_key) #TODO backwards replace key names in keyconfig.get_fife_key_names in the list # (currently this stores PLUS and PERIOD instead of + and . in the settings) keybox.addChild(listbox) popup.addChild(keybox) if not is_valid: apply_new_key() listbox.capture(apply_new_key) button_cbs = {OkButton.DEFAULT_NAME: True, CancelButton.DEFAULT_NAME: False} self.mainmenu.show_dialog(popup, button_cbs, modal=True) def toggle(self): """Called on help action. Toggles help screen via static variable *help_is_displayed*. Can be called both from main menu and in-game interface. """ help_dlg = self.widgets['help'] if not self._is_displayed: self._is_displayed = True # make game pause if there is a game and we're not in the main menu if self.mainmenu.session is not None and self.mainmenu.current != self.widgets['ingamemenu']: PauseCommand().execute(self.mainmenu.session) if self.mainmenu.session is not None: self.mainmenu.session.ingame_gui.on_escape() # close dialogs that might be open self.mainmenu.show_dialog(help_dlg, {OkButton.DEFAULT_NAME : True}) self.toggle() # toggle state else: self._is_displayed = False if self.mainmenu.session is not None and self.mainmenu.current != self.widgets['ingamemenu']: UnPauseCommand().execute(self.mainmenu.session) help_dlg.hide()
class Gui(SingleplayerMenu, MultiplayerMenu): """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', } def __init__(self): #i18n this defines how each line in our help looks like. Default: '[C] = Chat' self.HELPSTRING_LAYOUT = _('[{key}] = {text}') #xgettext:python-format self.mainlistener = MainListener(self) self.current = None # currently active window self.widgets = LazyWidgetsDict(self.styles) # access widgets with their filenames without '.xml' self.keyconf = KeyConfig() # before build_help_strings self.build_help_strings() self.session = None self.current_dialog = None self.dialog_executed = False self.__pause_displayed = False self._background_image = self._get_random_background() self.subscribe() 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 self.widgets.reload('ingamemenu') def do_load(): did_load = self.load_game() if did_load: 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, 'e_save' : 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('ingamemenu', 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() _help_is_displayed = False def on_help(self): """Called on help action. Toggles help screen via static variable *help_is_displayed*. Can be called both from main menu and in-game interface. """ help_dlg = self.widgets['help'] if not self._help_is_displayed: self._help_is_displayed = True # make game pause if there is a game and we're not in the main menu if self.session is not None and self.current != self.widgets['ingamemenu']: PauseCommand().execute(self.session) if self.session is not None: self.session.ingame_gui.on_escape() # close dialogs that might be open self.show_dialog(help_dlg, {OkButton.DEFAULT_NAME : True}) self.on_help() # toggle state else: self._help_is_displayed = False if self.session is not None and self.current != self.widgets['ingamemenu']: UnPauseCommand().execute(self.session) help_dlg.hide() 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 build_help_strings(self): """ Loads the help strings from pychan object widgets (containing no key definitions) and adds the keys defined in the keyconfig configuration object in front of them. The layout is defined through HELPSTRING_LAYOUT and translated. """ widgets = self.widgets['help'] labels = widgets.getNamedChildren() # filter misc labels that do not describe key functions labels = dict( (name[4:], lbl[0]) for (name, lbl) in labels.iteritems() if name.startswith('lbl_') ) # now prepend the actual keys to the function strings defined in xml actionmap = self.keyconf.get_actionname_to_keyname_map() for (name, lbl) in labels.items(): keyname = actionmap.get(name, 'SHIFT') #TODO #HACK hardcoded shift key lbl.explanation = _(lbl.text) lbl.text = self.HELPSTRING_LAYOUT.format(text=lbl.explanation, key=keyname) lbl.capture(Callback(self.show_hotkey_change_popup, name, lbl, keyname)) def show_hotkey_change_popup(self, action, lbl, keyname): def apply_new_key(newkey=None): if not newkey: newkey = free_keys[listbox.selected] else: listbox.selected = listbox.items.index(newkey) self.keyconf.save_new_key(action, newkey=newkey) update_hotkey_info(action, newkey) lbl.text = self.HELPSTRING_LAYOUT.format(text=lbl.explanation, key=newkey) lbl.capture(Callback(self.show_hotkey_change_popup, action, lbl, newkey)) lbl.adaptLayout() def update_hotkey_info(action, keyname): default = self.keyconf.get_default_key_for_action(action) popup.message.text = (lbl.explanation + #xgettext:python-format u'\n' + _('Current key: [{key}]').format(key=keyname) + #xgettext:python-format u'\t' + _('Default key: [{key}]').format(key=default)) popup.message.helptext = _('Click to reset to default key') reset_to_default = Callback(apply_new_key, default) popup.message.capture(reset_to_default) #xgettext:python-format headline = _('Change hotkey for {action}').format(action=action) message = '' if keyname in ('SHIFT', 'ESCAPE'): message = _('This key can not be reassigned at the moment.') self.show_popup(headline, message, {OkButton.DEFAULT_NAME: True}) return popup = self.build_popup(headline, message, size=2, show_cancel_button=True) update_hotkey_info(action, keyname) keybox = pychan.widgets.ScrollArea() listbox = pychan.widgets.ListBox(is_focusable=False) keybox.max_size = listbox.max_size = \ keybox.min_size = listbox.min_size = \ keybox.size = listbox.size = (200, 200) keybox.position = listbox.position = (90, 110) prefer_short = lambda k: (len(k) > 1, len(k) > 3, k) is_valid, default_key = self.keyconf.is_valid_and_get_default_key(keyname, action) if not is_valid: headline = _('Invalid key') message = _('The default key for this action has been selected.') self.show_popup(headline, message, {OkButton.DEFAULT_NAME: True}) valid_key = keyname if is_valid else default_key free_key_dict = self.keyconf.get_keys_by_name(only_free_keys=True, force_include=[valid_key]) free_keys = sorted(free_key_dict.keys(), key=prefer_short) listbox.items = free_keys listbox.selected = listbox.items.index(valid_key) #TODO backwards replace key names in keyconfig.get_fife_key_names in the list # (currently this stores PLUS and PERIOD instead of + and . in the settings) keybox.addChild(listbox) popup.addChild(keybox) if not is_valid: apply_new_key() listbox.capture(apply_new_key) button_cbs = {OkButton.DEFAULT_NAME: True, CancelButton.DEFAULT_NAME: False} self.show_dialog(popup, button_cbs, modal=True) 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 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_file_display[selected_map_index])
class Gui(SingleplayerMenu, MultiplayerMenu): """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 = defaultdict(lambda: 'book') styles.update({ 'mainmenu': 'menu', 'ingamemenu': 'headline', }) def __init__(self): #i18n this defines how each line in our help looks like. Default: '[C] = Chat' self.HELPSTRING_LAYOUT = _('[{key}] = {text}') #xgettext:python-format self.mainlistener = MainListener(self) self.current = None # currently active window self.widgets = LazyWidgetsDict(self.styles) # access widgets with their filenames without '.xml' self.keyconf = KeyConfig() # before build_help_strings self.build_help_strings() self.session = None self.current_dialog = None self.dialog_executed = False self.__pause_displayed = False self._background_image = self._get_random_background() self.subscribe() 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 self.widgets.reload('ingamemenu') def do_load(): did_load = self.load_game() if did_load: 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, 'e_save' : 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('ingamemenu', 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() _help_is_displayed = False def on_help(self): """Called on help action. Toggles help screen via static variable *help_is_displayed*. Can be called both from main menu and in-game interface. """ help_dlg = self.widgets['help'] if not self._help_is_displayed: self._help_is_displayed = True # make game pause if there is a game and we're not in the main menu if self.session is not None and self.current != self.widgets['ingamemenu']: PauseCommand().execute(self.session) if self.session is not None: self.session.ingame_gui.on_escape() # close dialogs that might be open self.show_dialog(help_dlg, {OkButton.DEFAULT_NAME : True}) self.on_help() # toggle state else: self._help_is_displayed = False if self.session is not None and self.current != self.widgets['ingamemenu']: UnPauseCommand().execute(self.session) help_dlg.hide() 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 build_help_strings(self): """ Loads the help strings from pychan object widgets (containing no key definitions) and adds the keys defined in the keyconfig configuration object in front of them. The layout is defined through HELPSTRING_LAYOUT and translated. """ widgets = self.widgets['help'] labels = widgets.getNamedChildren() # filter misc labels that do not describe key functions labels = dict( (name[4:], lbl[0]) for (name, lbl) in labels.iteritems() if name.startswith('lbl_') ) # now prepend the actual keys to the function strings defined in xml actionmap = self.keyconf.get_actionname_to_keyname_map() for (name, lbl) in labels.items(): keyname = actionmap.get(name, 'SHIFT') #TODO #HACK hardcoded shift key lbl.explanation = _(lbl.text) lbl.text = self.HELPSTRING_LAYOUT.format(text=lbl.explanation, key=keyname) lbl.capture(Callback(self.show_hotkey_change_popup, name, lbl, keyname)) def show_hotkey_change_popup(self, action, lbl, keyname): def apply_new_key(newkey=None): if not newkey: newkey = free_keys[listbox.selected] else: listbox.selected = listbox.items.index(newkey) self.keyconf.save_new_key(action, newkey=newkey) update_hotkey_info(action, newkey) lbl.text = self.HELPSTRING_LAYOUT.format(text=lbl.explanation, key=newkey) lbl.capture(Callback(self.show_hotkey_change_popup, action, lbl, newkey)) lbl.adaptLayout() def update_hotkey_info(action, keyname): default = self.keyconf.get_default_key_for_action(action) popup.message.text = (lbl.explanation + #xgettext:python-format u'\n' + _('Current key: [{key}]').format(key=keyname) + #xgettext:python-format u'\t' + _('Default key: [{key}]').format(key=default)) popup.message.helptext = _('Click to reset to default key') reset_to_default = Callback(apply_new_key, default) popup.message.capture(reset_to_default) #xgettext:python-format headline = _('Change hotkey for {action}').format(action=action) message = '' if keyname in ('SHIFT', 'ESCAPE'): message = _('This key can not be reassigned at the moment.') self.show_popup(headline, message, {OkButton.DEFAULT_NAME: True}) return popup = self.build_popup(headline, message, size=2, show_cancel_button=True) update_hotkey_info(action, keyname) keybox = pychan.widgets.ScrollArea() listbox = pychan.widgets.ListBox(is_focusable=False) keybox.max_size = listbox.max_size = \ keybox.min_size = listbox.min_size = \ keybox.size = listbox.size = (200, 200) keybox.position = listbox.position = (90, 110) prefer_short = lambda k: (len(k) > 1, len(k) > 3, k) is_valid, default_key = self.keyconf.is_valid_and_get_default_key(keyname, action) if not is_valid: headline = _('Invalid key') message = _('The default key for this action has been selected.') self.show_popup(headline, message, {OkButton.DEFAULT_NAME: True}) valid_key = keyname if is_valid else default_key free_key_dict = self.keyconf.get_keys_by_name(only_free_keys=True, force_include=[valid_key]) free_keys = sorted(free_key_dict.keys(), key=prefer_short) listbox.items = free_keys listbox.selected = listbox.items.index(valid_key) #TODO backwards replace key names in keyconfig.get_fife_key_names in the list # (currently this stores PLUS and PERIOD instead of + and . in the settings) keybox.addChild(listbox) popup.addChild(keybox) if not is_valid: apply_new_key() listbox.capture(apply_new_key) button_cbs = {OkButton.DEFAULT_NAME: True, CancelButton.DEFAULT_NAME: False} self.show_dialog(popup, button_cbs, modal=True) 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 selected_map_index = self.current.collectData('maplist') assert selected_map_index != -1, "No map selected" self.current = old_current self.show_loading_screen() horizons.main.edit_map(map_file_display[selected_map_index])