Beispiel #1
0
	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()
Beispiel #2
0
    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)
Beispiel #3
0
	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()
Beispiel #4
0
	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)
Beispiel #5
0
	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()
Beispiel #6
0
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()
Beispiel #9
0
    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)
Beispiel #10
0
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()
Beispiel #13
0
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()
Beispiel #14
0
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])
Beispiel #15
0
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])