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 __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' build_help_strings(self.widgets['help']) self.session = None self.current_dialog = None self.dialog_executed = False self.__pause_displayed = False self._background_image = self._get_random_background()
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)
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_campaign': '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', } 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' build_help_strings(self.widgets['help']) self.session = None self.current_dialog = None self.dialog_executed = False self.__pause_displayed = False self._background_image = self._get_random_background() GuiAction.subscribe( 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 lable 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, 'closeButton' : self.show_quit, 'quit' : self.show_quit, 'dead_link' : self.on_chime, # call for help; SoC information 'chimebell' : self.on_chime, 'creditsLink' : self.show_credits, 'credits' : self.show_credits, 'loadgameButton' : horizons.main.load_game, 'loadgame' : horizons.main.load_game, 'changeBackground' : self.get_random_background_by_button }) self.on_escape = self.show_quit def toggle_pause(self): """ Show Pause menu """ # 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 = horizons.main.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'], }) # load transparent background, that de facto prohibits access to other # gui elements by eating all events height = horizons.main.fife.engine_settings.getScreenHeight() width = horizons.main.fife.engine_settings.getScreenWidth() image = horizons.main.fife.imagemanager.loadBlank(width, height) image = fife.GuiImage(image) self.current.additional_widget = pychan.Icon(image=image) self.current.additional_widget.position = (0, 0) self.current.additional_widget.show() 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): horizons.main.fife.show_settings() _help_is_displayed = False def on_help(self): """Called on help action Toggles help screen via static variable help_is_displayed""" 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 """ 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. @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 on_chime(self): """ Called chime action. Displaying call for help on artists and game design, introduces information for SoC applicants (if valid). """ AmbientSoundComponent.play_special("message") self.show_dialog(self.widgets['call_for_support'], {OkButton.DEFAULT_NAME : True}) def show_credits(self, number=0): """Shows the credits dialog. """ for box in self.widgets['credits'+str(number)].findChildren(name='box'): box.margins = (30, 0) # to get some indentation if number in [2, 0]: # #TODO fix this hardcoded translators page ref box.padding = 1 # further decrease if more entries box.parent.padding = 3 # see above label = [self.widgets['credits'+str(number)].findChild(name=section+"_lbl") \ for section in ('team','patchers','translators','packagers','special_thanks')] for i in xrange(5): if label[i]: # add callbacks to each pickbelt that is displayed label[i].capture(Callback(self.show_credits, i), event_name="mouseClicked") if self.current_dialog is not None: self.current_dialog.hide() self.show_dialog(self.widgets['credits'+str(number)], {OkButton.DEFAULT_NAME : True}) def show_select_savegame(self, mode, sanity_checker=None, sanity_criteria=None): """Shows menu to select a savegame. @param mode: 'save', 'load' or 'mp_load' @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 mp = False args = mode, sanity_checker, sanity_criteria # for reshow if mode.startswith('mp'): mode = mode[3:] mp = True # 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 len(map_files) == 0: 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') self.current.findChild(name='headline').text = _('Save game') if mode == 'save' else _('Load game') self.current.findChild(name=OkButton.DEFAULT_NAME).helptext = _('Save game') if mode == 'save' else _('Load game') 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._getParent() 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 if self.current.collectData('savegamelist') == -1: # set blank if nothing is selected self.current.findChild(name="savegamefile").text = u"" else: self.current.distributeData({'savegamefile' : \ map_file_display[self.current.collectData('savegamelist')]}) self.current.distributeInitialData({'savegamelist' : map_file_display}) self.current.distributeData({'savegamelist' : -1}) # Don't select anything by default cb = Callback.ChainedCallbacks(Gui._create_show_savegame_details(self.current, map_files, 'savegamelist'), \ tmp_selected_changed) cb() # Refresh data on start self.current.findChild(name="savegamelist").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?') if not self.show_popup(_("Confirmation for overwriting"), message, show_cancel_button=True): 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') selected_savegame = None if selected_savegame == -1 else map_files[selected_savegame] if selected_savegame is None: # ok button has been pressed, but no savegame was selected self.show_popup(_("Select a savegame"), _("Please select a savegame or click on cancel.")) self.current = old_current return self.show_select_savegame(*args) # reshow dialog 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() try: self.current.additional_widget.hide() del self.current.additional_widget except AttributeError: pass # only used for some widgets, e.g. pause def is_visible(self): return self.current is not None and self.current.isVisible() def show_dialog(self, dlg, bind, event_map=None): """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() """ self.current_dialog = dlg if event_map is not None: dlg.mapEvents(event_map) # 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 pychan.internal.get_manager().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 return ret def show_popup(self, windowtitle, message, show_cancel_button=False, size=0): """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. @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 ExtScheduler().add_new_object(lambda : popup.findChild(name=OkButton.DEFAULT_NAME).requestFocus(), self, run_in=0) if show_cancel_button: return self.show_dialog(popup,{OkButton.DEFAULT_NAME : True, CancelButton.DEFAULT_NAME : False}) else: return self.show_dialog(popup, {OkButton.DEFAULT_NAME : True}) 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 += _(u"Details:") + u" " + details try: self.show_popup( _(u"Error:") + u" " + 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) headline = popup.findChild(name='headline') # just to be safe, the gettext-function is used twice, # once on the original, once on the unicode string. headline.text = _(_(windowtitle)) popup.findChild(name='popup_message').text = _(_(message)) popup.adaptLayout() # recalculate widths return popup def show_loading_screen(self): self._switch_current_widget('loadingscreen', center=True, show=True) # 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] # Set background image bg = self.current.findChild(name='background') if bg: 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 box = gui.findChild(name="savegamedetails_box") old_label = box.findChild(name="savegamedetails_lbl") if old_label is not None: box.removeChild(old_label) map_file = None map_file_index = gui.collectData(savegamelist) if map_file_index == -1: return 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) # screenshot (len can be 0 if save failed in a weird way) if 'screenshot' in savegame_info and \ savegame_info['screenshot'] is not None and \ len(savegame_info['screenshot']) > 0: # try to find a writeable 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 = pychan.widgets.Label(min_size=(290, 0), max_size=(290, 290), wrap_text=True) details_label.name = "savegamedetails_lbl" details_label.text = u"" if savegame_info['timestamp'] == -1: details_label.text += _("Unknown savedate") else: #xgettext:python-format details_label.text += _("Saved at {time}").format( time=time.strftime("%c", time.localtime(savegame_info['timestamp'])).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_t') 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: #xgettext:python-format details_label.text += u" " + _("(potentially 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 += _("Incompatible version") box.addChild( details_label ) 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.main.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.main.fife.set_uh_setting("LatestBackground", background_choice) horizons.main.fife.save_settings() return background_choice def _on_gui_action(self, msg): AmbientSoundComponent.play_special('click')
def __init__(self, session, gui): super(IngameGui, self).__init__() self.session = session assert isinstance(self.session, horizons.session.Session) self.main_gui = gui self.main_widget = None self.tabwidgets = {} self.settlement = None self.resource_source = None self.resources_needed, self.resources_usable = {}, {} self._old_menu = None self.widgets = LazyWidgetsDict(self.styles, center_widgets=False) self.cityinfo = self.widgets['city_info'] self.cityinfo.child_finder = PychanChildFinder(self.cityinfo) self.logbook = LogBook(self.session) self.message_widget = MessageWidget(self.session) self.players_overview = PlayersOverview(self.session) self.players_settlements = PlayersSettlements(self.session) self.players_ships = PlayersShips(self.session) # self.widgets['minimap'] is the guichan gui around the actual minimap, # which is saved in self.minimap minimap = self.widgets['minimap'] minimap.position_technique = "right+0:top+0" icon = minimap.findChild(name="minimap") self.minimap = Minimap(icon, targetrenderer=horizons.globals.fife.targetrenderer, imagemanager=horizons.globals.fife.imagemanager, session=self.session, view=self.session.view) def speed_up(): SpeedUpCommand().execute(self.session) def speed_down(): SpeedDownCommand().execute(self.session) minimap.mapEvents({ 'zoomIn' : self.session.view.zoom_in, 'zoomOut' : self.session.view.zoom_out, 'rotateRight' : Callback.ChainedCallbacks(self.session.view.rotate_right, self.minimap.rotate_right), 'rotateLeft' : Callback.ChainedCallbacks(self.session.view.rotate_left, self.minimap.rotate_left), 'speedUp' : speed_up, 'speedDown' : speed_down, 'destroy_tool' : self.session.toggle_destroy_tool, 'build' : self.show_build_menu, 'diplomacyButton' : self.show_diplomacy_menu, 'gameMenuButton' : self.main_gui.toggle_pause, 'logbook' : self.logbook.toggle_visibility }) minimap.show() #minimap.position_technique = "right+15:top+153" self.widgets['tooltip'].hide() self.resource_overview = ResourceOverviewBar(self.session) ResourceBarResize.subscribe(self._on_resourcebar_resize) # Register for messages SettlerUpdate.subscribe(self._on_settler_level_change) SettlerInhabitantsChanged.subscribe(self._on_settler_inhabitant_change) HoverSettlementChanged.subscribe(self._cityinfo_set)
class IngameGui(LivingObject): """Class handling all the ingame gui events. Assumes that only 1 instance is used (class variables)""" gui = livingProperty() tabwidgets = livingProperty() message_widget = livingProperty() minimap = livingProperty() styles = { 'city_info' : 'resource_bar', 'change_name' : 'book', 'save_map' : 'book', 'chat' : 'book', } def __init__(self, session, gui): super(IngameGui, self).__init__() self.session = session assert isinstance(self.session, horizons.session.Session) self.main_gui = gui self.main_widget = None self.tabwidgets = {} self.settlement = None self.resource_source = None self.resources_needed, self.resources_usable = {}, {} self._old_menu = None self.widgets = LazyWidgetsDict(self.styles, center_widgets=False) self.cityinfo = self.widgets['city_info'] self.cityinfo.child_finder = PychanChildFinder(self.cityinfo) self.logbook = LogBook(self.session) self.message_widget = MessageWidget(self.session) self.players_overview = PlayersOverview(self.session) self.players_settlements = PlayersSettlements(self.session) self.players_ships = PlayersShips(self.session) # self.widgets['minimap'] is the guichan gui around the actual minimap, # which is saved in self.minimap minimap = self.widgets['minimap'] minimap.position_technique = "right+0:top+0" icon = minimap.findChild(name="minimap") self.minimap = Minimap(icon, targetrenderer=horizons.globals.fife.targetrenderer, imagemanager=horizons.globals.fife.imagemanager, session=self.session, view=self.session.view) def speed_up(): SpeedUpCommand().execute(self.session) def speed_down(): SpeedDownCommand().execute(self.session) minimap.mapEvents({ 'zoomIn' : self.session.view.zoom_in, 'zoomOut' : self.session.view.zoom_out, 'rotateRight' : Callback.ChainedCallbacks(self.session.view.rotate_right, self.minimap.rotate_right), 'rotateLeft' : Callback.ChainedCallbacks(self.session.view.rotate_left, self.minimap.rotate_left), 'speedUp' : speed_up, 'speedDown' : speed_down, 'destroy_tool' : self.session.toggle_destroy_tool, 'build' : self.show_build_menu, 'diplomacyButton' : self.show_diplomacy_menu, 'gameMenuButton' : self.main_gui.toggle_pause, 'logbook' : self.logbook.toggle_visibility }) minimap.show() #minimap.position_technique = "right+15:top+153" self.widgets['tooltip'].hide() self.resource_overview = ResourceOverviewBar(self.session) ResourceBarResize.subscribe(self._on_resourcebar_resize) # Register for messages SettlerUpdate.subscribe(self._on_settler_level_change) SettlerInhabitantsChanged.subscribe(self._on_settler_inhabitant_change) HoverSettlementChanged.subscribe(self._cityinfo_set) def _on_resourcebar_resize(self, message): self._update_cityinfo_position() def end(self): self.widgets['minimap'].mapEvents({ 'zoomIn' : None, 'zoomOut' : None, 'rotateRight' : None, 'rotateLeft': None, 'destroy_tool' : None, 'build' : None, 'diplomacyButton' : None, 'gameMenuButton' : None }) for w in self.widgets.itervalues(): if w.parent is None: w.hide() self.message_widget = None self.tabwidgets = None self.minimap = None self.resource_overview.end() self.resource_overview = None self.hide_menu() SettlerUpdate.unsubscribe(self._on_settler_level_change) ResourceBarResize.unsubscribe(self._on_resourcebar_resize) HoverSettlementChanged.unsubscribe(self._cityinfo_set) SettlerInhabitantsChanged.unsubscribe(self._on_settler_inhabitant_change) super(IngameGui, self).end() def _cityinfo_set(self, message): """Sets the city name at top center of screen. Show/Hide is handled automatically To hide cityname, set name to '' @param message: HoverSettlementChanged message """ settlement = message.settlement old_was_player_settlement = False if self.settlement is not None: self.settlement.remove_change_listener(self.update_settlement) old_was_player_settlement = self.settlement.owner.is_local_player # save reference to new "current" settlement in self.settlement self.settlement = settlement if settlement is None: # we want to hide the widget now (but perhaps delayed). if old_was_player_settlement: # After scrolling away from settlement, leave name on screen for some # seconds. Players can still click on it to rename the settlement now. ExtScheduler().add_new_object(self.cityinfo.hide, self, run_in=GUI.CITYINFO_UPDATE_DELAY) #TODO 'click to rename' tooltip of cityinfo can stay visible in # certain cases if cityinfo gets hidden in tooltip delay buffer. else: # hovered settlement of other player, simply hide the widget self.cityinfo.hide() else:# do not hide if settlement is hovered and a hide was previously scheduled ExtScheduler().rem_call(self, self.cityinfo.hide) self.update_settlement() # calls show() settlement.add_change_listener(self.update_settlement) def _on_settler_inhabitant_change(self, message): assert isinstance(message, SettlerInhabitantsChanged) foundlabel = self.cityinfo.child_finder('city_inhabitants') old_amount = int(foundlabel.text) if foundlabel.text else 0 foundlabel.text = u' {amount:>4d}'.format(amount=old_amount+message.change) foundlabel.resizeToContent() def update_settlement(self): city_name_label = self.cityinfo.child_finder('city_name') if self.settlement.owner.is_local_player: # allow name changes # Update settlement on the resource overview to make sure it # is setup correctly for the coming calculations self.resource_overview.set_inventory_instance(self.settlement) cb = Callback(self.show_change_name_dialog, self.settlement) helptext = _("Click to change the name of your settlement") city_name_label.enable_cursor_change_on_hover() else: # no name changes cb = lambda : AmbientSoundComponent.play_special('error') helptext = u"" city_name_label.disable_cursor_change_on_hover() self.cityinfo.mapEvents({ 'city_name': cb }) city_name_label.helptext = helptext foundlabel = self.cityinfo.child_finder('owner_emblem') foundlabel.image = 'content/gui/images/tabwidget/emblems/emblem_%s.png' % (self.settlement.owner.color.name) foundlabel.helptext = self.settlement.owner.name foundlabel = self.cityinfo.child_finder('city_name') foundlabel.text = self.settlement.get_component(SettlementNameComponent).name foundlabel.resizeToContent() foundlabel = self.cityinfo.child_finder('city_inhabitants') foundlabel.text = u' {amount:>4d}'.format(amount=self.settlement.inhabitants) foundlabel.resizeToContent() self._update_cityinfo_position() def _update_cityinfo_position(self): """ Places cityinfo widget depending on resource bar dimensions. For a normal-sized resource bar and reasonably large resolution: * determine resource bar length (includes gold) * determine empty horizontal space between resbar end and minimap start * display cityinfo centered in that area if it is sufficiently large If too close to the minimap (cityinfo larger than length of this empty space) move cityinfo centered to very upper screen edge. Looks bad, works usually. In this case, the resbar is redrawn to put the cityinfo "behind" it visually. """ width = horizons.globals.fife.engine_settings.getScreenWidth() resbar = self.resource_overview.get_size() is_foreign = (self.settlement.owner != self.session.world.player) blocked = self.cityinfo.size[0] + int(1.5*self.minimap.get_size()[1]) # minimap[1] returns width! Use 1.5*width because of the GUI around it if is_foreign: # other player, no resbar exists self.cityinfo.pos = ('center', 'top') xoff = 0 yoff = 19 elif blocked < width < resbar[0] + blocked: # large resbar / small resolution self.cityinfo.pos = ('center', 'top') xoff = 0 yoff = 0 # upper screen edge else: self.cityinfo.pos = ('left', 'top') xoff = resbar[0] + (width - blocked - resbar[0]) // 2 yoff = 24 self.cityinfo.offset = (xoff, yoff) self.cityinfo.position_technique = "{pos[0]}{off[0]:+d}:{pos[1]}{off[1]:+d}".format( pos=self.cityinfo.pos, off=self.cityinfo.offset) self.cityinfo.hide() self.cityinfo.show() def minimap_to_front(self): """Make sure the full right top gui is visible and not covered by some dialog""" self.widgets['minimap'].hide() self.widgets['minimap'].show() def show_diplomacy_menu(self): # check if the menu is already shown if getattr(self.get_cur_menu(), 'name', None) == "diplomacy_widget": self.hide_menu() return if not DiplomacyTab.is_useable(self.session.world): self.main_gui.show_popup(_("No diplomacy possible"), _("Cannot do diplomacy as there are no other players.")) return tab = DiplomacyTab(self, self.session.world) self.show_menu(tab) def show_multi_select_tab(self): tab = TabWidget(self, tabs=[SelectMultiTab(self.session)], name='select_multi') self.show_menu(tab) def show_build_menu(self, update=False): """ @param update: set when build possiblities change (e.g. after settler upgrade) """ # check if build menu is already shown if hasattr(self.get_cur_menu(), 'name') and self.get_cur_menu().name == "build_menu_tab_widget": self.hide_menu() if not update: # this was only a toggle call, don't reshow return self.session.set_cursor() # set default cursor for build menu self.deselect_all() if not any( settlement.owner.is_local_player for settlement in self.session.world.settlements): # player has not built any settlements yet. Accessing the build menu at such a point # indicates a mistake in the mental model of the user. Display a hint. tab = TabWidget(self, tabs=[ TabInterface(widget="buildtab_no_settlement.xml") ]) else: btabs = BuildTab.create_tabs(self.session, self._build) tab = TabWidget(self, tabs=btabs, name="build_menu_tab_widget", active_tab=BuildTab.last_active_build_tab) self.show_menu(tab) def deselect_all(self): for instance in self.session.selected_instances: instance.get_component(SelectableComponent).deselect() self.session.selected_instances.clear() def _build(self, building_id, unit=None): """Calls the games buildingtool class for the building_id. @param building_id: int with the building id that is to be built. @param unit: weakref to the unit, that builds (e.g. ship for warehouse)""" self.hide_menu() self.deselect_all() cls = Entities.buildings[building_id] if hasattr(cls, 'show_build_menu'): cls.show_build_menu() self.session.set_cursor('building', cls, None if unit is None else unit()) def toggle_road_tool(self): if not isinstance(self.session.cursor, BuildingTool) or self.session.cursor._class.id != BUILDINGS.TRAIL: self._build(BUILDINGS.TRAIL) else: self.session.set_cursor() def _get_menu_object(self, menu): """Returns pychan object if menu is a string, else returns menu @param menu: str with the guiname or pychan object. """ if isinstance(menu, str): menu = self.widgets[menu] return menu def get_cur_menu(self): """Returns menu that is currently displayed""" return self._old_menu def show_menu(self, menu): """Shows a menu @param menu: str with the guiname or pychan object. """ if self._old_menu is not None: if hasattr(self._old_menu, "remove_remove_listener"): self._old_menu.remove_remove_listener( Callback(self.show_menu, None) ) self._old_menu.hide() self._old_menu = self._get_menu_object(menu) if self._old_menu is not None: if hasattr(self._old_menu, "add_remove_listener"): self._old_menu.add_remove_listener( Callback(self.show_menu, None) ) self._old_menu.show() self.minimap_to_front() TabWidgetChanged.broadcast(self) def hide_menu(self): self.show_menu(None) def toggle_menu(self, menu): """Shows a menu or hides it if it is already displayed. @param menu: parameter supported by show_menu(). """ if self.get_cur_menu() == self._get_menu_object(menu): self.hide_menu() else: self.show_menu(menu) def save(self, db): self.message_widget.save(db) self.logbook.save(db) self.resource_overview.save(db) def load(self, db): self.message_widget.load(db) self.logbook.load(db) self.resource_overview.load(db) cur_settlement = LastActivePlayerSettlementManager().get_current_settlement() self._cityinfo_set( HoverSettlementChanged(self, cur_settlement) ) self.minimap.draw() # update minimap to new world def show_change_name_dialog(self, instance): """Shows a dialog where the user can change the name of a NamedComponant. The game gets paused while the dialog is executed.""" events = { OkButton.DEFAULT_NAME: Callback(self.change_name, instance), CancelButton.DEFAULT_NAME: self._hide_change_name_dialog } self.main_gui.on_escape = self._hide_change_name_dialog changename = self.widgets['change_name'] oldname = changename.findChild(name='old_name') oldname.text = instance.get_component(SettlementNameComponent).name newname = changename.findChild(name='new_name') changename.mapEvents(events) newname.capture(Callback(self.change_name, instance)) def forward_escape(event): # the textfield will eat everything, even control events if event.getKey().getValue() == fife.Key.ESCAPE: self.main_gui.on_escape() newname.capture( forward_escape, "keyPressed" ) changename.show() newname.requestFocus() def _hide_change_name_dialog(self): """Escapes the change_name dialog""" self.main_gui.on_escape = self.main_gui.toggle_pause self.widgets['change_name'].hide() def change_name(self, instance): """Applies the change_name dialogs input and hides it. If the new name has length 0 or only contains blanks, the old name is kept. """ new_name = self.widgets['change_name'].collectData('new_name') self.widgets['change_name'].findChild(name='new_name').text = u'' if not new_name or not new_name.isspace(): # different namedcomponent classes share the name RenameObject(instance.get_component_by_name(NamedComponent.NAME), new_name).execute(self.session) self._hide_change_name_dialog() def show_save_map_dialog(self): """Shows a dialog where the user can set the name of the saved map.""" events = { OkButton.DEFAULT_NAME: self.save_map, CancelButton.DEFAULT_NAME: self._hide_save_map_dialog } self.main_gui.on_escape = self._hide_save_map_dialog dialog = self.widgets['save_map'] name = dialog.findChild(name='map_name') name.text = u'' dialog.mapEvents(events) name.capture(Callback(self.save_map)) dialog.show() name.requestFocus() def _hide_save_map_dialog(self): """Closes the map saving dialog.""" self.main_gui.on_escape = self.main_gui.toggle_pause self.widgets['save_map'].hide() def save_map(self): """Saves the map and hides the dialog.""" name = self.widgets['save_map'].collectData('map_name') if re.match('^[a-zA-Z0-9_-]+$', name): self.session.save_map(name) self._hide_save_map_dialog() else: #xgettext:python-format message = _('Valid map names are in the following form: {expression}').format(expression='[a-zA-Z0-9_-]+') #xgettext:python-format advice = _('Try a name that only contains letters and numbers.') self.session.gui.show_error_popup(_('Error'), message, advice) def on_escape(self): if self.main_widget: self.main_widget.hide() else: return False return True def on_switch_main_widget(self, widget): """The main widget has been switched to the given one (possibly None).""" if self.main_widget and self.main_widget != widget: # close the old one if it exists old_main_widget = self.main_widget self.main_widget = None old_main_widget.hide() self.main_widget = widget def display_game_speed(self, text): """ @param text: unicode string to display as speed value """ wdg = self.widgets['minimap'].findChild(name="speed_text") wdg.text = text wdg.resizeToContent() self.widgets['minimap'].show() def _on_settler_level_change(self, message): """Gets called when the player changes""" if message.sender.owner.is_local_player: menu = self.get_cur_menu() if hasattr(menu, "name") and menu.name == "build_menu_tab_widget": # player changed and build menu is currently displayed self.show_build_menu(update=True) # TODO: Use a better measure then first tab # Quite fragile, makes sure the tablist in the mainsquare menu is updated if hasattr(menu, '_tabs') and isinstance(menu._tabs[0], MainSquareOverviewTab): instance = list(self.session.selected_instances)[0] instance.get_component(SelectableComponent).show_menu(jump_to_tabclass=type(menu.current_tab)) def show_chat_dialog(self): """Show a dialog where the user can enter a chat message""" events = { OkButton.DEFAULT_NAME: self._do_chat, CancelButton.DEFAULT_NAME: self._hide_chat_dialog } self.main_gui.on_escape = self._hide_chat_dialog self.widgets['chat'].mapEvents(events) def forward_escape(event): # the textfield will eat everything, even control events if event.getKey().getValue() == fife.Key.ESCAPE: self.main_gui.on_escape() self.widgets['chat'].findChild(name='msg').capture( forward_escape, "keyPressed" ) self.widgets['chat'].findChild(name='msg').capture( self._do_chat ) self.widgets['chat'].show() self.widgets['chat'].findChild(name="msg").requestFocus() def _hide_chat_dialog(self): """Escapes the chat dialog""" self.main_gui.on_escape = self.main_gui.toggle_pause self.widgets['chat'].hide() def _do_chat(self): """Actually initiates chatting and hides the dialog""" msg = self.widgets['chat'].findChild(name='msg').text Chat(msg).execute(self.session) self.widgets['chat'].findChild(name='msg').text = u'' self._hide_chat_dialog()
def __init__(self, session, gui): super(IngameGui, self).__init__() self.session = session assert isinstance(self.session, horizons.session.Session) self.main_gui = gui self.main_widget = None self.tabwidgets = {} self.settlement = None self.resource_source = None self.resources_needed, self.resources_usable = {}, {} self._old_menu = None self.widgets = LazyWidgetsDict(self.styles, center_widgets=False) self.cityinfo = self.widgets['city_info'] self.cityinfo.child_finder = PychanChildFinder(self.cityinfo) self.logbook = LogBook(self.session) self.message_widget = MessageWidget(self.session) self.players_overview = PlayersOverview(self.session) self.players_settlements = PlayersSettlements(self.session) self.players_ships = PlayersShips(self.session) # self.widgets['minimap'] is the guichan gui around the actual minimap, # which is saved in self.minimap minimap = self.widgets['minimap'] minimap.position_technique = "right+0:top+0" icon = minimap.findChild(name="minimap") self.minimap = Minimap( icon, targetrenderer=horizons.globals.fife.targetrenderer, imagemanager=horizons.globals.fife.imagemanager, session=self.session, view=self.session.view) def speed_up(): SpeedUpCommand().execute(self.session) def speed_down(): SpeedDownCommand().execute(self.session) minimap.mapEvents({ 'zoomIn': self.session.view.zoom_in, 'zoomOut': self.session.view.zoom_out, 'rotateRight': Callback.ChainedCallbacks(self.session.view.rotate_right, self.minimap.rotate_right), 'rotateLeft': Callback.ChainedCallbacks(self.session.view.rotate_left, self.minimap.rotate_left), 'speedUp': speed_up, 'speedDown': speed_down, 'destroy_tool': self.session.toggle_destroy_tool, 'build': self.show_build_menu, 'diplomacyButton': self.show_diplomacy_menu, 'gameMenuButton': self.main_gui.toggle_pause, 'logbook': self.logbook.toggle_visibility }) minimap.show() #minimap.position_technique = "right+15:top+153" self.widgets['tooltip'].hide() self.resource_overview = ResourceOverviewBar(self.session) ResourceBarResize.subscribe(self._on_resourcebar_resize) # Register for messages SettlerUpdate.subscribe(self._on_settler_level_change) SettlerInhabitantsChanged.subscribe(self._on_settler_inhabitant_change) HoverSettlementChanged.subscribe(self._cityinfo_set)
class IngameGui(LivingObject): """Class handling all the ingame gui events. Assumes that only 1 instance is used (class variables)""" gui = livingProperty() tabwidgets = livingProperty() message_widget = livingProperty() minimap = livingProperty() styles = { 'city_info': 'resource_bar', 'change_name': 'book', 'save_map': 'book', 'chat': 'book', } def __init__(self, session, gui): super(IngameGui, self).__init__() self.session = session assert isinstance(self.session, horizons.session.Session) self.main_gui = gui self.main_widget = None self.tabwidgets = {} self.settlement = None self.resource_source = None self.resources_needed, self.resources_usable = {}, {} self._old_menu = None self.widgets = LazyWidgetsDict(self.styles, center_widgets=False) self.cityinfo = self.widgets['city_info'] self.cityinfo.child_finder = PychanChildFinder(self.cityinfo) self.logbook = LogBook(self.session) self.message_widget = MessageWidget(self.session) self.players_overview = PlayersOverview(self.session) self.players_settlements = PlayersSettlements(self.session) self.players_ships = PlayersShips(self.session) # self.widgets['minimap'] is the guichan gui around the actual minimap, # which is saved in self.minimap minimap = self.widgets['minimap'] minimap.position_technique = "right+0:top+0" icon = minimap.findChild(name="minimap") self.minimap = Minimap( icon, targetrenderer=horizons.globals.fife.targetrenderer, imagemanager=horizons.globals.fife.imagemanager, session=self.session, view=self.session.view) def speed_up(): SpeedUpCommand().execute(self.session) def speed_down(): SpeedDownCommand().execute(self.session) minimap.mapEvents({ 'zoomIn': self.session.view.zoom_in, 'zoomOut': self.session.view.zoom_out, 'rotateRight': Callback.ChainedCallbacks(self.session.view.rotate_right, self.minimap.rotate_right), 'rotateLeft': Callback.ChainedCallbacks(self.session.view.rotate_left, self.minimap.rotate_left), 'speedUp': speed_up, 'speedDown': speed_down, 'destroy_tool': self.session.toggle_destroy_tool, 'build': self.show_build_menu, 'diplomacyButton': self.show_diplomacy_menu, 'gameMenuButton': self.main_gui.toggle_pause, 'logbook': self.logbook.toggle_visibility }) minimap.show() #minimap.position_technique = "right+15:top+153" self.widgets['tooltip'].hide() self.resource_overview = ResourceOverviewBar(self.session) ResourceBarResize.subscribe(self._on_resourcebar_resize) # Register for messages SettlerUpdate.subscribe(self._on_settler_level_change) SettlerInhabitantsChanged.subscribe(self._on_settler_inhabitant_change) HoverSettlementChanged.subscribe(self._cityinfo_set) def _on_resourcebar_resize(self, message): self._update_cityinfo_position() def end(self): self.widgets['minimap'].mapEvents({ 'zoomIn': None, 'zoomOut': None, 'rotateRight': None, 'rotateLeft': None, 'destroy_tool': None, 'build': None, 'diplomacyButton': None, 'gameMenuButton': None }) for w in self.widgets.itervalues(): if w.parent is None: w.hide() self.message_widget = None self.tabwidgets = None self.minimap = None self.resource_overview.end() self.resource_overview = None self.hide_menu() SettlerUpdate.unsubscribe(self._on_settler_level_change) ResourceBarResize.unsubscribe(self._on_resourcebar_resize) HoverSettlementChanged.unsubscribe(self._cityinfo_set) SettlerInhabitantsChanged.unsubscribe( self._on_settler_inhabitant_change) super(IngameGui, self).end() def _cityinfo_set(self, message): """Sets the city name at top center of screen. Show/Hide is handled automatically To hide cityname, set name to '' @param message: HoverSettlementChanged message """ settlement = message.settlement old_was_player_settlement = False if self.settlement is not None: self.settlement.remove_change_listener(self.update_settlement) old_was_player_settlement = self.settlement.owner.is_local_player # save reference to new "current" settlement in self.settlement self.settlement = settlement if settlement is None: # we want to hide the widget now (but perhaps delayed). if old_was_player_settlement: # After scrolling away from settlement, leave name on screen for some # seconds. Players can still click on it to rename the settlement now. ExtScheduler().add_new_object(self.cityinfo.hide, self, run_in=GUI.CITYINFO_UPDATE_DELAY) #TODO 'click to rename' tooltip of cityinfo can stay visible in # certain cases if cityinfo gets hidden in tooltip delay buffer. else: # hovered settlement of other player, simply hide the widget self.cityinfo.hide() else: # do not hide if settlement is hovered and a hide was previously scheduled ExtScheduler().rem_call(self, self.cityinfo.hide) self.update_settlement() # calls show() settlement.add_change_listener(self.update_settlement) def _on_settler_inhabitant_change(self, message): assert isinstance(message, SettlerInhabitantsChanged) foundlabel = self.cityinfo.child_finder('city_inhabitants') old_amount = int(foundlabel.text) if foundlabel.text else 0 foundlabel.text = u' {amount:>4d}'.format(amount=old_amount + message.change) foundlabel.resizeToContent() def update_settlement(self): city_name_label = self.cityinfo.child_finder('city_name') if self.settlement.owner.is_local_player: # allow name changes # Update settlement on the resource overview to make sure it # is setup correctly for the coming calculations self.resource_overview.set_inventory_instance(self.settlement) cb = Callback(self.show_change_name_dialog, self.settlement) helptext = _("Click to change the name of your settlement") city_name_label.enable_cursor_change_on_hover() else: # no name changes cb = lambda: AmbientSoundComponent.play_special('error') helptext = u"" city_name_label.disable_cursor_change_on_hover() self.cityinfo.mapEvents({'city_name': cb}) city_name_label.helptext = helptext foundlabel = self.cityinfo.child_finder('owner_emblem') foundlabel.image = 'content/gui/images/tabwidget/emblems/emblem_%s.png' % ( self.settlement.owner.color.name) foundlabel.helptext = self.settlement.owner.name foundlabel = self.cityinfo.child_finder('city_name') foundlabel.text = self.settlement.get_component( SettlementNameComponent).name foundlabel.resizeToContent() foundlabel = self.cityinfo.child_finder('city_inhabitants') foundlabel.text = u' {amount:>4d}'.format( amount=self.settlement.inhabitants) foundlabel.resizeToContent() self._update_cityinfo_position() def _update_cityinfo_position(self): """ Places cityinfo widget depending on resource bar dimensions. For a normal-sized resource bar and reasonably large resolution: * determine resource bar length (includes gold) * determine empty horizontal space between resbar end and minimap start * display cityinfo centered in that area if it is sufficiently large If too close to the minimap (cityinfo larger than length of this empty space) move cityinfo centered to very upper screen edge. Looks bad, works usually. In this case, the resbar is redrawn to put the cityinfo "behind" it visually. """ width = horizons.globals.fife.engine_settings.getScreenWidth() resbar = self.resource_overview.get_size() is_foreign = (self.settlement.owner != self.session.world.player) blocked = self.cityinfo.size[0] + int(1.5 * self.minimap.get_size()[1]) # minimap[1] returns width! Use 1.5*width because of the GUI around it if is_foreign: # other player, no resbar exists self.cityinfo.pos = ('center', 'top') xoff = 0 yoff = 19 elif blocked < width < resbar[ 0] + blocked: # large resbar / small resolution self.cityinfo.pos = ('center', 'top') xoff = 0 yoff = 0 # upper screen edge else: self.cityinfo.pos = ('left', 'top') xoff = resbar[0] + (width - blocked - resbar[0]) // 2 yoff = 24 self.cityinfo.offset = (xoff, yoff) self.cityinfo.position_technique = "{pos[0]}{off[0]:+d}:{pos[1]}{off[1]:+d}".format( pos=self.cityinfo.pos, off=self.cityinfo.offset) self.cityinfo.hide() self.cityinfo.show() def minimap_to_front(self): """Make sure the full right top gui is visible and not covered by some dialog""" self.widgets['minimap'].hide() self.widgets['minimap'].show() def show_diplomacy_menu(self): # check if the menu is already shown if getattr(self.get_cur_menu(), 'name', None) == "diplomacy_widget": self.hide_menu() return if not DiplomacyTab.is_useable(self.session.world): self.main_gui.show_popup( _("No diplomacy possible"), _("Cannot do diplomacy as there are no other players.")) return tab = DiplomacyTab(self, self.session.world) self.show_menu(tab) def show_multi_select_tab(self): tab = TabWidget(self, tabs=[SelectMultiTab(self.session)], name='select_multi') self.show_menu(tab) def show_build_menu(self, update=False): """ @param update: set when build possiblities change (e.g. after settler upgrade) """ # check if build menu is already shown if hasattr(self.get_cur_menu(), 'name') and self.get_cur_menu( ).name == "build_menu_tab_widget": self.hide_menu() if not update: # this was only a toggle call, don't reshow return self.session.set_cursor() # set default cursor for build menu self.deselect_all() if not any(settlement.owner.is_local_player for settlement in self.session.world.settlements): # player has not built any settlements yet. Accessing the build menu at such a point # indicates a mistake in the mental model of the user. Display a hint. tab = TabWidget( self, tabs=[TabInterface(widget="buildtab_no_settlement.xml")]) else: btabs = BuildTab.create_tabs(self.session, self._build) tab = TabWidget(self, tabs=btabs, name="build_menu_tab_widget", active_tab=BuildTab.last_active_build_tab) self.show_menu(tab) def deselect_all(self): for instance in self.session.selected_instances: instance.get_component(SelectableComponent).deselect() self.session.selected_instances.clear() def _build(self, building_id, unit=None): """Calls the games buildingtool class for the building_id. @param building_id: int with the building id that is to be built. @param unit: weakref to the unit, that builds (e.g. ship for warehouse)""" self.hide_menu() self.deselect_all() cls = Entities.buildings[building_id] if hasattr(cls, 'show_build_menu'): cls.show_build_menu() self.session.set_cursor('building', cls, None if unit is None else unit()) def toggle_road_tool(self): if not isinstance( self.session.cursor, BuildingTool ) or self.session.cursor._class.id != BUILDINGS.TRAIL: self._build(BUILDINGS.TRAIL) else: self.session.set_cursor() def _get_menu_object(self, menu): """Returns pychan object if menu is a string, else returns menu @param menu: str with the guiname or pychan object. """ if isinstance(menu, str): menu = self.widgets[menu] return menu def get_cur_menu(self): """Returns menu that is currently displayed""" return self._old_menu def show_menu(self, menu): """Shows a menu @param menu: str with the guiname or pychan object. """ if self._old_menu is not None: if hasattr(self._old_menu, "remove_remove_listener"): self._old_menu.remove_remove_listener( Callback(self.show_menu, None)) self._old_menu.hide() self._old_menu = self._get_menu_object(menu) if self._old_menu is not None: if hasattr(self._old_menu, "add_remove_listener"): self._old_menu.add_remove_listener( Callback(self.show_menu, None)) self._old_menu.show() self.minimap_to_front() TabWidgetChanged.broadcast(self) def hide_menu(self): self.show_menu(None) def toggle_menu(self, menu): """Shows a menu or hides it if it is already displayed. @param menu: parameter supported by show_menu(). """ if self.get_cur_menu() == self._get_menu_object(menu): self.hide_menu() else: self.show_menu(menu) def save(self, db): self.message_widget.save(db) self.logbook.save(db) self.resource_overview.save(db) def load(self, db): self.message_widget.load(db) self.logbook.load(db) self.resource_overview.load(db) cur_settlement = LastActivePlayerSettlementManager( ).get_current_settlement() self._cityinfo_set(HoverSettlementChanged(self, cur_settlement)) self.minimap.draw() # update minimap to new world def show_change_name_dialog(self, instance): """Shows a dialog where the user can change the name of a NamedComponant. The game gets paused while the dialog is executed.""" events = { OkButton.DEFAULT_NAME: Callback(self.change_name, instance), CancelButton.DEFAULT_NAME: self._hide_change_name_dialog } self.main_gui.on_escape = self._hide_change_name_dialog changename = self.widgets['change_name'] oldname = changename.findChild(name='old_name') oldname.text = instance.get_component(SettlementNameComponent).name newname = changename.findChild(name='new_name') changename.mapEvents(events) newname.capture(Callback(self.change_name, instance)) def forward_escape(event): # the textfield will eat everything, even control events if event.getKey().getValue() == fife.Key.ESCAPE: self.main_gui.on_escape() newname.capture(forward_escape, "keyPressed") changename.show() newname.requestFocus() def _hide_change_name_dialog(self): """Escapes the change_name dialog""" self.main_gui.on_escape = self.main_gui.toggle_pause self.widgets['change_name'].hide() def change_name(self, instance): """Applies the change_name dialogs input and hides it. If the new name has length 0 or only contains blanks, the old name is kept. """ new_name = self.widgets['change_name'].collectData('new_name') self.widgets['change_name'].findChild(name='new_name').text = u'' if not new_name or not new_name.isspace(): # different namedcomponent classes share the name RenameObject(instance.get_component_by_name(NamedComponent.NAME), new_name).execute(self.session) self._hide_change_name_dialog() def show_save_map_dialog(self): """Shows a dialog where the user can set the name of the saved map.""" events = { OkButton.DEFAULT_NAME: self.save_map, CancelButton.DEFAULT_NAME: self._hide_save_map_dialog } self.main_gui.on_escape = self._hide_save_map_dialog dialog = self.widgets['save_map'] name = dialog.findChild(name='map_name') name.text = u'' dialog.mapEvents(events) name.capture(Callback(self.save_map)) dialog.show() name.requestFocus() def _hide_save_map_dialog(self): """Closes the map saving dialog.""" self.main_gui.on_escape = self.main_gui.toggle_pause self.widgets['save_map'].hide() def save_map(self): """Saves the map and hides the dialog.""" name = self.widgets['save_map'].collectData('map_name') if re.match('^[a-zA-Z0-9_-]+$', name): self.session.save_map(name) self._hide_save_map_dialog() else: #xgettext:python-format message = _( 'Valid map names are in the following form: {expression}' ).format(expression='[a-zA-Z0-9_-]+') #xgettext:python-format advice = _('Try a name that only contains letters and numbers.') self.session.gui.show_error_popup(_('Error'), message, advice) def on_escape(self): if self.main_widget: self.main_widget.hide() else: return False return True def on_switch_main_widget(self, widget): """The main widget has been switched to the given one (possibly None).""" if self.main_widget and self.main_widget != widget: # close the old one if it exists old_main_widget = self.main_widget self.main_widget = None old_main_widget.hide() self.main_widget = widget def display_game_speed(self, text): """ @param text: unicode string to display as speed value """ wdg = self.widgets['minimap'].findChild(name="speed_text") wdg.text = text wdg.resizeToContent() self.widgets['minimap'].show() def _on_settler_level_change(self, message): """Gets called when the player changes""" if message.sender.owner.is_local_player: menu = self.get_cur_menu() if hasattr(menu, "name") and menu.name == "build_menu_tab_widget": # player changed and build menu is currently displayed self.show_build_menu(update=True) # TODO: Use a better measure then first tab # Quite fragile, makes sure the tablist in the mainsquare menu is updated if hasattr(menu, '_tabs') and isinstance(menu._tabs[0], MainSquareOverviewTab): instance = list(self.session.selected_instances)[0] instance.get_component(SelectableComponent).show_menu( jump_to_tabclass=type(menu.current_tab)) def show_chat_dialog(self): """Show a dialog where the user can enter a chat message""" events = { OkButton.DEFAULT_NAME: self._do_chat, CancelButton.DEFAULT_NAME: self._hide_chat_dialog } self.main_gui.on_escape = self._hide_chat_dialog self.widgets['chat'].mapEvents(events) def forward_escape(event): # the textfield will eat everything, even control events if event.getKey().getValue() == fife.Key.ESCAPE: self.main_gui.on_escape() self.widgets['chat'].findChild(name='msg').capture( forward_escape, "keyPressed") self.widgets['chat'].findChild(name='msg').capture(self._do_chat) self.widgets['chat'].show() self.widgets['chat'].findChild(name="msg").requestFocus() def _hide_chat_dialog(self): """Escapes the chat dialog""" self.main_gui.on_escape = self.main_gui.toggle_pause self.widgets['chat'].hide() def _do_chat(self): """Actually initiates chatting and hides the dialog""" msg = self.widgets['chat'].findChild(name='msg').text Chat(msg).execute(self.session) self.widgets['chat'].findChild(name='msg').text = u'' self._hide_chat_dialog()
def __init__(self, session, gui): super(IngameGui, self).__init__() self.session = session assert isinstance(self.session, horizons.session.Session) self.main_gui = gui self.main_widget = None self.tabwidgets = {} self.settlement = None self.resource_source = None self.resources_needed, self.resources_usable = {}, {} self._old_menu = None self.widgets = LazyWidgetsDict(self.styles, center_widgets=False) cityinfo = self.widgets['city_info'] cityinfo.child_finder = PychanChildFinder(cityinfo) # special settings for really small resolutions #TODO explain what actually happens width = horizons.main.fife.engine_settings.getScreenWidth() x = 'center' y = 'top' x_offset = +15 y_offset = +4 if width < 800: x = 'left' x_offset = 10 y_offset = +66 elif width < 1020: x_offset = (1050 - width) / 2 cityinfo.position_technique = "%s%+d:%s%+d" % (x, x_offset, y, y_offset) # usually "center-10:top+4" self.logbook = LogBook(self.session) self.message_widget = MessageWidget(self.session) self.players_overview = PlayersOverview(self.session) self.players_settlements = PlayersSettlements(self.session) self.players_ships = PlayersShips(self.session) self.scenario_chooser = ScenarioChooser(self.session) # self.widgets['minimap'] is the guichan gui around the actual minimap, # which is saved in self.minimap minimap = self.widgets['minimap'] minimap.position_technique = "right+0:top+0" icon = minimap.findChild(name="minimap") self.minimap = Minimap(icon, targetrenderer=horizons.main.fife.targetrenderer, imagemanager=horizons.main.fife.imagemanager, session=self.session, view=self.session.view) def speed_up(): SpeedUpCommand().execute(self.session) def speed_down(): SpeedDownCommand().execute(self.session) minimap.mapEvents({ 'zoomIn' : self.session.view.zoom_in, 'zoomOut' : self.session.view.zoom_out, 'rotateRight' : Callback.ChainedCallbacks(self.session.view.rotate_right, self.minimap.rotate_right), 'rotateLeft' : Callback.ChainedCallbacks(self.session.view.rotate_left, self.minimap.rotate_left), 'speedUp' : speed_up, 'speedDown' : speed_down, 'destroy_tool' : self.session.toggle_destroy_tool, 'build' : self.show_build_menu, 'diplomacyButton' : self.show_diplomacy_menu, 'gameMenuButton' : self.main_gui.toggle_pause, 'logbook' : self.logbook.toggle_visibility }) minimap.show() #minimap.position_technique = "right+15:top+153" self.widgets['tooltip'].hide() self.resource_overview = ResourceOverviewBar(self.session) ResourceBarResize.subscribe(self._on_resourcebar_resize) # map buildings to build functions calls with their building id. # This is necessary because BuildTabs have no session. self.callbacks_build = dict() for building_id in Entities.buildings.iterkeys(): self.callbacks_build[building_id] = Callback(self._build, building_id) # Register for messages SettlerUpdate.subscribe(self._on_settler_level_change) SettlerInhabitantsChanged.subscribe(self._on_settler_inhabitant_change) HoverSettlementChanged.subscribe(self._cityinfo_set)
class IngameGui(LivingObject): """Class handling all the ingame gui events. Assumes that only 1 instance is used (class variables)""" gui = livingProperty() tabwidgets = livingProperty() message_widget = livingProperty() minimap = livingProperty() styles = { 'city_info' : 'city_info', 'change_name' : 'book', 'save_map' : 'book', 'chat' : 'book', } def __init__(self, session, gui): super(IngameGui, self).__init__() self.session = session assert isinstance(self.session, horizons.session.Session) self.main_gui = gui self.main_widget = None self.tabwidgets = {} self.settlement = None self.resource_source = None self.resources_needed, self.resources_usable = {}, {} self._old_menu = None self.widgets = LazyWidgetsDict(self.styles, center_widgets=False) cityinfo = self.widgets['city_info'] cityinfo.child_finder = PychanChildFinder(cityinfo) # special settings for really small resolutions #TODO explain what actually happens width = horizons.main.fife.engine_settings.getScreenWidth() x = 'center' y = 'top' x_offset = +15 y_offset = +4 if width < 800: x = 'left' x_offset = 10 y_offset = +66 elif width < 1020: x_offset = (1050 - width) / 2 cityinfo.position_technique = "%s%+d:%s%+d" % (x, x_offset, y, y_offset) # usually "center-10:top+4" self.logbook = LogBook(self.session) self.message_widget = MessageWidget(self.session) self.players_overview = PlayersOverview(self.session) self.players_settlements = PlayersSettlements(self.session) self.players_ships = PlayersShips(self.session) self.scenario_chooser = ScenarioChooser(self.session) # self.widgets['minimap'] is the guichan gui around the actual minimap, # which is saved in self.minimap minimap = self.widgets['minimap'] minimap.position_technique = "right+0:top+0" icon = minimap.findChild(name="minimap") self.minimap = Minimap(icon, targetrenderer=horizons.main.fife.targetrenderer, imagemanager=horizons.main.fife.imagemanager, session=self.session, view=self.session.view) def speed_up(): SpeedUpCommand().execute(self.session) def speed_down(): SpeedDownCommand().execute(self.session) minimap.mapEvents({ 'zoomIn' : self.session.view.zoom_in, 'zoomOut' : self.session.view.zoom_out, 'rotateRight' : Callback.ChainedCallbacks(self.session.view.rotate_right, self.minimap.rotate_right), 'rotateLeft' : Callback.ChainedCallbacks(self.session.view.rotate_left, self.minimap.rotate_left), 'speedUp' : speed_up, 'speedDown' : speed_down, 'destroy_tool' : self.session.toggle_destroy_tool, 'build' : self.show_build_menu, 'diplomacyButton' : self.show_diplomacy_menu, 'gameMenuButton' : self.main_gui.toggle_pause, 'logbook' : self.logbook.toggle_visibility }) minimap.show() #minimap.position_technique = "right+15:top+153" self.widgets['tooltip'].hide() self.resource_overview = ResourceOverviewBar(self.session) ResourceBarResize.subscribe(self._on_resourcebar_resize) # map buildings to build functions calls with their building id. # This is necessary because BuildTabs have no session. self.callbacks_build = dict() for building_id in Entities.buildings.iterkeys(): self.callbacks_build[building_id] = Callback(self._build, building_id) # Register for messages SettlerUpdate.subscribe(self._on_settler_level_change) SettlerInhabitantsChanged.subscribe(self._on_settler_inhabitant_change) HoverSettlementChanged.subscribe(self._cityinfo_set) def _on_resourcebar_resize(self, message): ### # TODO implement ### pass def end(self): self.widgets['minimap'].mapEvents({ 'zoomIn' : None, 'zoomOut' : None, 'rotateRight' : None, 'rotateLeft': None, 'destroy_tool' : None, 'build' : None, 'diplomacyButton' : None, 'gameMenuButton' : None }) for w in self.widgets.itervalues(): if w.parent is None: w.hide() self.message_widget = None self.tabwidgets = None self.minimap = None self.resource_overview.end() self.resource_overview = None self.hide_menu() SettlerUpdate.unsubscribe(self._on_settler_level_change) ResourceBarResize.unsubscribe(self._on_resourcebar_resize) HoverSettlementChanged.unsubscribe(self._cityinfo_set) SettlerInhabitantsChanged.unsubscribe(self._on_settler_inhabitant_change) super(IngameGui, self).end() def _cityinfo_set(self, message): """Sets the city name at top center of screen. Show/Hide is handled automatically To hide cityname, set name to '' @param message: HoverSettlementChanged message """ settlement = message.settlement old_was_player_settlement = False if self.settlement is not None: self.settlement.remove_change_listener(self.update_settlement) old_was_player_settlement = (self.settlement.owner == self.session.world.player) # save reference to new "current" settlement in self.settlement self.settlement = settlement if settlement is None: # we want to hide the widget now (but perhaps delayed). if old_was_player_settlement: # Interface feature: Since players might need to scroll to an area not # occupied by the current settlement, leave name on screen in case they # want to e.g. rename the settlement which requires a click on cityinfo ExtScheduler().add_new_object(self.widgets['city_info'].hide, self, run_in=GUI.CITYINFO_UPDATE_DELAY) #TODO 'click to rename' tooltip of cityinfo can stay visible in # certain cases if cityinfo gets hidden in tooltip delay buffer. else: # this happens if you have not hovered an own settlement, # but others like AI settlements. Simply hide the widget. self.widgets['city_info'].hide() else:# we want to show the widget. # do not hide cityinfo if we again hover the settlement # before the delayed hide of the old info kicks in ExtScheduler().rem_call(self, self.widgets['city_info'].hide) self.widgets['city_info'].show() self.update_settlement() settlement.add_change_listener(self.update_settlement) def _on_settler_inhabitant_change(self, message): assert isinstance(message, SettlerInhabitantsChanged) cityinfo = self.widgets['city_info'] foundlabel = cityinfo.child_finder('city_inhabitants') foundlabel.text = u' %s' % ((int(foundlabel.text) if foundlabel.text else 0) + message.change) foundlabel.resizeToContent() def update_settlement(self): cityinfo = self.widgets['city_info'] if self.settlement.owner.is_local_player: # allow name changes cb = Callback(self.show_change_name_dialog, self.settlement) helptext = _("Click to change the name of your settlement") else: # no name changes cb = lambda : 42 helptext = u"" cityinfo.mapEvents({ 'city_name': cb }) cityinfo.findChild(name="city_name").helptext = helptext foundlabel = cityinfo.child_finder('owner_emblem') foundlabel.image = 'content/gui/images/tabwidget/emblems/emblem_%s.png' % (self.settlement.owner.color.name) foundlabel.helptext = self.settlement.owner.name foundlabel = cityinfo.child_finder('city_name') foundlabel.text = self.settlement.get_component(SettlementNameComponent).name foundlabel.resizeToContent() foundlabel = cityinfo.child_finder('city_inhabitants') foundlabel.text = u' %s' % (self.settlement.inhabitants) foundlabel.resizeToContent() cityinfo.adaptLayout() def minimap_to_front(self): """Make sure the full right top gui is visible and not covered by some dialog""" self.widgets['minimap'].hide() self.widgets['minimap'].show() def show_diplomacy_menu(self): # check if the menu is already shown if hasattr(self.get_cur_menu(), 'name') and self.get_cur_menu().name == "diplomacy_widget": self.hide_menu() return players = set(self.session.world.players) players.add(self.session.world.pirate) players.discard(self.session.world.player) players.discard(None) # e.g. when the pirate is disabled if len(players) == 0: # this dialog is pretty useless in this case self.main_gui.show_popup(_("No diplomacy possible"), \ _("Cannot do diplomacy as there are no other players.")) return dtabs = [] for player in players: dtabs.append(DiplomacyTab(player)) tab = TabWidget(self, tabs=dtabs, name="diplomacy_widget") self.show_menu(tab) def show_multi_select_tab(self): tab = TabWidget(self, tabs = [SelectMultiTab(self.session)], name = 'select_multi') self.show_menu(tab) def show_build_menu(self, update=False): """ @param update: set when build possiblities change (e.g. after settler upgrade) """ # check if build menu is already shown if hasattr(self.get_cur_menu(), 'name') and self.get_cur_menu().name == "build_menu_tab_widget": self.hide_menu() if not update: # this was only a toggle call, don't reshow return self.session.set_cursor() # set default cursor for build menu self.deselect_all() if not any( settlement.owner.is_local_player for settlement in self.session.world.settlements): # player has not built any settlements yet. Accessing the build menu at such a point # indicates a mistake in the mental model of the user. Display a hint. tab = TabWidget(self, tabs=[ TabInterface(widget="buildtab_no_settlement.xml") ]) else: btabs = [BuildTab(index+1, self.callbacks_build, self.session) for index in \ xrange(self.session.world.player.settler_level+1)] tab = TabWidget(self, tabs=btabs, name="build_menu_tab_widget", \ active_tab=BuildTab.last_active_build_tab) self.show_menu(tab) def deselect_all(self): for instance in self.session.selected_instances: instance.get_component(SelectableComponent).deselect() self.session.selected_instances.clear() def _build(self, building_id, unit = None): """Calls the games buildingtool class for the building_id. @param building_id: int with the building id that is to be built. @param unit: weakref to the unit, that builds (e.g. ship for warehouse)""" self.hide_menu() self.deselect_all() cls = Entities.buildings[building_id] if hasattr(cls, 'show_build_menu'): cls.show_build_menu() self.session.set_cursor('building', cls, None if unit is None else unit()) def toggle_road_tool(self): if not isinstance(self.session.cursor, BuildingTool) or self.session.cursor._class.id != BUILDINGS.TRAIL: if isinstance(self.session.cursor, BuildingTool): print self.session.cursor._class.id, BUILDINGS.TRAIL self._build(BUILDINGS.TRAIL) else: self.session.set_cursor() def _get_menu_object(self, menu): """Returns pychan object if menu is a string, else returns menu @param menu: str with the guiname or pychan object. """ if isinstance(menu, str): menu = self.widgets[menu] return menu def get_cur_menu(self): """Returns menu that is currently displayed""" return self._old_menu def show_menu(self, menu): """Shows a menu @param menu: str with the guiname or pychan object. """ if self._old_menu is not None: if hasattr(self._old_menu, "remove_remove_listener"): self._old_menu.remove_remove_listener( Callback(self.show_menu, None) ) self._old_menu.hide() self._old_menu = self._get_menu_object(menu) if self._old_menu is not None: if hasattr(self._old_menu, "add_remove_listener"): self._old_menu.add_remove_listener( Callback(self.show_menu, None) ) self._old_menu.show() self.minimap_to_front() def hide_menu(self): self.show_menu(None) def toggle_menu(self, menu): """Shows a menu or hides it if it is already displayed. @param menu: parameter supported by show_menu(). """ if self.get_cur_menu() == self._get_menu_object(menu): self.hide_menu() else: self.show_menu(menu) def save(self, db): self.message_widget.save(db) self.logbook.save(db) self.resource_overview.save(db) def load(self, db): self.message_widget.load(db) self.logbook.load(db) self.resource_overview.load(db) cur_settlement = LastActivePlayerSettlementManager().get_current_settlement() self._cityinfo_set( HoverSettlementChanged(self, cur_settlement) ) self.minimap.draw() # update minimap to new world def show_change_name_dialog(self, instance): """Shows a dialog where the user can change the name of a NamedComponant. The game gets paused while the dialog is executed.""" events = { OkButton.DEFAULT_NAME: Callback(self.change_name, instance), CancelButton.DEFAULT_NAME: self._hide_change_name_dialog } self.main_gui.on_escape = self._hide_change_name_dialog changename = self.widgets['change_name'] oldname = changename.findChild(name='old_name') oldname.text = instance.get_component(SettlementNameComponent).name newname = changename.findChild(name='new_name') changename.mapEvents(events) newname.capture(Callback(self.change_name, instance)) def forward_escape(event): # the textfield will eat everything, even control events if event.getKey().getValue() == fife.Key.ESCAPE: self.main_gui.on_escape() newname.capture( forward_escape, "keyPressed" ) changename.show() newname.requestFocus() def _hide_change_name_dialog(self): """Escapes the change_name dialog""" self.main_gui.on_escape = self.main_gui.toggle_pause self.widgets['change_name'].hide() def change_name(self, instance): """Applies the change_name dialogs input and hides it. If the new name has length 0 or only contains blanks, the old name is kept. """ new_name = self.widgets['change_name'].collectData('new_name') self.widgets['change_name'].findChild(name='new_name').text = u'' if not (len(new_name) == 0 or new_name.isspace()): # different namedcomponent classes share the name RenameObject(instance.get_component_by_name(NamedComponent.NAME), new_name).execute(self.session) self._hide_change_name_dialog() def show_save_map_dialog(self): """Shows a dialog where the user can set the name of the saved map.""" events = { OkButton.DEFAULT_NAME: self.save_map, CancelButton.DEFAULT_NAME: self._hide_save_map_dialog } self.main_gui.on_escape = self._hide_save_map_dialog dialog = self.widgets['save_map'] name = dialog.findChild(name = 'map_name') name.text = u'' dialog.mapEvents(events) name.capture(Callback(self.save_map)) dialog.show() name.requestFocus() def _hide_save_map_dialog(self): """Closes the map saving dialog.""" self.main_gui.on_escape = self.main_gui.toggle_pause self.widgets['save_map'].hide() def save_map(self): """Saves the map and hides the dialog.""" name = self.widgets['save_map'].collectData('map_name') if re.match('^[a-zA-Z0-9_-]+$', name): self.session.save_map(name) self._hide_save_map_dialog() else: #xgettext:python-format message = _('Valid map names are in the following form: {expression}').format(expression='[a-zA-Z0-9_-]+') #xgettext:python-format advice = _('Try a name that only contains letters and numbers.') self.session.gui.show_error_popup(_('Error'), message, advice) def on_escape(self): if self.main_widget: self.main_widget.hide() else: return False return True def on_switch_main_widget(self, widget): """The main widget has been switched to the given one (possibly None).""" if self.main_widget and self.main_widget != widget: # close the old one if it exists old_main_widget = self.main_widget self.main_widget = None old_main_widget.hide() self.main_widget = widget def display_game_speed(self, text): """ @param text: unicode string to display as speed value """ wdg = self.widgets['minimap'].findChild(name="speed_text") wdg.text = text wdg.resizeToContent() self.widgets['minimap'].show() def _on_settler_level_change(self, message): """Gets called when the player changes""" if message.sender.owner.is_local_player: menu = self.get_cur_menu() if hasattr(menu, "name") and menu.name == "build_menu_tab_widget": # player changed and build menu is currently displayed self.show_build_menu(update=True) def show_chat_dialog(self): """Show a dialog where the user can enter a chat message""" events = { OkButton.DEFAULT_NAME: self._do_chat, CancelButton.DEFAULT_NAME: self._hide_chat_dialog } self.main_gui.on_escape = self._hide_chat_dialog self.widgets['chat'].mapEvents(events) def forward_escape(event): # the textfield will eat everything, even control events if event.getKey().getValue() == fife.Key.ESCAPE: self.main_gui.on_escape() self.widgets['chat'].findChild(name='msg').capture( forward_escape, "keyPressed" ) self.widgets['chat'].findChild(name='msg').capture( self._do_chat ) self.widgets['chat'].show() self.widgets['chat'].findChild(name="msg").requestFocus() def _hide_chat_dialog(self): """Escapes the chat dialog""" self.main_gui.on_escape = self.main_gui.toggle_pause self.widgets['chat'].hide() def _do_chat(self): """Actually initiates chatting and hides the dialog""" msg = self.widgets['chat'].findChild(name='msg').text Chat(msg).execute(self.session) self.widgets['chat'].findChild(name='msg').text = u'' self._hide_chat_dialog()
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(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])