view tools/editor/plugins/ObjectEdit.py @ 378:64738befdf3b

bringing in the changes from the build_system_rework branch in preparation for the 0.3.0 release. This commit will require the Jan2010 devkit. Clients will also need to be modified to the new way to import fife.
author vtchill@33b003aa-7bff-0310-803a-e67f0ece8222
date Mon, 11 Jan 2010 23:34:52 +0000
parents
children fa1373b9fa16
line wrap: on
line source

# coding: utf-8
# ###################################################
# Copyright (C) 2008 The Zero-Projekt team
# http://zero-projekt.net
# info@zero-projekt.net
# This file is part of Zero "Was vom Morgen blieb"
#
# The Zero-Projekt codebase is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
# ###################################################

""" a tool for FIFEdit to edit object and instance attributes """

from fife import fife
from fife.extensions import pychan
import fife.extensions.pychan.widgets as widgets
from fife.extensions.pychan.tools import callbackWithArguments as cbwa

from fife.extensions.fife_timer import Timer

import scripts
import scripts.plugin as plugin
from scripts.events import *
from scripts.gui.action import Action


import os
try:
	import xml.etree.cElementTree as ET
except:
	import xml.etree.ElementTree as ET

import math

WHITE = {
	"r"	:	205,
	"g"	:	205,
	"b"	:	205
}
OUTLINE_SIZE = 1

class ObjectEdit(plugin.Plugin):
	""" The B{ObjectEdit} module is a plugin for FIFedit and allows to edit
	attributes of an selected instance - like offset, instance id or rotation
	(namespaces and object id editing is excluded)
	
	current features:
		- click instance and get all known data
		- edit offsets, rotation, instance id
		- save offsets to object file
		- outline highlighting of the selected object
		- animation viewer

	FIXME:
		- add static and blocking flag to save routine
		- fix animation rotation (FIFE has no method yet to export all 
		  angles of an animation to python)
	"""
	def __init__(self):
		self.active = False
		self._camera = None
		self._layer = None
		self._anim_timer = None
		
		self._enabled = False
		
		self.imagepool = None
		self._animationpool = None
		
		self.guidata = {}
		self.objectdata = {}

	def _reset(self):
		"""
			resets all dynamic vars, but leaves out static ones (e.g. camera, layer)

		"""
		if self._anim_timer:
			self._anim_timer.stop()
			# reset the ToggleButton
			if self._gui_anim_playback._isToggled():
				self._gui_anim_playback._setToggled(0)
		self._anim_timer = None
		
		self._object = None
		self._instances = None
		self._image = None
		self._image_default_x_offset = None
		self._image_default_y_offset = None
		self._animation = False
		self._anim_data = {}
		self._rotation = None
		self._avail_rotations = []
		self._namespace = None	
		self._blocking = 0
		self._static = 0
		self._object_id = None	
		self._instance_id = None
		self._fixed_rotation = None
		
		if self._camera is not None:
			self.renderer.removeAllOutlines()			

	def enable(self):
		""" plugin method """
		if self._enabled is True:
			return
			
		self._editor = scripts.editor.getEditor()
		self.engine = self._editor.getEngine()
		
		self.imagepool = self.engine.getImagePool()
		self._animationpool = self.engine.getAnimationPool()
		
		self._showAction = Action(unicode(self.getName(),"utf-8"), checkable=True)
		scripts.gui.action.activated.connect(self.toggle_gui, sender=self._showAction)
		
		self._editor._tools_menu.addAction(self._showAction)
		
		events.onInstancesSelected.connect(self.input)
		
		self._reset()		
		self.create_gui()

	def disable(self):
		""" plugin method """
		if self._enabled is False:
			return
			
		self._reset()
		self.container.hide()
		self.removeAllChildren()
		
		events.onInstancesSelected.disconnect(self.input)
		
		self._editor._tools_menu.removeAction(self._showAction)

	def isEnabled(self):
		""" plugin method """
		return self._enabled;

	def getName(self):
		""" plugin method """
		return "Object editor v2"

	def create_gui(self):
		"""
			- creates the gui skeleton by loading the xml file
			- finds some important childs and saves their widget in the object
			
		FIXME:
			- move all dynamic widgets to dict
		"""
		self.container = pychan.loadXML('gui/objectedit.xml')
		self.container.mapEvents({
			'x_offset_up' 		: cbwa(self.change_offset_x, 1),
			'x_offset_dn' 		: cbwa(self.change_offset_x, -1),
			
			'y_offset_up' 		: cbwa(self.change_offset_y, 1),
			'y_offset_dn' 		: cbwa(self.change_offset_y, -1),
			
			'use_data'			: self.use_user_data,
			'change_data'		: self.save_user_data,
			
			'anim_left'			: self.previous_anim_frame,
			'anim_right'		: self.next_anim_frame,
			'anim_start_pos' 	: self.anim_start_frame,
			'anim_end_pos'		: self.anim_end_frame,
		})

		self._gui_anim_panel_wrapper = self.container.findChild(name="animation_panel_wrapper")
		self._gui_anim_panel = self._gui_anim_panel_wrapper.findChild(name="animation_panel")
		
		self._gui_rotation_dropdown = self.container.findChild(name="select_rotations")
		self._gui_rotation_dropdown.capture(self.gui_rotate_instance,"mouseWheelMovedUp")
		self._gui_rotation_dropdown.capture(self.gui_rotate_instance,"mouseWheelMovedDown")
		self._gui_rotation_dropdown.capture(self.gui_rotate_instance,"action")	
		
		self._gui_anim_actions_dropdown = self._gui_anim_panel_wrapper.findChild(name="select_actions")	
		self._gui_anim_actions_dropdown.capture(self.eval_gui_anim_action,"mouseWheelMovedUp")
		self._gui_anim_actions_dropdown.capture(self.eval_gui_anim_action,"mouseWheelMovedDown")
		self._gui_anim_actions_dropdown.capture(self.eval_gui_anim_action,"action")	
		
		self._gui_anim_playback = self._gui_anim_panel_wrapper.findChild(name="anim_playback")
		self._gui_anim_playback.capture(self.anim_playback, "mousePressed")
		self._gui_anim_loop = self._gui_anim_panel_wrapper.findChild(name="anim_loop")

		self._gui_current_frame = self._gui_anim_panel_wrapper.findChild(name="anim_current_frame")
		self._gui_current_frame.capture(self.previous_anim_frame,"mouseWheelMovedUp")
		self._gui_current_frame.capture(self.next_anim_frame,"mouseWheelMovedDown")	
		
		self._gui_xoffset_textfield = self.container.findChild(name="x_offset")
		self._gui_yoffset_textfield = self.container.findChild(name="y_offset")
		
		self._gui_instance_id_textfield = self.container.findChild(name="instance_id")

	def anim_playback(self, widget):
		""" start / stop playback of an animation due to status of a gui ToggleButton
			Sets also two ivars of timer object (active & loop)
		"""
		if widget._isToggled():
			self._anim_timer.stop()
			self._anim_timer.active = False
		else:
			frame_delay = self._anim_data['obj'].getFrameDuration(self._anim_data['current'])
			self._anim_timer = Timer(delay=frame_delay,callback=self.next_anim_frame)
			self._anim_timer.active = True
			self._anim_timer.loop = self._gui_anim_loop._isMarked()
			self._anim_timer.start()
	
	def previous_anim_frame(self):
		""" show previous anim frame """
		if self._anim_data['current'] > 0:
			self._anim_data['current'] -= 1
			self.update_gui()
		
	def next_anim_frame(self):
		""" show next anim frame and reset animation frame to 0 if playback looping is active"""
		if self._anim_timer.loop and (self._anim_data['current'] == self._anim_data['frames']):
			self._anim_data['current'] = 0
		
		if self._anim_data['current'] < self._anim_data['frames']:
			self._anim_data['current'] += 1
			self.update_gui()
		
	def anim_start_frame(self):
		""" set start frame of animation """
		self._anim_data['current'] = 0
		self.update_gui()

	def anim_end_frame(self):
		""" set end frame of animation """		
		self._anim_data['current'] = self._anim_data['frames']
		self.update_gui()
	
	def set_default_offset(self, axis):
		""" set default image offset for given axis """
		if axis == 'x':
			self.set_offset(x=self._image_default_x_offset)
		elif axis == 'y':
			self.set_offset(y=self._image_default_y_offset)

	def update_gui(self):
		"""
			updates the gui widgets with current instance data
			
		"""
		# show the image we retrieved from an animated object
		if self._animation:
			if not self._gui_anim_panel_wrapper.findChild(name="animation_panel"):
				self._gui_anim_panel_wrapper.addChild(self._gui_anim_panel)			

			# get current selected image and update the icon widget				
			dur = 0
			for i in range(self._anim_data['frames']):
				dur += self._anim_data['obj'].getFrameDuration(i)
				
				# set new duration for the playback timer
				if self._anim_timer:
					frame_delay = self._anim_data['obj'].getFrameDuration(self._anim_data['current'])
				
				if i == self._anim_data['current']:
					# set new duration for the playback timer
					if self._anim_timer and self._anim_timer.active:
						self._anim_timer.setPeriod(self._anim_data['obj'].getFrameDuration(self._anim_data['current']))		
					break
											
			image = self._anim_data['obj'].getFrameByTimestamp(dur)	
			self.container.findChild(name="animTest").image = image.getResourceFile()
			self.container.findChild(name="animTest").size= (250,250)
			self.container.findChild(name="animTest").min_size= (250,250)
		
			self.container.distributeInitialData({
				'anim_current_frame'	:	unicode(str(self._anim_data['current'])),
				'anim_rotation'			:	unicode(str(self._anim_data['obj'].getDirection())),
			})	

		else:
			if self._gui_anim_panel_wrapper.findChild(name="animation_panel"):
				self._gui_anim_panel_wrapper.removeChild(self._gui_anim_panel)			
			
		if self._image is not None:
			x_offset = unicode( self._image.getXShift() )
			y_offset = unicode( self._image.getYShift() )
		else:
			x_offset = unicode( 0 )
			y_offset = unicode( 0 )
			
		self.container.distributeInitialData({
			'select_rotations' 	: self._avail_rotations,
			'instance_id'		: unicode( self._instances[0].getId() ),
			'object_id'			: unicode( self._object_id ),
			'x_offset'			: x_offset,
			'y_offset'			: y_offset,
			'instance_rotation' : unicode( self._instances[0].getRotation() ),
			'object_namespace'	: unicode( self._namespace ),
			'object_blocking'	: unicode( self._blocking ),
			'object_static'		: unicode( self._static ),
		})
		
		if not self._animation:
			if self._fixed_rotation in self._avail_rotations:
				index = self._avail_rotations.index( self._fixed_rotation )
				self._gui_rotation_dropdown._setSelected(index)
			else:
				print "Internal FIFE rotation: ", self._instances[0].getRotation()
				print "Fixed rotation (cam rot) ", self._fixed_rotation + int(abs(self._camera.getRotation()))
				print "Collected rots from object ", self._avail_rotations
				

		self.container.adaptLayout(False)			
		
	def toggle_gui(self):
		"""
			show / hide the gui
		"""
		if self.active is True:
			self.active = False
			if self.container.isVisible() or self.container.isDocked():
				self.container.setDocked(False)
				self.container.hide()
			self._showAction.setChecked(False)
		else:
			self.active = True
			self._showAction.setChecked(True)
	
	def highlight_selected_instance(self):
		""" highlights selected instance """
		self.renderer.removeAllOutlines() 
		self.renderer.addOutlined(self._instances[0], WHITE["r"], WHITE["g"], WHITE["b"], OUTLINE_SIZE)		
			
	def change_offset_x(self, value=1):
		"""
			- callback for changing x offset
			- changes x offset of current instance (image)
			- updates gui
			
			@type	value:	int
			@param	value:	the modifier for the x offset
		"""		
		if self._animation:
			print "Offset changes of animations are not supported yet"
			return
		
		if self._image is not None:
			self._image.setXShift(self._image.getXShift() + value)
			self.update_gui()

	def change_offset_y(self, value=1):
		"""
			- callback for changing y offset
			- changes y offset of current instance (image)
			- updates gui
			
			@type	value:	int
			@param	value:	the modifier for the y offset
		"""
		if self._animation:
			print "Offset changes of animations are not supported yet"
			return
		
		if self._image is not None:
			self._image.setYShift(self._image.getYShift() + value)
			self.update_gui()

	def use_user_data(self):
		"""
			- takes the users values and applies them directly to the current ._instance
			- writes current data record
			- writes previous data record
			- updates gui
		
			FIXME:
			- parse user data in case user think strings are considered to be integer offset values...
		"""
		if self._animation:
			print "Editing animated instances is not supported yet"
			return		
		
		xoffset = self._gui_xoffset_textfield._getText()
		yoffset = self._gui_yoffset_textfield._getText()
		
		instance_id = str(self._gui_instance_id_textfield._getText())
		
		if instance_id == "":
			instance_id = "None"

		if instance_id is not None and instance_id is not "None":
			existing_instances = self._editor.getActiveMapView().getController()._layer.getInstances(instance_id)
			if len(existing_instances) <= 0:
				self._instances[0].setId(instance_id)
				print "Set new instance id: ", instance_id		
			else:
				print "Instance ID is already in use."
		
		# update rotation
		angle = self.eval_gui_rotation()
		self.set_rotation(angle)
		
		# update offsets
		self.set_offset(int(xoffset), int(yoffset))

		self.update_gui()
		
	def save_user_data(self):
		""" saves the current object to its xml file 
		
			NOTE:
				- animations can't be saved for now
				
			FIXME:
				- add missing object attributes to saving routine		
		"""
		if self._object is None:
			return
		if self._animation:
			return
		
		file = self._object.getResourceFile()	
		self.tree = ET.parse(file)
		
		img_lst = self.tree.findall("image")
		
		# apply changes to the XML structure due to current user settings in the gui
		for img_tag in img_lst:
			if img_tag.attrib["direction"] == self._avail_rotations[self._gui_rotation_dropdown._getSelected()]:
				img_tag.attrib["x_offset"] = self._gui_xoffset_textfield._getText()
				img_tag.attrib["y_offset"] = self._gui_yoffset_textfield._getText()
				break

		xmlcontent = ET.tostring(self.tree.getroot())
		
		# save xml data beneath the <?fife type="object"?> definition into the object file
		tmp = open(file, 'w')
		tmp.write('<?fife type="object"?>\n')
		tmp.write(xmlcontent + "\n")
		tmp.close()
		
	def gui_rotate_instance(self):
		""" rotate an instance due to selected angle """
		angle = self.eval_gui_rotation()
		self.set_rotation(angle)
		
	def eval_gui_rotation(self):
		""" prepare rotation from gui and apply it to the current selected instance """
		index = self._gui_rotation_dropdown._getSelected()
		angle = int( self._avail_rotations[index] )
	
		if angle == 360:
			angle = 0
			
		return angle
		
	def eval_gui_anim_action(self):	
		""" check the selected action of an animation and update the gui accordingly """
		if not self._anim_data['actions']: return
		
		index = self._gui_anim_actions_dropdown._getSelected()
		action = self._anim_data['actions'][index]
		
		self.update_anim_data(action)
		self.update_gui()
		
	def set_rotation(self, angle):
		""" set the rotation of the current instance """	
#		print "...setting instance rotation from %s to %s" % (self._rotation, angle)
		self._instances[0].setRotation(angle)
		self.get_instance_data(None, None, angle)
		self.update_gui()	
#		print "...new internal FIFE rotation ", int(self._instances[0].getRotation())
		
	def set_offset(self, x=None, y=None):
		""" set x/y offset of current selected instance """
		if x is not None:
			self._image.setXShift(x)
		if y is not None:
			self._image.setYShift(y)
			
	def update_anim_data(self, action=None):
		""" update animation data for the current selected instance from FIFE's data structure
		
		@type	animation	FIFE animation
		@return	animation	current selected animation
		"""
		if action:
			animation_id = action.get2dGfxVisual().getAnimationIndexByAngle(self._fixed_rotation)
			animation = self._animationpool.getAnimation(animation_id)		

		action_ids = []
		actions = []
		
		try:
			action_ids = self._object.getActionIds()
			for id in action_ids:
				actions.append(self._object.getAction(id))
		except:
			pass		
		
		self._anim_data = {}
		self._anim_data['obj'] = animation
		self._anim_data['id'] = animation_id
		self._anim_data['frames'] = animation.getNumFrames()
		self._anim_data['current'] = 0
		self._anim_data['actions'] = actions
		self._anim_data['action_ids'] = action_ids
		self._anim_data['default_action'] = self._object.getDefaultAction()	
		self._anim_data['action'] = action		

		return animation
		
	def get_instance_data(self, timestamp=None, frame=None, angle=-1, instance=None):
		"""
			- grabs all available data from both object and instance
		
			FIXME:
				1.) we need to fix the instance rotation / rotation issue
		"""
		visual = None
		self._avail_rotations = []
			
		if instance is None:
			instance = self._instances[0]
			
		object = instance.getObject()
		self._object = object
		self._namespace = object.getNamespace()
		self._object_id = object.getId()
		
		self._instance_id = instance.getId()
	
		if self._instance_id == '':
			self._instance_id = 'None'

		if angle == -1:
			angle = int(instance.getRotation())
		else:
			angle = int(angle)	
			
		self._rotation = angle
		
		if object.isBlocking():
			self._blocking = 1
			
		if object.isStatic():
			self._static = 1
		
		try:
			visual = object.get2dGfxVisual()
		except:
			print 'Fetching visual of object - failed. :/'
			raise			

#		print "Camera tilt: ", self._camera.getTilt()
#		print "Camera rotation: ", self._camera.getRotation()
#		print "Instance rotation: ", instance.getRotation()

#		self._fixed_rotation = int(instance.getRotation() + abs( self._camera.getRotation() ) )		
		self._fixed_rotation = instance.getRotation()
#		self._fixed_rotation = visual.getClosestMatchingAngle(self._fixed_rotation)	

		index = visual.getStaticImageIndexByAngle(self._fixed_rotation)

		if index is -1:
			# object is an animation
			self._animation = True
			self._image = None
			
			# no static image available, try default action
			action = object.getDefaultAction()

			if action:
				animation = self.update_anim_data(action)

				# update gui
				if animation:
					self._gui_anim_actions_dropdown._setItems(self._anim_data['action_ids'])
					self._gui_anim_actions_dropdown._setSelected(0)					
				
				if timestamp is None and frame is not None:
					self._image = animation.getFrame(frame)	
				elif timestamp is not None and frame is None:
					self._image = animation.getFrameByTimestamp(timestamp)
				else:
					self._image = animation.getFrameByTimestamp(0)
		elif index is not -1:
			# object is a static image
			self._animation = False
			self._image = self.imagepool.getImage(index)

		if not self._animation:
			rotations = visual.getStaticImageAngles()
			for angle in rotations:
#				angle += int(abs( self._camera.getRotation() ))
				self._avail_rotations.append(angle)
	
			self._image_default_x_offset = self._image.getXShift()
			self._image_default_y_offset = self._image.getYShift()
		else:
			# these doesn't work correctly
#			rotations = [0,60,120,180,240,300]

			# testbench to get valid angles
#			angle = 0
#			rotations = []
#			while angle != 360:
#				angle += 10
#				rotations.append(angle)

			# estimated angles (for hex!) to make things work - use testbench to test 
			# various angles and note down the working ones (watch instance 
			# rotation and the animation rotations shown in the gui; valid
			# angles are given once the rotations are in sync
			self._avail_rotations = [9,69,139,169,249,319]

	def input(self, instances):
		"""
			if called _and_ the user wishes to edit offsets,
			gets instance data and show gui
			
		"""
		if instances != self._instances:
			if self.active is True:
				self._reset()
				self._instances = instances
				
				if self._camera is None:
					self._camera = self._editor.getActiveMapView().getCamera()
					self.renderer = fife.InstanceRenderer.getInstance(self._camera)				
					
				self._layer = self._editor.getActiveMapView().getController()._layer
			
				if self._instances != ():
					self.highlight_selected_instance()
					self.get_instance_data()
					self.update_gui()
					self.container.show()
				else:
					self._reset()
					self.container.hide()
					
		self.container.adaptLayout(False)