diff 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 diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/editor/plugins/ObjectEdit.py	Mon Jan 11 23:34:52 2010 +0000
@@ -0,0 +1,653 @@
+# 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)