view tools/editor/plugins/ObjectEdit.py @ 697:ecaa4d98f05f tip

Abstracted the GUI code and refactored the GUIChan-specific code into its own module. * Most of the GUIChan code has been refactored into its own gui/guichan module. However, references to the GuiFont class still persist in the Engine and GuiManager code and these will need further refactoring. * GuiManager is now an abstract base class which specific implementations (e.g. GUIChan) should subclass. * The GUIChan GUI code is now a concrete implementation of GuiManager, most of which is in the new GuiChanGuiManager class. * The GUI code in the Console class has been refactored out of the Console and into the GUIChan module as its own GuiChanConsoleWidget class. The rest of the Console class related to executing commands was left largely unchanged. * Existing client code may need to downcast the GuiManager pointer received from FIFE::Engine::getGuiManager() to GuiChanGuiManager, since not all functionality is represented in the GuiManager abstract base class. Python client code can use the new GuiChanGuiManager.castTo static method for this purpose.
author M. George Hansen <technopolitica@gmail.com>
date Sat, 18 Jun 2011 00:28:40 -1000
parents 4f36c890b1dd
children
line wrap: on
line source

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

# ####################################################################
#  Copyright (C) 2005-2010 by the FIFE team
#  http://www.fifengine.de
#  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
# ####################################################################

""" 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
	"""
	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 = {}
		
		self._help_dialog = None

	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._object_blocking = 0
		self._instance_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)
		events.preMapClosed.connect(self.hide)
		events.preMapShown.connect(self.hide)
		
		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)
		events.preMapClosed.disconnect(self.hide)
		events.preMapShown.disconnect(self.hide)
		
		self._editor._tools_menu.removeAction(self._showAction)

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

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

	def _show_help(self):
		""" shows the help dialog """
		if self._help_dialog is not None:
			self._help_dialog.show()
			return
		
		self._help_dialog = pychan.loadXML("gui/help.xml")
		self._help_dialog.title = u"Help (Object Editor)"
		self._help_dialog.mapEvents({
			"closeButton" : self._help_dialog.hide,
		})
		
		# gimme some more space
		_SIZE = (320,400)
		scrollarea = self._help_dialog.findChildren(__class__=pychan.widgets.ScrollArea)[0]
		scrollarea.size = _SIZE
		scrollarea.min_size = _SIZE
		scrollarea.max_size = _SIZE
		
		f = open('lang/help_object_edit.txt', 'r')
		self._help_dialog.findChild(name="helpText").text = unicode(f.read())
		f.close()
		
		self._help_dialog.show()

	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({
			'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,
			
			'show_help'	: self._show_help,
		})
		
		self.container.findChild(name="x_offset_up").capture(self.change_offset, "mousePressed")
		self.container.findChild(name="x_offset_dn").capture(self.change_offset, "mousePressed")
		self._gui_x_offset = self.container.findChild(name="x_offset")
		self._gui_x_offset.capture(self.change_offset, "mouseWheelMovedUp")
		self._gui_x_offset.capture(self.change_offset, "mouseWheelMovedDown")

		self.container.findChild(name="y_offset_up").capture(self.change_offset, "mousePressed")
		self.container.findChild(name="y_offset_dn").capture(self.change_offset, "mousePressed")
		self._gui_y_offset = self.container.findChild(name="y_offset")
		self._gui_y_offset.capture(self.change_offset, "mouseWheelMovedUp")
		self._gui_y_offset.capture(self.change_offset, "mouseWheelMovedDown")

		self.container.findChild(name="object_blocking_toggle").capture(self.object_blocking_toggle, "mousePressed")
		self.container.findChild(name="instance_blocking_toggle").capture(self.instance_blocking_toggle, "mousePressed")

		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
		else:
			self._anim_data['current'] = self._anim_data['frames']

		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_data['current'] < self._anim_data['frames']:
			self._anim_data['current'] += 1
		else:
			self._anim_data['current'] = 0
			
		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 update_gui(self):
		""" updates the gui widgets with current instance data """
		if self._instances is None: return
		
		# 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 )

		if self._instances[0].isOverrideBlocking():
			self.container.findChild(name="override_blocking_toggle")._setMarked(True)
		else:
			self.container.findChild(name="override_blocking_toggle")._setMarked(False)

		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 ),
			'instance_blocking'	: unicode( self._instance_blocking ),
			'object_blocking'	: unicode( self._object_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(self, event, widget):
		""" widget callback: change the offset of an object 
		
		@type	event:	object
		@param	event:	FIFE mouseevent or keyevent
		@type	widget:	object
		@param	widget:	pychan widget
		"""
		if self._animation:
			self._editor.getStatusBar().setText(u"Offset changes of animations are not supported yet")
			return
		
		etype = event.getType()
		
		x = self._image.getXShift()
		y = self._image.getYShift()
		
		if etype == fife.MouseEvent.WHEEL_MOVED_UP or widget.name.endswith("up"):
			modifier = 1
		elif etype == fife.MouseEvent.WHEEL_MOVED_DOWN or widget.name.endswith("dn"):
			modifier = -1

		if widget.name.startswith("x"):
			x += modifier
		elif widget.name.startswith("y"):
			y += modifier

		self.set_offset(x, y)
		self.update_gui()

	def object_blocking_toggle(self, event, widget):
		""" widget callback: change the blocking of an instance 

		@type	event:	object
		@param	event:	FIFE mouseevent or keyevent
		@type	widget:	object
		@param	widget:	pychan widget
		"""
		self.check_override_blocking()
		object = self._instances[0].getObject()
		object_id = object.getId()
		blocking = not object.isBlocking()
		object.setBlocking(blocking)

		instances = self._layer.getInstances()
		for instance in instances:
			object = instance.getObject()
			if object.getId() == object_id:
				instance.setBlocking(blocking)

		self._object_blocking = int(blocking)
		self._instance_blocking = int(self._instances[0].isBlocking())

		self.update_gui()

	def instance_blocking_toggle(self, event, widget):
		""" widget callback: change the blocking of an instance 

		@type	event:	object
		@param	event:	FIFE mouseevent or keyevent
		@type	widget:	object
		@param	widget:	pychan widget
		"""
		self.check_override_blocking()
		instance = self._instances[0]
		instance.setBlocking(not instance.isBlocking())
		self._instance_blocking = int(instance.isBlocking())

		self.update_gui()

	def check_override_blocking(self):
		instance = self._instances[0]
		marked = self.container.findChild(name="override_blocking_toggle")._isMarked()
		if marked:
			instance.setOverrideBlocking(True)
		else:
			instance.setOverrideBlocking(False)

	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...
		"""
		instance_id = str(self._gui_instance_id_textfield._getText())
		msg = ''
		
		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)
				msg = unicode("Set new instance id: " + str(instance_id))
				self._editor.getStatusBar().setText(msg)
			else:
				self._editor.getStatusBar().setText(u"Instance ID is already in use.")

		if self._animation:
			msg = msg + "\n" + u"Editing offset and rotation of animated instances is not supported yet"
			self._editor.getStatusBar().setText(msg)
			return

		xoffset = self._gui_xoffset_textfield._getText()
		yoffset = self._gui_yoffset_textfield._getText()

		# 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"] == str(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

		block = self.tree.getroot()
		block.attrib["blocking"] = str(int(self._object_blocking))

		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
		
		"""
		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._object_blocking = 1

		if instance.isBlocking():
			self._instance_blocking = 1

		if object.isStatic():
			self._static = 1
		
		try:
			visual = object.get2dGfxVisual()
		except:
			self._editor.getStatusBar().setText(u"Fetching visual of object failed")
			raise			

		self._fixed_rotation = instance.getRotation()

		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:
				self._avail_rotations.append(angle)
	
			self._image_default_x_offset = self._image.getXShift()
			self._image_default_y_offset = self._image.getYShift()
		else:
			self._avail_rotations = object.getDefaultAction().get2dGfxVisual().getActionImageAngles()

	def show(self):
		""" show the plugin gui - and update it """
		self.update_gui()
		self.container.show()	
		self.container.adaptLayout(False)						
	
	def hide(self):
		""" hide the plugin gui - and reset it """
		self.container.hide()
		self._reset()		

	def input(self, instances):
		""" if called _and_ the user wishes to edit offsets,
			gets instance data and show gui

			(we only use the top instance of the selected cell)

		@type	instances:	list
		@param	instances:	a list of instances in the selected cell 
		"""
		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.show()
				else:
					self.hide()