Esempio n. 1
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()

# 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,
			'startMulti'     : self.show_multi,
			'settingsLink'   : self.show_settings,
			'helpLink'       : self.on_help,
			'closeButton'    : self.show_quit,
			'dead_link'      : self.on_chime, # call for help; SoC information
			'creditsLink'    : self.show_credits,
			'loadgameButton' : horizons.main.load_game
		})

		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):
		self.on_escape = lambda : horizons.main.fife._setting.OptionsDlg.hide()
		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' : True}, onPressEscape = 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' : True}, onPressEscape = 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 == 2: # #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' : True}, onPressEscape = 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').helptext = _('Save game') if mode == 'save' else _('Load game')

		name_box = self.current.findChild(name="gamename_box")
		if mp and mode == 'load': # have gamename
			name_box.parent.showChild(name_box)
			gamename_textfield = self.current.findChild(name="gamename")
			def clear_gamename_textfield():
				gamename_textfield.text = u""
			gamename_textfield.capture(clear_gamename_textfield, 'mouseReleased', 'default')
		else:
			if name_box not in name_box.parent.hidden_children:
				name_box.parent.hideChild(name_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,
		    'savegamelist/mouseWheelMovedUp'   : cb,
		    'savegamelist/mouseWheelMovedDown' : cb
		})
		self.current.findChild(name="savegamelist").capture(cb, event_name="keyPressed")

		eventMap = {
			'okButton'     : True,
			'cancelButton' : False,
			'deleteButton' : 'delete'
		}

		if mode == 'save':
			eventMap['savegamefile'] = True

		retval = self.show_dialog(self.current, eventMap, onPressEscape = False)
		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')
		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, onPressEscape = None, 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 onPressEscape: callback that is to be called if the escape button is pressed
		@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)
		if onPressEscape is not None:
			def _escape(event):
				if event.getKey().getValue() == fife.Key.ESCAPE:
					pychan.internal.get_manager().breakFromMainLoop(onPressEscape)
					dlg.hide()
			dlg.capture(_escape, event_name="keyPressed")
		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').requestFocus(), self, run_in=0)
		if show_cancel_button:
			return self.show_dialog(popup, {'okButton' : True, 'cancelButton' : False}, onPressEscape = False)
		else:
			return self.show_dialog(popup, {'okButton' : True}, onPressEscape = 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")
			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:
				if savegame_info['savegamerev'] == VERSION.SAVEGAMEREVISION:
					#xgettext:python-format
					details_label.text += _("Savegame version {version}").format(
					                         version=savegame_info['savegamerev'])
				else:
					#xgettext:python-format
					details_label.text += _("WARNING: Incompatible version {version}!").format(
					                         version=savegame_info['savegamerev']) + u"\n"
					#xgettext:python-format
					details_label.text += _("Required version: {required}!").format(
					                         required=VERSION.SAVEGAMEREVISION)
			except KeyError:
				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(self):
		"""Randomly select a background image to use through out the game menu."""
		available_images = glob.glob('content/gui/images/background/mainmenu/bg_*.png')
		return random.choice(available_images)
Esempio n. 2
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",
        "multiplayermenu": "book",
        "multiplayer_creategame": "book",
        "multiplayer_gamelobby": "book",
        "playerdataselection": "book",
        "aidataselection": "book",
        "select_savegame": "book",
        "ingame_pause": "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'
        self.session = None
        self.current_dialog = None

        self.dialog_executed = False

        self.__pause_displayed = False

    # 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,
                "startMulti": self.show_multi,
                "settingsLink": self.show_settings,
                "helpLink": self.on_help,
                "closeButton": self.show_quit,
                "dead_link": self.on_chime,  # call for help; SoC information
                "creditsLink": self.show_credits,
                "loadgameButton": horizons.main.load_game,
            },
        )

        self.on_escape = self.show_quit

    def toggle_pause(self):
        """
		Show Pause menu
		"""
        # import here because we get a weird cycle otherwise
        if self.__pause_displayed:
            self.__pause_displayed = False
            self.hide()
            self.current = None
            self.session.speed_unpause(True)
            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")
            self._switch_current_widget(
                "ingamemenu",
                center=True,
                show=False,
                event_map={
                    # icons
                    "loadgameButton": horizons.main.load_game,
                    "savegameButton": self.save_game,
                    "settingsLink": self.show_settings,
                    "helpLink": self.on_help,
                    "startGame": self.toggle_pause,
                    "closeButton": self.quit_session,
                    # labels
                    "loadgame": horizons.main.load_game,
                    "savegame": self.save_game,
                    "settings": self.show_settings,
                    "help": self.on_help,
                    "start": self.toggle_pause,
                    "quit": self.quit_session,
                },
            )
            self.current.additional_widget = pychan.Icon(image="content/gui/images/background/transparent.png")
            self.current.additional_widget.position = (0, 0)
            self.current.additional_widget.show()
            self.current.show()

            self.session.speed_pause(True)
            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:
            self.show_popup(_("Error"), _("Failed to save."))

    def show_settings(self):
        horizons.main.fife._setting.onOptionsPress()

    _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"]:
                self.session.speed_pause()
            self.show_dialog(help_dlg, {"okButton": True}, onPressEscape=True)
            self.on_help()  # toggle state
        else:
            self._help_is_displayed = False
            if self.session is not None and self.current != self.widgets["ingamemenu"]:
                self.session.speed_unpause()
            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()

    def on_chime(self):
        """
		Called chime action. Displaying call for help on artists and game design,
		introduces information for SoC applicants (if valid).
		"""
        AmbientSound.play_special("message")
        self.show_dialog(self.widgets["call_for_support"], {"okButton": True}, onPressEscape=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 == 2:  # #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", "special_thanks")
        ]
        for i in xrange(0, 4):
            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": True}, onPressEscape=True)

    def show_select_savegame(self, mode):
        """Shows menu to select a savegame.
		@param mode: 'save' or 'load'
		@return: Path to savegamefile or None"""
        assert mode in ("save", "load")
        map_files, map_file_display = None, None
        if mode == "load":
            map_files, map_file_display = SavegameManager.get_saves()
            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
            map_files, map_file_display = SavegameManager.get_regular_saves()

            # Prepare widget
        old_current = self._switch_current_widget("select_savegame")
        self.current.findChild(name="headline").text = _("Save game") if mode == "save" else _("Load game")

        """ this doesn't work (yet), see http://fife.trac.cvsdude.com/engine/ticket/375
		if mode == 'save': # only show enter_filename on save
			self.current.findChild(name='enter_filename').show()
		else:
			self.current.findChild(name='enter_filename').hide()
		"""

        def tmp_selected_changed():
            """Fills in the name of the savegame in the textbox when selected in the list"""
            if self.current.collectData("savegamelist") != -1:  # check if we actually collect valid data
                self.current.distributeData(
                    {"savegamefile": map_file_display[self.current.collectData("savegamelist")]}
                )

        self.current.distributeInitialData({"savegamelist": map_file_display})
        cb = Callback.ChainedCallbacks(
            Gui._create_show_savegame_details(self.current, map_files, "savegamelist"), tmp_selected_changed
        )
        self.current.findChild(name="savegamelist").mapEvents(
            {"savegamelist/action": cb, "savegamelist/mouseWheelMovedUp": cb, "savegamelist/mouseWheelMovedDown": cb}
        )
        self.current.findChild(name="savegamelist").capture(cb, event_name="keyPressed")

        retval = self.show_dialog(
            self.current,
            {"okButton": True, "cancelButton": False, "deleteButton": "delete", "savegamefile": True},
            onPressEscape=False,
        )
        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
            self._delete_savegame(map_files)
            self.current = old_current
            return self.show_select_savegame(mode=mode)

        selected_savegame = None
        if mode == "save":  # return from textfield
            selected_savegame = self.current.collectData("savegamefile")
            if selected_savegame in map_file_display:  # savegamename already exists
                message = (
                    _('A savegame with the name "%s" already exists. \nShould i overwrite it?') % selected_savegame
                )
                if not self.show_popup(_("Confirmation for overwriting"), message, show_cancel_button=True):
                    self.current = old_current
                    return self.show_select_savegame(mode=mode)  # 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(mode=mode)  # reshow dialog
        self.current = old_current  # reuse old widget
        return selected_savegame

    # 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, e:
                pass  # only used for some widgets, e.g. pause
Esempio n. 3
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',
	  'multiplayermenu' : 'book',
	  'multiplayer_creategame' : 'book',
	  'multiplayer_gamelobby' : 'book',
	  'playerdataselection' : 'book',
	  'aidataselection' : 'book',
	  'select_savegame': 'book',
	  'ingame_pause': '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'
		self.session = None
		self.current_dialog = None

		self.__pause_displayed = False

# 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,
			'startMulti'     : self.show_multi,
			'settingsLink'   : self.show_settings,
			'helpLink'       : self.on_help,
			'closeButton'    : self.show_quit,
			'dead_link'      : self.on_chime, # call for help; SoC information
			'creditsLink'    : self.show_credits,
			'loadgameButton' : horizons.main.load_game
		})

		self.on_escape = self.show_quit

	def toggle_pause(self):
		"""
		Show Pause menu
		"""
		if self.__pause_displayed:
			self.__pause_displayed = False
			self.return_to_game()
		else:
			self.__pause_displayed = True
			self._switch_current_widget('ingamemenu', center=True, show=True, event_map={
				  # icons
				'loadgameButton' : horizons.main.load_game,
				'savegameButton' : self.save_game,
				'settingsLink'   : self.show_settings,
				'helpLink'       : self.on_help,
				'startGame'      : self.return_to_game,
				'closeButton'    : self.quit_session,
				# labels
				'loadgame' : horizons.main.load_game,
				'savegame' : self.save_game,
				'settings' : self.show_settings,
				'help'     : self.on_help,
				'start'    : self.return_to_game,
				'quit'     : self.quit_session,
			})

			self.session.speed_pause()
			self.on_escape = self.toggle_pause

# what happens on button clicks

	def return_to_game(self):
		"""Return to the horizons."""
		self.hide() # Hide old gui
		self.current = None
		self.session.speed_unpause()
		self.on_escape = self.toggle_pause

	def save_game(self):
		"""Wrapper for saving for separating gui messages from save logic
		"""
		success = self.session.save()
		if not success:
			self.show_popup(_('Error'), _('Failed to save.'))

	def show_settings(self):
		horizons.main.fife._setting.onOptionsPress()

	_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']:
				self.session.speed_pause()
			self.show_dialog(help_dlg, {'okButton' : True}, onPressEscape = True)
			self.on_help() # toggle state
		else:
			self._help_is_displayed = False
			if self.session is not None and self.current != self.widgets['ingamemenu']:
				self.session.speed_unpause()
			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.current.hide()
				self.current = None
			if self.session is not None:
				self.session.end()
				self.session = None

			self.show_main()

	def on_chime(self):
		"""
		Called chime action. Displaying call for help on artists and game design,
		introduces information for SoC applicants (if valid).
		"""
		AmbientSound.play_special("message")
		self.show_dialog(self.widgets['call_for_support'], {'okButton' : True}, onPressEscape = 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 == 2: # #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','special_thanks')]
		for i in xrange (0,4):
			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' : True}, onPressEscape = True)

	def show_select_savegame(self, mode):
		"""Shows menu to select a savegame.
		@param mode: 'save' or 'load'
		@return: Path to savegamefile or None"""
		assert mode in ('save', 'load')
		map_files, map_file_display = None, None
		if mode == 'load':
			map_files, map_file_display = SavegameManager.get_saves()
			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
			map_files, map_file_display = SavegameManager.get_regular_saves()

		# Prepare widget
		old_current = self._switch_current_widget('select_savegame')
		self.current.findChild(name='headline').text = _('Save game') if mode == 'save' else _('Load game')

		""" this doesn't work (yet), see http://fife.trac.cvsdude.com/engine/ticket/375
		if mode == 'save': # only show enter_filename on save
			self.current.findChild(name='enter_filename').show()
		else:
			self.current.findChild(name='enter_filename').hide()
		"""

		def tmp_selected_changed():
			"""Fills in the name of the savegame in the textbox when selected in the list"""
			if self.current.collectData('savegamelist') != -1: # check if we actually collect valid data
				self.current.distributeData({'savegamefile' : \
				                             map_file_display[self.current.collectData('savegamelist')]})

		self.current.distributeInitialData({'savegamelist' : map_file_display})
		cb = Callback.ChainedCallbacks(Gui._create_show_savegame_details(self.current, map_files, 'savegamelist'), \
		                               tmp_selected_changed)
		self.current.findChild(name="savegamelist").mapEvents({
		    'savegamelist/action'              : cb,
		    'savegamelist/mouseWheelMovedUp'   : cb,
		    'savegamelist/mouseWheelMovedDown' : cb
		})
		self.current.findChild(name="savegamelist").capture(cb, event_name="keyPressed")

		retval = self.show_dialog(self.current, {
		                                          'okButton'     : True,
		                                          'cancelButton' : False,
		                                          'deleteButton' : 'delete',
		                                          'savegamefile' : True
		                                        },
		                                        onPressEscape = False)
		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
			self._delete_savegame(map_files)
			self.current = old_current
			return self.show_select_savegame(mode=mode)

		selected_savegame = None
		if mode == 'save': # return from textfield
			selected_savegame = self.current.collectData('savegamefile')
			if selected_savegame in map_file_display: # savegamename already exists
				message = _("A savegame with the name \"%s\" already exists. \nShould i overwrite it?") % selected_savegame
				if not self.show_popup(_("Confirmation for overwriting"),message,show_cancel_button = True):
					self.current = old_current
					return self.show_select_savegame(mode=mode) # 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(mode=mode) # reshow dialog
		self.current = old_current # reuse old widget
		return selected_savegame

# 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()

	def show_dialog(self, dlg, actions, onPressEscape = None, event_map = None):
		"""Shows any pychan dialog.
		@param dlg: dialog that is to be shown
		@param actions: actions that are executed by the dialog { 'ok': callback, 'cancel': callback }
		@param onPressEscape: callback that is to be called if the escape button is pressed
		@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)
		if onPressEscape is not None:
			def _escape(event):
				if event.getKey().getValue() == fife.Key.ESCAPE:
					pychan.internal.get_manager().breakFromMainLoop(onPressEscape)
					dlg.hide()
			dlg.capture(_escape, event_name="keyPressed")
		ret = dlg.execute(actions)
		return ret

	def show_popup(self, windowtitle, message, show_cancel_button = False):
		"""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
		@return: True on ok, False on cancel (if no cancel button, always True)
		"""
		popup = self.build_popup(windowtitle, message, show_cancel_button)
		if show_cancel_button:
			return self.show_dialog(popup, {'okButton' : True, 'cancelButton' : False}, onPressEscape = False)
		else:
			return self.show_dialog(popup, {'okButton' : True}, onPressEscape = True)

	def show_error_popup(self, windowtitle, description, advice=None, details=None):
		"""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

		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
		self.show_popup( _(u"Error:") + u" " + windowtitle, msg, show_cancel_button=False)

	def build_popup(self, windowtitle, message, show_cancel_button = False):
		""" 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
		@return: Container(name='popup_window') with buttons 'okButton' and optionally 'cancelButton'
		"""
		# 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.
		if show_cancel_button:
			self.widgets.reload('popup_with_cancel')
			popup = self.widgets['popup_with_cancel']
		else:
			self.widgets.reload('popup')
			popup = self.widgets['popup']
		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)
			old.hide()
		self.log.debug("Gui: setting current to %s", new_widget)
		self.current = self.widgets[new_widget]
		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"""
			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
			try:
				map_file = map_files[gui.collectData(savegamelist)]
			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)
			details_label = pychan.widgets.Label(min_size=(140, 0), max_size=(140, 290), wrap_text=True)
			details_label.name = "savegamedetails_lbl"
			details_label.text = u""
			if savegame_info['timestamp'] == -1:
				details_label.text += _("Unknown savedate\n")
			else:
				details_label.text += _("Saved at %s\n") % \
										time.strftime("%H:%M, %A, %B %d", time.localtime(savegame_info['timestamp']))
			counter = savegame_info['savecounter']
			# N_ takes care of plural forms for different languages
			details_label.text += N_("Saved %(counter)d time\n", \
			                         "Saved %(counter)d times\n", \
			                         counter) % {'counter':counter}
			details_label.stylize('book_t')

			from horizons.constants import VERSION
			try:
				if savegame_info['savegamerev'] == VERSION.SAVEGAMEREVISION:
					details_label.text += _("Savegame ver. %d") % ( savegame_info['savegamerev'] )
				else:
					details_label.text += _("WARNING: Incompatible ver. %(ver)d!\nNeed ver. %(need)d!") \
					             % {'ver' : savegame_info['savegamerev'], 'need' : VERSION.SAVEGAMEREVISION}
			except KeyError:
				details_label.text += _("INCOMPATIBLE VERSION\n")


			box.addChild( details_label )

			"""
			if savegame_info['screenshot']:
				fd, filename = tempfile.mkstemp()
				os.fdopen(fd, "w").write(savegame_info['screenshot'])
				box.addChild( pychan.widgets.Icon(image=filename) )
			"""

			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]
		message = _('Do you really want to delete the savegame "%s"?') % \
		             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