view engine/python/fife/extensions/fife_settings.py @ 499:3dff106b945b

Combined the settings extension with the editor settings module. It is now a little more robust. Note that the settings file format has changed. All demos and tools now use the new settings extension.
author prock@33b003aa-7bff-0310-803a-e67f0ece8222
date Fri, 14 May 2010 17:37:42 +0000
parents 559a26347730
children ee65aa323457
line wrap: on
line source

# -*- coding: utf-8 -*-

# ####################################################################
#  Copyright (C) 2005-2010 by the FIFE team
#  http://www.fifengine.net
#  This file is part of FIFE.
#
#  FIFE is free software; you can redistribute it and/or
#  modify it under the terms of the GNU Lesser General Public
#  License as published by the Free Software Foundation; either
#  version 2.1 of the License, or (at your option) any later version.
#
#  This library is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#  Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public
#  License along with this library; if not, write to the
#  Free Software Foundation, Inc.,
#  51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
# ####################################################################

"""
Settings
==================================

This module provides a nice framework for loading and saving game settings.
It is by no means complete but it does provide a good starting point.

@note:  Please note that you MUST provide a default settings-dist.xml file
in the root directory of your project for this module to function correctly.
"""

import shutil
import os
from StringIO import StringIO

from fife.extensions import pychan
from fife.extensions.fife_utils import getUserDataDirectory
try:
	import xml.etree.cElementTree as ET
except:
	import xml.etree.ElementTree as ET


SETTINGS_GUI_XML="""\
<Window name="Settings" title="Settings">
	<Label text="Settings menu!" />
	<HBox>
		<VBox>
			<Label text="Resolution:" />
			<Label text="Renderer:" />
		</VBox>
		<VBox min_size="120,60">
			<DropDown name="screen_resolution" min_size="120,0" />
			<DropDown name="render_backend" min_size="120,0" />
		</VBox>
	</HBox>
	<CheckBox name="enable_fullscreen" text="Use the full screen mode" />
	<CheckBox name="enable_sound" text="Enable sound" />
	<HBox>
		<Spacer />
		<Button name="cancelButton" text="Cancel" />
		<Button name="okButton" text="Ok" />
		<Button name="defaultButton" text="Defaults" />
	</HBox>
</Window>
"""

CHANGES_REQUIRE_RESTART="""\
<Window title="Changes require restart">
	<Label text="Some of your changes require you to restart." />
	<HBox>
		<Spacer />
		<Button name="closeButton" text="Ok" />
	</HBox>
</Window>
"""

class Setting(object):
	"""
	This class manages loading and saving of game settings.
	
	Usage::
		from fife.extensions.fife_settings import Setting
		settings = Setting(app_name="myapp")
		screen_width = settings.readSetting("ScreenWidth")
	"""
	
	def __init__(self, app_name="", settings_file="", settings_gui_xml=""):
		"""
		Initializes the Setting object.
		
		@param app_name: The applications name.  If this parameter is provided 
		alone it will try to read the settings file from the users home directory. 
		In windows this will be	something like:	C:\Documents and Settings\user\Application Data\fife
		@type app_name: C{string}
		@param settings_file: The name of the settings file.  If this parameter is
		provided it will look for the setting file as you specify it, first looking 
		in the working directory.
		@type settings_file: C{string}
		@param settings_gui_xml: If you specify this parameter you can customize the look
		of the settings dialog box.
		@note: As of now you MUST have all the elements of the default settings dialog box.
		At some point we may make it customizable.
		
		"""
		self._app_name = app_name
		self._settings_file = settings_file
		self._settings_gui_xml = settings_gui_xml
	
		if self._settings_file == "":
			self._settings_file = "settings.xml"
			self._appdata = getUserDataDirectory("fife", self._app_name)
		else:
			self._appdata = os.path.dirname(self._settings_file)
			self._settings_file = os.path.basename(self._settings_file)

		
		if self._settings_gui_xml == "":
			self.settings_gui_xml = SETTINGS_GUI_XML
		
		
		if not os.path.exists(os.path.join(self._appdata, self._settings_file)):
			shutil.copyfile('settings-dist.xml', os.path.join(self._appdata, self._settings_file))
		
		self._tree = ET.parse(os.path.join(self._appdata, self._settings_file))
		self._root_element = self._tree.getroot()
		self.validateTree()

	def validateTree(self):
		""" Iterates the settings tree and prints warning when an invalid tag is found """
		for c in self._root_element.getchildren():
			if c.tag != "Module":
				print "Invalid tag in settings.xml. Expected Module, got: ", c.tag
			elif c.get("name", "") == "":
				print "Invalid tag in settings.xml. Module name is empty."
			else:
				for e in c.getchildren():
					if e.tag != "Setting":
						print "Invalid tag in settings.xml in module: ",c.tag,
						print ". Expected Setting, got: ", e.tag
					elif c.get("name", "") == "":
						print "Invalid tag in settings.xml in module: ",c.tag,
						print ". Setting name is empty", e.tag
		
	def getModuleTree(self, module):
		""" 
		Returns a module element from the settings tree. If no module with the specified
		name exists, a new element will be created. 
		
		@param module: The module to get from the settings tree
		@type module: C{string}
		"""
		if not isinstance(module, str) and not isinstance(module, unicode):
			raise AttributeError("Settings:getModuleTree: Invalid type for module argument.")
			
		for c in self._root_element.getchildren():
			if c.tag == "Module" and c.get("name", "") == module:
				return c
	
		# Create module
		return ET.SubElement(self._root_element, "Module", {"name":module})

	def get(self, module, name, defaultValue=None):
		""" Gets the value of a specified setting
		
		@param module: Name of the module to get the setting from
		@param name: Setting name
		@param defaultValue: Specifies the default value to return if the setting is not found
		@type defaultValue: C{str} or C{unicode} or C{int} or C{float} or C{bool} or C{list} or C{dict}
		"""
		if not isinstance(name, str) and not isinstance(name, unicode):
			raise AttributeError("Settings:get: Invalid type for name argument.")
		
		moduleTree = self.getModuleTree(module)
		element = None
		for e in moduleTree.getchildren():
			if e.tag == "Setting" and e.get("name", "") == name:
				element = e
				break
		else: 
			return defaultValue
		
		e_value = element.text
		e_strip = element.get("strip", "1").strip().lower()
		e_type	= str(element.get("type", "str")).strip()
		
		if e_value is None: 
			return defaultValue
		
		# Strip value
		if e_strip == "" or e_strip == "false" or e_strip == "no" or e_strip == "0":
			e_strip = False
		else: e_strip = True
		
		if e_type == "str" or e_type == "unicode":
			if e_strip: e_value = e_value.strip()
		else:
			e_value = e_value.strip()
		
		# Return value
		if e_type == 'int':
			return int(e_value)
		elif e_type == 'float':
			return float(e_value)
		elif e_type == 'bool':
			e_value = e_value.lower()
			if e_value == "" or e_value == "false" or e_value == "no" or e_value == "0":
				return False
			else:
				return True
		elif e_type == 'str':
			return str(e_value)
		elif e_type == 'unicode':
			return unicode(e_value)
		elif e_type == 'list':
			return self._deserializeList(e_value)
		elif e_type == 'dict':
			return self._deserializeDict(e_value)

	def set(self, module, name, value, extra_attrs={}):
		"""
		Sets a setting to specified value.
		
		@param module: Module where the setting should be set
		@param name: Name of setting
		@param value: Value to assign to setting
		@type value: C{str} or C{unicode} or C{int} or C{float} or C{bool} or C{list} or C{dict}
		@param extra_attrs: Extra attributes to be stored in the XML-file
		@type extra_attrs: C{dict}
		"""
		if not isinstance(name, str) and not isinstance(name, unicode):
			raise AttributeError("Settings:set: Invalid type for name argument.")
			
		moduleTree = self.getModuleTree(module)
		e_type = "str"
		
		if isinstance(value, bool): # This must be before int
			e_type = "bool"
			value = str(value)
		elif isinstance(value, int):
			e_type = "int"
			value = str(value)
		elif isinstance(value, float):
			e_type = "float"
			value = str(value)
		elif isinstance(value, unicode):
			e_type = "unicode"
			value = unicode(value)
		elif isinstance(value, list):
			e_type = "list"
			value = self._serializeList(value)
		elif isinstance(value, dict):
			e_type = "dict"
			value = self._serializeDict(value)
		else:
			e_type = "str"
			value = str(value)
		
		for e in moduleTree.getchildren():
			if e.tag != "Setting": continue
			if e.get("name", "") == name:
				e.text = value
				break
		else:
			attrs = {"name":name, "type":e_type}
			for k in extra_attrs:
				if k not in attrs:
					attrs[k] = extra_args[k]
			elm = ET.SubElement(moduleTree, "Setting", attrs)
			elm.text = value

	def saveSettings(self):
		""" Writes the settings to the settings file """
		self._indent(self._root_element)
		self._tree.write(os.path.join(self._appdata, self._settings_file), 'UTF-8')
		
	def _indent(self, elem, level=0):
		""" 
		Adds whitespace, so the resulting XML-file is properly indented.
		Shamelessly stolen from http://effbot.org/zone/element-lib.htm 
		"""
		i = "\n" + level*"  "
		if len(elem):
			if not elem.text or not elem.text.strip():
				elem.text = i + "  "
			if not elem.tail or not elem.tail.strip():
				elem.tail = i
			for elem in elem:
				self._indent(elem, level+1)
			if not elem.tail or not elem.tail.strip():
				elem.tail = i
		else:
			if level and (not elem.tail or not elem.tail.strip()):
				elem.tail = i
		
	# FIXME:
	# These serialization functions are not reliable at all
	# This will only serialize the first level of a dict or list
	# It will not check the types nor the content for conflicts.
	# Perhaps we should add a small serialization library?
	def _serializeList(self, list):
		""" Serializes a list, so it can be stored in a text file """
		return " ; ".join(list)
		
	def _deserializeList(self, string):
		""" Deserializes a list back into a list object """
		return string.split(" ; ")

	def _serializeDict(self, dict):
		""" Serializes a list, so it can be stored in a text file """
		serial = ""
		for key in dict:
			value = dict[key]
			if serial != "": serial += " ; "
			serial += str(key)+" : "+str(value)
			
		return serial
	
	def _deserializeDict(self, serial):
		""" Deserializes a list back into a dict object """
		dict = {}
		items = serial.split(" ; ")
		for i in items:
			kv_pair = i.split(" : ")
			dict[kv_pair[0]] = kv_pair[1]
		return dict

	def onOptionsPress(self):
		"""
		Opens the options dialog box.  Usually you would bind this to a button.
		"""
		self.changesRequireRestart = False
		self.isSetToDefault = False
		self.Resolutions = ['640x480', '800x600', '1024x768', '1280x800', '1440x900']
		if not hasattr(self, 'OptionsDlg'):
			self.OptionsDlg = None
		if not self.OptionsDlg:
			self.OptionsDlg = pychan.loadXML(StringIO(SETTINGS_GUI_XML))
			self.OptionsDlg.distributeInitialData({
				'screen_resolution' : self.Resolutions,
				'render_backend' : ['OpenGL', 'SDL']
			})
			self.OptionsDlg.distributeData({
				'screen_resolution' : self.Resolutions.index(str(self.get("FIFE", "ScreenWidth")) + 'x' + str(self.get("FIFE", "ScreenHeight"))),
				'render_backend' : 0 if self.get("FIFE", "RenderBackend") == "OpenGL" else 1,
				'enable_fullscreen' : self.get("FIFE", "FullScreen"),
				'enable_sound' : self.get("FIFE", "PlaySounds")
			})
			self.OptionsDlg.mapEvents({
				'okButton' : self.applySettings,
				'cancelButton' : self.OptionsDlg.hide,
				'defaultButton' : self.setDefaults
			})
		self.OptionsDlg.show()

	def applySettings(self):
		"""
		Writes the settings file.  If a change requires a restart of the engine
		it notifies you with a small dialog box.
		"""
		screen_resolution, render_backend, enable_fullscreen, enable_sound = self.OptionsDlg.collectData('screen_resolution', 'render_backend', 'enable_fullscreen', 'enable_sound')
		render_backend = 'OpenGL' if render_backend is 0 else 'SDL'
		if render_backend != self.get("FIFE", "RenderBackend"):
			self.set("FIFE", 'RenderBackend', render_backend)
			self.changesRequireRestart = True
		if int(enable_fullscreen) != int(self.get("FIFE", "FullScreen")):
			self.set("FIFE", 'FullScreen', int(enable_fullscreen))
			self.changesRequireRestart = True
		if int(enable_sound) != int(self.get("FIFE", "PlaySounds")):
			self.set("FIFE", 'PlaySounds', int(enable_sound))
			self.changesRequireRestart = True
		if screen_resolution != self.Resolutions.index(str(self.get("FIFE", "ScreenWidth")) + 'x' + str(self.get("FIFE", "ScreenHeight"))):
			self.set("FIFE", 'ScreenWidth', int(self.Resolutions[screen_resolution].partition('x')[0]))
			self.set("FIFE", 'ScreenHeight', int(self.Resolutions[screen_resolution].partition('x')[2]))
			self.changesRequireRestart = True

		if not self.isSetToDefault:
			self.saveSettings()
			
		self.OptionsDlg.hide()
		if self.changesRequireRestart:
			RestartDlg = pychan.loadXML(StringIO(CHANGES_REQUIRE_RESTART))
			RestartDlg.mapEvents({ 'closeButton' : RestartDlg.hide })
			RestartDlg.show()

	def setDefaults(self):
		"""
		Overwrites the setting file with the default settings-dist.xml file.
		"""
		shutil.copyfile('settings-dist.xml', os.path.join(self._appdata, self._settings_file))
		self.isSetToDefault = True
		self.changesRequireRestart = True