comparison 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
comparison
equal deleted inserted replaced
377:fe6fb0e0ed23 378:64738befdf3b
1 # coding: utf-8
2 # ###################################################
3 # Copyright (C) 2008 The Zero-Projekt team
4 # http://zero-projekt.net
5 # info@zero-projekt.net
6 # This file is part of Zero "Was vom Morgen blieb"
7 #
8 # The Zero-Projekt codebase is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the
20 # Free Software Foundation, Inc.,
21 # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
22 # ###################################################
23
24 """ a tool for FIFEdit to edit object and instance attributes """
25
26 from fife import fife
27 from fife.extensions import pychan
28 import fife.extensions.pychan.widgets as widgets
29 from fife.extensions.pychan.tools import callbackWithArguments as cbwa
30
31 from fife.extensions.fife_timer import Timer
32
33 import scripts
34 import scripts.plugin as plugin
35 from scripts.events import *
36 from scripts.gui.action import Action
37
38
39 import os
40 try:
41 import xml.etree.cElementTree as ET
42 except:
43 import xml.etree.ElementTree as ET
44
45 import math
46
47 WHITE = {
48 "r" : 205,
49 "g" : 205,
50 "b" : 205
51 }
52 OUTLINE_SIZE = 1
53
54 class ObjectEdit(plugin.Plugin):
55 """ The B{ObjectEdit} module is a plugin for FIFedit and allows to edit
56 attributes of an selected instance - like offset, instance id or rotation
57 (namespaces and object id editing is excluded)
58
59 current features:
60 - click instance and get all known data
61 - edit offsets, rotation, instance id
62 - save offsets to object file
63 - outline highlighting of the selected object
64 - animation viewer
65
66 FIXME:
67 - add static and blocking flag to save routine
68 - fix animation rotation (FIFE has no method yet to export all
69 angles of an animation to python)
70 """
71 def __init__(self):
72 self.active = False
73 self._camera = None
74 self._layer = None
75 self._anim_timer = None
76
77 self._enabled = False
78
79 self.imagepool = None
80 self._animationpool = None
81
82 self.guidata = {}
83 self.objectdata = {}
84
85 def _reset(self):
86 """
87 resets all dynamic vars, but leaves out static ones (e.g. camera, layer)
88
89 """
90 if self._anim_timer:
91 self._anim_timer.stop()
92 # reset the ToggleButton
93 if self._gui_anim_playback._isToggled():
94 self._gui_anim_playback._setToggled(0)
95 self._anim_timer = None
96
97 self._object = None
98 self._instances = None
99 self._image = None
100 self._image_default_x_offset = None
101 self._image_default_y_offset = None
102 self._animation = False
103 self._anim_data = {}
104 self._rotation = None
105 self._avail_rotations = []
106 self._namespace = None
107 self._blocking = 0
108 self._static = 0
109 self._object_id = None
110 self._instance_id = None
111 self._fixed_rotation = None
112
113 if self._camera is not None:
114 self.renderer.removeAllOutlines()
115
116 def enable(self):
117 """ plugin method """
118 if self._enabled is True:
119 return
120
121 self._editor = scripts.editor.getEditor()
122 self.engine = self._editor.getEngine()
123
124 self.imagepool = self.engine.getImagePool()
125 self._animationpool = self.engine.getAnimationPool()
126
127 self._showAction = Action(unicode(self.getName(),"utf-8"), checkable=True)
128 scripts.gui.action.activated.connect(self.toggle_gui, sender=self._showAction)
129
130 self._editor._tools_menu.addAction(self._showAction)
131
132 events.onInstancesSelected.connect(self.input)
133
134 self._reset()
135 self.create_gui()
136
137 def disable(self):
138 """ plugin method """
139 if self._enabled is False:
140 return
141
142 self._reset()
143 self.container.hide()
144 self.removeAllChildren()
145
146 events.onInstancesSelected.disconnect(self.input)
147
148 self._editor._tools_menu.removeAction(self._showAction)
149
150 def isEnabled(self):
151 """ plugin method """
152 return self._enabled;
153
154 def getName(self):
155 """ plugin method """
156 return "Object editor v2"
157
158 def create_gui(self):
159 """
160 - creates the gui skeleton by loading the xml file
161 - finds some important childs and saves their widget in the object
162
163 FIXME:
164 - move all dynamic widgets to dict
165 """
166 self.container = pychan.loadXML('gui/objectedit.xml')
167 self.container.mapEvents({
168 'x_offset_up' : cbwa(self.change_offset_x, 1),
169 'x_offset_dn' : cbwa(self.change_offset_x, -1),
170
171 'y_offset_up' : cbwa(self.change_offset_y, 1),
172 'y_offset_dn' : cbwa(self.change_offset_y, -1),
173
174 'use_data' : self.use_user_data,
175 'change_data' : self.save_user_data,
176
177 'anim_left' : self.previous_anim_frame,
178 'anim_right' : self.next_anim_frame,
179 'anim_start_pos' : self.anim_start_frame,
180 'anim_end_pos' : self.anim_end_frame,
181 })
182
183 self._gui_anim_panel_wrapper = self.container.findChild(name="animation_panel_wrapper")
184 self._gui_anim_panel = self._gui_anim_panel_wrapper.findChild(name="animation_panel")
185
186 self._gui_rotation_dropdown = self.container.findChild(name="select_rotations")
187 self._gui_rotation_dropdown.capture(self.gui_rotate_instance,"mouseWheelMovedUp")
188 self._gui_rotation_dropdown.capture(self.gui_rotate_instance,"mouseWheelMovedDown")
189 self._gui_rotation_dropdown.capture(self.gui_rotate_instance,"action")
190
191 self._gui_anim_actions_dropdown = self._gui_anim_panel_wrapper.findChild(name="select_actions")
192 self._gui_anim_actions_dropdown.capture(self.eval_gui_anim_action,"mouseWheelMovedUp")
193 self._gui_anim_actions_dropdown.capture(self.eval_gui_anim_action,"mouseWheelMovedDown")
194 self._gui_anim_actions_dropdown.capture(self.eval_gui_anim_action,"action")
195
196 self._gui_anim_playback = self._gui_anim_panel_wrapper.findChild(name="anim_playback")
197 self._gui_anim_playback.capture(self.anim_playback, "mousePressed")
198 self._gui_anim_loop = self._gui_anim_panel_wrapper.findChild(name="anim_loop")
199
200 self._gui_current_frame = self._gui_anim_panel_wrapper.findChild(name="anim_current_frame")
201 self._gui_current_frame.capture(self.previous_anim_frame,"mouseWheelMovedUp")
202 self._gui_current_frame.capture(self.next_anim_frame,"mouseWheelMovedDown")
203
204 self._gui_xoffset_textfield = self.container.findChild(name="x_offset")
205 self._gui_yoffset_textfield = self.container.findChild(name="y_offset")
206
207 self._gui_instance_id_textfield = self.container.findChild(name="instance_id")
208
209 def anim_playback(self, widget):
210 """ start / stop playback of an animation due to status of a gui ToggleButton
211 Sets also two ivars of timer object (active & loop)
212 """
213 if widget._isToggled():
214 self._anim_timer.stop()
215 self._anim_timer.active = False
216 else:
217 frame_delay = self._anim_data['obj'].getFrameDuration(self._anim_data['current'])
218 self._anim_timer = Timer(delay=frame_delay,callback=self.next_anim_frame)
219 self._anim_timer.active = True
220 self._anim_timer.loop = self._gui_anim_loop._isMarked()
221 self._anim_timer.start()
222
223 def previous_anim_frame(self):
224 """ show previous anim frame """
225 if self._anim_data['current'] > 0:
226 self._anim_data['current'] -= 1
227 self.update_gui()
228
229 def next_anim_frame(self):
230 """ show next anim frame and reset animation frame to 0 if playback looping is active"""
231 if self._anim_timer.loop and (self._anim_data['current'] == self._anim_data['frames']):
232 self._anim_data['current'] = 0
233
234 if self._anim_data['current'] < self._anim_data['frames']:
235 self._anim_data['current'] += 1
236 self.update_gui()
237
238 def anim_start_frame(self):
239 """ set start frame of animation """
240 self._anim_data['current'] = 0
241 self.update_gui()
242
243 def anim_end_frame(self):
244 """ set end frame of animation """
245 self._anim_data['current'] = self._anim_data['frames']
246 self.update_gui()
247
248 def set_default_offset(self, axis):
249 """ set default image offset for given axis """
250 if axis == 'x':
251 self.set_offset(x=self._image_default_x_offset)
252 elif axis == 'y':
253 self.set_offset(y=self._image_default_y_offset)
254
255 def update_gui(self):
256 """
257 updates the gui widgets with current instance data
258
259 """
260 # show the image we retrieved from an animated object
261 if self._animation:
262 if not self._gui_anim_panel_wrapper.findChild(name="animation_panel"):
263 self._gui_anim_panel_wrapper.addChild(self._gui_anim_panel)
264
265 # get current selected image and update the icon widget
266 dur = 0
267 for i in range(self._anim_data['frames']):
268 dur += self._anim_data['obj'].getFrameDuration(i)
269
270 # set new duration for the playback timer
271 if self._anim_timer:
272 frame_delay = self._anim_data['obj'].getFrameDuration(self._anim_data['current'])
273
274 if i == self._anim_data['current']:
275 # set new duration for the playback timer
276 if self._anim_timer and self._anim_timer.active:
277 self._anim_timer.setPeriod(self._anim_data['obj'].getFrameDuration(self._anim_data['current']))
278 break
279
280 image = self._anim_data['obj'].getFrameByTimestamp(dur)
281 self.container.findChild(name="animTest").image = image.getResourceFile()
282 self.container.findChild(name="animTest").size= (250,250)
283 self.container.findChild(name="animTest").min_size= (250,250)
284
285 self.container.distributeInitialData({
286 'anim_current_frame' : unicode(str(self._anim_data['current'])),
287 'anim_rotation' : unicode(str(self._anim_data['obj'].getDirection())),
288 })
289
290 else:
291 if self._gui_anim_panel_wrapper.findChild(name="animation_panel"):
292 self._gui_anim_panel_wrapper.removeChild(self._gui_anim_panel)
293
294 if self._image is not None:
295 x_offset = unicode( self._image.getXShift() )
296 y_offset = unicode( self._image.getYShift() )
297 else:
298 x_offset = unicode( 0 )
299 y_offset = unicode( 0 )
300
301 self.container.distributeInitialData({
302 'select_rotations' : self._avail_rotations,
303 'instance_id' : unicode( self._instances[0].getId() ),
304 'object_id' : unicode( self._object_id ),
305 'x_offset' : x_offset,
306 'y_offset' : y_offset,
307 'instance_rotation' : unicode( self._instances[0].getRotation() ),
308 'object_namespace' : unicode( self._namespace ),
309 'object_blocking' : unicode( self._blocking ),
310 'object_static' : unicode( self._static ),
311 })
312
313 if not self._animation:
314 if self._fixed_rotation in self._avail_rotations:
315 index = self._avail_rotations.index( self._fixed_rotation )
316 self._gui_rotation_dropdown._setSelected(index)
317 else:
318 print "Internal FIFE rotation: ", self._instances[0].getRotation()
319 print "Fixed rotation (cam rot) ", self._fixed_rotation + int(abs(self._camera.getRotation()))
320 print "Collected rots from object ", self._avail_rotations
321
322
323 self.container.adaptLayout(False)
324
325 def toggle_gui(self):
326 """
327 show / hide the gui
328 """
329 if self.active is True:
330 self.active = False
331 if self.container.isVisible() or self.container.isDocked():
332 self.container.setDocked(False)
333 self.container.hide()
334 self._showAction.setChecked(False)
335 else:
336 self.active = True
337 self._showAction.setChecked(True)
338
339 def highlight_selected_instance(self):
340 """ highlights selected instance """
341 self.renderer.removeAllOutlines()
342 self.renderer.addOutlined(self._instances[0], WHITE["r"], WHITE["g"], WHITE["b"], OUTLINE_SIZE)
343
344 def change_offset_x(self, value=1):
345 """
346 - callback for changing x offset
347 - changes x offset of current instance (image)
348 - updates gui
349
350 @type value: int
351 @param value: the modifier for the x offset
352 """
353 if self._animation:
354 print "Offset changes of animations are not supported yet"
355 return
356
357 if self._image is not None:
358 self._image.setXShift(self._image.getXShift() + value)
359 self.update_gui()
360
361 def change_offset_y(self, value=1):
362 """
363 - callback for changing y offset
364 - changes y offset of current instance (image)
365 - updates gui
366
367 @type value: int
368 @param value: the modifier for the y offset
369 """
370 if self._animation:
371 print "Offset changes of animations are not supported yet"
372 return
373
374 if self._image is not None:
375 self._image.setYShift(self._image.getYShift() + value)
376 self.update_gui()
377
378 def use_user_data(self):
379 """
380 - takes the users values and applies them directly to the current ._instance
381 - writes current data record
382 - writes previous data record
383 - updates gui
384
385 FIXME:
386 - parse user data in case user think strings are considered to be integer offset values...
387 """
388 if self._animation:
389 print "Editing animated instances is not supported yet"
390 return
391
392 xoffset = self._gui_xoffset_textfield._getText()
393 yoffset = self._gui_yoffset_textfield._getText()
394
395 instance_id = str(self._gui_instance_id_textfield._getText())
396
397 if instance_id == "":
398 instance_id = "None"
399
400 if instance_id is not None and instance_id is not "None":
401 existing_instances = self._editor.getActiveMapView().getController()._layer.getInstances(instance_id)
402 if len(existing_instances) <= 0:
403 self._instances[0].setId(instance_id)
404 print "Set new instance id: ", instance_id
405 else:
406 print "Instance ID is already in use."
407
408 # update rotation
409 angle = self.eval_gui_rotation()
410 self.set_rotation(angle)
411
412 # update offsets
413 self.set_offset(int(xoffset), int(yoffset))
414
415 self.update_gui()
416
417 def save_user_data(self):
418 """ saves the current object to its xml file
419
420 NOTE:
421 - animations can't be saved for now
422
423 FIXME:
424 - add missing object attributes to saving routine
425 """
426 if self._object is None:
427 return
428 if self._animation:
429 return
430
431 file = self._object.getResourceFile()
432 self.tree = ET.parse(file)
433
434 img_lst = self.tree.findall("image")
435
436 # apply changes to the XML structure due to current user settings in the gui
437 for img_tag in img_lst:
438 if img_tag.attrib["direction"] == self._avail_rotations[self._gui_rotation_dropdown._getSelected()]:
439 img_tag.attrib["x_offset"] = self._gui_xoffset_textfield._getText()
440 img_tag.attrib["y_offset"] = self._gui_yoffset_textfield._getText()
441 break
442
443 xmlcontent = ET.tostring(self.tree.getroot())
444
445 # save xml data beneath the <?fife type="object"?> definition into the object file
446 tmp = open(file, 'w')
447 tmp.write('<?fife type="object"?>\n')
448 tmp.write(xmlcontent + "\n")
449 tmp.close()
450
451 def gui_rotate_instance(self):
452 """ rotate an instance due to selected angle """
453 angle = self.eval_gui_rotation()
454 self.set_rotation(angle)
455
456 def eval_gui_rotation(self):
457 """ prepare rotation from gui and apply it to the current selected instance """
458 index = self._gui_rotation_dropdown._getSelected()
459 angle = int( self._avail_rotations[index] )
460
461 if angle == 360:
462 angle = 0
463
464 return angle
465
466 def eval_gui_anim_action(self):
467 """ check the selected action of an animation and update the gui accordingly """
468 if not self._anim_data['actions']: return
469
470 index = self._gui_anim_actions_dropdown._getSelected()
471 action = self._anim_data['actions'][index]
472
473 self.update_anim_data(action)
474 self.update_gui()
475
476 def set_rotation(self, angle):
477 """ set the rotation of the current instance """
478 # print "...setting instance rotation from %s to %s" % (self._rotation, angle)
479 self._instances[0].setRotation(angle)
480 self.get_instance_data(None, None, angle)
481 self.update_gui()
482 # print "...new internal FIFE rotation ", int(self._instances[0].getRotation())
483
484 def set_offset(self, x=None, y=None):
485 """ set x/y offset of current selected instance """
486 if x is not None:
487 self._image.setXShift(x)
488 if y is not None:
489 self._image.setYShift(y)
490
491 def update_anim_data(self, action=None):
492 """ update animation data for the current selected instance from FIFE's data structure
493
494 @type animation FIFE animation
495 @return animation current selected animation
496 """
497 if action:
498 animation_id = action.get2dGfxVisual().getAnimationIndexByAngle(self._fixed_rotation)
499 animation = self._animationpool.getAnimation(animation_id)
500
501 action_ids = []
502 actions = []
503
504 try:
505 action_ids = self._object.getActionIds()
506 for id in action_ids:
507 actions.append(self._object.getAction(id))
508 except:
509 pass
510
511 self._anim_data = {}
512 self._anim_data['obj'] = animation
513 self._anim_data['id'] = animation_id
514 self._anim_data['frames'] = animation.getNumFrames()
515 self._anim_data['current'] = 0
516 self._anim_data['actions'] = actions
517 self._anim_data['action_ids'] = action_ids
518 self._anim_data['default_action'] = self._object.getDefaultAction()
519 self._anim_data['action'] = action
520
521 return animation
522
523 def get_instance_data(self, timestamp=None, frame=None, angle=-1, instance=None):
524 """
525 - grabs all available data from both object and instance
526
527 FIXME:
528 1.) we need to fix the instance rotation / rotation issue
529 """
530 visual = None
531 self._avail_rotations = []
532
533 if instance is None:
534 instance = self._instances[0]
535
536 object = instance.getObject()
537 self._object = object
538 self._namespace = object.getNamespace()
539 self._object_id = object.getId()
540
541 self._instance_id = instance.getId()
542
543 if self._instance_id == '':
544 self._instance_id = 'None'
545
546 if angle == -1:
547 angle = int(instance.getRotation())
548 else:
549 angle = int(angle)
550
551 self._rotation = angle
552
553 if object.isBlocking():
554 self._blocking = 1
555
556 if object.isStatic():
557 self._static = 1
558
559 try:
560 visual = object.get2dGfxVisual()
561 except:
562 print 'Fetching visual of object - failed. :/'
563 raise
564
565 # print "Camera tilt: ", self._camera.getTilt()
566 # print "Camera rotation: ", self._camera.getRotation()
567 # print "Instance rotation: ", instance.getRotation()
568
569 # self._fixed_rotation = int(instance.getRotation() + abs( self._camera.getRotation() ) )
570 self._fixed_rotation = instance.getRotation()
571 # self._fixed_rotation = visual.getClosestMatchingAngle(self._fixed_rotation)
572
573 index = visual.getStaticImageIndexByAngle(self._fixed_rotation)
574
575 if index is -1:
576 # object is an animation
577 self._animation = True
578 self._image = None
579
580 # no static image available, try default action
581 action = object.getDefaultAction()
582
583 if action:
584 animation = self.update_anim_data(action)
585
586 # update gui
587 if animation:
588 self._gui_anim_actions_dropdown._setItems(self._anim_data['action_ids'])
589 self._gui_anim_actions_dropdown._setSelected(0)
590
591 if timestamp is None and frame is not None:
592 self._image = animation.getFrame(frame)
593 elif timestamp is not None and frame is None:
594 self._image = animation.getFrameByTimestamp(timestamp)
595 else:
596 self._image = animation.getFrameByTimestamp(0)
597 elif index is not -1:
598 # object is a static image
599 self._animation = False
600 self._image = self.imagepool.getImage(index)
601
602 if not self._animation:
603 rotations = visual.getStaticImageAngles()
604 for angle in rotations:
605 # angle += int(abs( self._camera.getRotation() ))
606 self._avail_rotations.append(angle)
607
608 self._image_default_x_offset = self._image.getXShift()
609 self._image_default_y_offset = self._image.getYShift()
610 else:
611 # these doesn't work correctly
612 # rotations = [0,60,120,180,240,300]
613
614 # testbench to get valid angles
615 # angle = 0
616 # rotations = []
617 # while angle != 360:
618 # angle += 10
619 # rotations.append(angle)
620
621 # estimated angles (for hex!) to make things work - use testbench to test
622 # various angles and note down the working ones (watch instance
623 # rotation and the animation rotations shown in the gui; valid
624 # angles are given once the rotations are in sync
625 self._avail_rotations = [9,69,139,169,249,319]
626
627 def input(self, instances):
628 """
629 if called _and_ the user wishes to edit offsets,
630 gets instance data and show gui
631
632 """
633 if instances != self._instances:
634 if self.active is True:
635 self._reset()
636 self._instances = instances
637
638 if self._camera is None:
639 self._camera = self._editor.getActiveMapView().getCamera()
640 self.renderer = fife.InstanceRenderer.getInstance(self._camera)
641
642 self._layer = self._editor.getActiveMapView().getController()._layer
643
644 if self._instances != ():
645 self.highlight_selected_instance()
646 self.get_instance_data()
647 self.update_gui()
648 self.container.show()
649 else:
650 self._reset()
651 self.container.hide()
652
653 self.container.adaptLayout(False)