comparison clients/editor/plugins/objectedit.py @ 134:ade070598fd1

- added object editor plugin NOTES: - plugin is not ready for productive enviroments, yet - lacks saving functionality - some issues left, but it works better as previous versions ;-)
author chewie@33b003aa-7bff-0310-803a-e67f0ece8222
date Sat, 13 Sep 2008 23:28:52 +0000
parents
children 7dc59bd3d6b1
comparison
equal deleted inserted replaced
133:cb35643ad1a7 134:ade070598fd1
1 #!/usr/bin/env python
2 # coding: utf-8
3 # ###################################################
4 # Copyright (C) 2008 The Zero-Projekt team
5 # http://zero-projekt.net
6 # info@zero-projekt.net
7 # This file is part of Zero "Was vom Morgen blieb"
8 #
9 # The Zero-Projekt codebase is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program; if not, write to the
21 # Free Software Foundation, Inc.,
22 # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
23 # ###################################################
24
25 import fife
26 import plugin
27 import pychan
28 import pychan.widgets as widgets
29 from pychan.tools import callbackWithArguments as cbwa
30
31 import settings as Settings
32
33 class ObjectEdit(plugin.Plugin):
34 def __init__(self, engine, mapedit):
35 """
36 ObjectEdit plugin for FIFEdit
37
38 Mission: provide a gui mask to edit all important object data within the editor
39 (id, offsets, rotation, blocking, static)
40
41 namespaces and object ids are excluded
42
43 Current features:
44 - click instance and get all known data
45 - edit offsets, rotation, blocking, static
46 - outline highlighting of the selected object
47 - 3 data states: current, previous and default (so there is at least a one-step-undo)
48
49 Missing features:
50 - object saving
51 - id saving (handled by Fifedit via save map, but we have to set the id from here)
52 - a lot of bug fixing concerning the rotation and the data records ^^
53 - cleanup
54
55 NOTE:
56 - this tool isn't ready for a working enviroment (yet)
57 """
58 # Fifedit plugin data
59 self.menu_items = { 'ObjectEdit' : self.toggle_offsetedit }
60
61 self._mapedit = mapedit
62
63 # FIXME
64 # this is _very bad_ - but I need to change the current rotation code by providing
65 # project specific rotation angles. FIFE later should provide a list of the loaded
66 # object rotations (they are provided by the xml files, so we just need to use them...)
67 self._mapedit._objectedit_rotations = None
68 # end FIXME
69 self.active = False
70
71 self.imagepool = engine.getImagePool()
72 self.animationpool = engine.getAnimationPool()
73
74 self._camera = None
75 self._layer = None
76
77 self.guidata = {}
78 self.objectdata = {}
79
80 self._reset()
81 self.create_gui()
82
83
84 def _reset(self):
85 """
86 resets all dynamic vars, but leaves out static ones (e.g. camera, layer)
87
88 """
89 self._instances = None
90 self._image = None
91 self._animation = False
92 self._rotation = None
93 self._avail_rotations = []
94 self._namespace = None
95 self._blocking = 0
96 self._static = 0
97 self._object_id = None
98 self._instance_id = None
99 self._fixed_rotation = None
100
101 self.guidata['instance_id'] = 'None'
102 self.guidata['object_id'] = 'None'
103 self.guidata['x_offset'] = 0
104 self.guidata['y_offset'] = 0
105 self.guidata['instance_rotation'] = 0
106 self.guidata['namespace'] = 'None'
107 self.guidata['blocking'] = 0
108 self.guidata['static'] = 0
109
110 if self._camera is not None:
111 self.renderer.removeAllOutlines()
112
113 def create_gui(self):
114 """
115 - creates the gui skeleton by loading the xml file
116 - finds some important childs and saves their widget in the object
117 """
118 self.container = pychan.loadXML('gui/offsetedit.xml')
119 self.container.mapEvents({
120 'x_offset_up' : cbwa(self.change_offset_x, 1),
121 'x_offset_dn' : cbwa(self.change_offset_x, -1),
122
123 'y_offset_up' : cbwa(self.change_offset_y, 1),
124 'y_offset_dn' : cbwa(self.change_offset_y, -1),
125
126 'use_data' : cbwa(self.use_user_data),
127 'previous_data' : cbwa(self.load_previous_data),
128 'default_data' : cbwa(self.load_default_data)
129 })
130
131 self._gui_anim_panel_wrapper = self.container.findChild(name="animation_panel_wrapper")
132 self._gui_anim_panel = self._gui_anim_panel_wrapper.findChild(name="animation_panel")
133
134 self._gui_anim_panel_wrapper.removeChild(self._gui_anim_panel)
135
136 self._gui_rotation_dropdown = self.container.findChild(name="select_rotations")
137
138 self._gui_xoffset_textfield = self.container.findChild(name="x_offset")
139 self._gui_yoffset_textfield = self.container.findChild(name="y_offset")
140
141 def _get_gui_size(self):
142 """
143 gets the current size of the gui window and calculates new position
144 (atm top right corner)
145 """
146 size = self.container._getSize()
147 self.position = ((Settings.ScreenWidth - 10 - size[0]), 10)
148
149 def update_gui(self):
150 """
151 updates the gui widgets with current instance data
152
153 FIXME:
154 - drop animation support or turn it into something useful
155 """
156 #if self._animation is False:
157 #try:
158 #self._gui_anim_panel_wrapper.removeChild(self._gui_anim_panel)
159 #except:
160 #pass
161 #elif self._animation is True:
162 #try:
163 #self._gui_anim_panel_wrapper.resizeToContent()
164 #self._gui_anim_panel_wrapper.addChild(self._gui_anim_panel)
165 #self._gui_anim_panel_wrapper.resizeToContent()
166 #except:
167 #pass
168
169 self.container.distributeInitialData({
170 'select_rotations' : self._avail_rotations,
171 'instance_id' : self.guidata['instance_id'],
172 'object_id' : self.guidata['object_id'],
173 'x_offset' : self.guidata['x_offset'],
174 'y_offset' : self.guidata['y_offset'],
175 'instance_rotation' : self.guidata['instance_rotation'],
176 'object_namespace' : self.guidata['namespace'],
177 'object_blocking' : self.guidata['blocking'],
178 'object_static' : self.guidata['static'],
179 })
180 try:
181 print self._avail_rotations
182 print self._fixed_rotation
183 index = self._avail_rotations.index( str(self._fixed_rotation) )
184 self._gui_rotation_dropdown._setSelected(index)
185 except:
186 # pass
187 print "Angle (", self._fixed_rotation, ") not supported by this instance"
188
189 def toggle_gui(self):
190 """
191 show / hide the gui
192
193 FIXME:
194 - ATM not in use, needs some additional code when showing / hiding the gui (see input() )
195 """
196 if self.container.isVisible():
197 self.container.hide()
198 else:
199 self.container.show()
200
201 def toggle_offsetedit(self):
202 """
203 - toggles the object editor activ / inactiv - just in case the user don't want to have
204 the gui popping up all the time while mapping :-)
205 - hides gui
206 """
207 if self.active is True:
208 self.active = False
209 if self.container.isVisible():
210 self.container.hide()
211 else:
212 self.active = True
213
214 def highlight_selected_instance(self):
215 """
216 just highlights selected instance
217 """
218 self.renderer.removeAllOutlines()
219 self.renderer.addOutlined(self._instances[0], 205, 205, 205, 1)
220
221 def change_offset_x(self, value=1):
222 """
223 - callback for changing x offset
224 - changes x offset of current instance (image)
225 - updates gui
226
227 @param int value the modifier for the x offset
228 """
229 if self._image is not None:
230 self._image.setXShift(self._image.getXShift() + value)
231
232 self.guidata['x_offset'] = str( self._image.getXShift() )
233 self.update_gui()
234
235 def change_offset_y(self, value=1):
236 """
237 - callback for changing y offset
238 - changes y offset of current instance (image)
239 - updates gui
240
241 @param int value the modifier for the y offset
242 """
243 if self._image is not None:
244 self._image.setYShift(self._image.getYShift() + value)
245
246 self.guidata['y_offset'] = str( self._image.getYShift() )
247 self.update_gui()
248
249 def use_user_data(self):
250 """
251 - takes the users values and applies them directly to the current ._instance
252 - writes current data record
253 - writes previous data record
254 - updates gui
255
256 FIXME:
257 - parse user data in case user think strings are considered to be integer offset values...
258 """
259 xoffset = self._gui_xoffset_textfield._getText()
260 yoffset = self._gui_yoffset_textfield._getText()
261
262 # workaround - dropdown list only has 2 entries, but sends 3 -> pychan bug?
263 if len(self._avail_rotations) < self._gui_rotation_dropdown._getSelected():
264 index = len(self._avail_rotations)
265 else:
266 index = self._gui_rotation_dropdown._getSelected()
267
268 # strange, but this helps to rotate the image correctly to the value the user selected
269 angle = int( self._avail_rotations[index] )
270 angle = int(angle - abs( self._camera.getTilt() ) )
271 if angle == 360:
272 angle = 0
273
274 self._instances[0].setRotation(angle)
275 self.get_instance_data(None, None, angle)
276
277 try:
278 self._image.setXShift( int(xoffset) )
279 except:
280 pass
281 # print "x offset must me of type int!"
282 try:
283 self._image.setYShift( int(yoffset) )
284 except:
285 pass
286 # print "y offset must be of type int!"
287
288 self.write_current_data()
289 self.objectdata[self._namespace][self._object_id]['previous'] = self.objectdata[self._namespace][self._object_id]['current'].copy()
290 self.update_gui()
291
292 def load_previous_data(self):
293 """
294 - writes a copy of the previous record back to the current record (aka one-step-undo)
295 - loads current data into class object
296 - updates gui
297 """
298 self.objectdata[self._namespace][self._object_id]['current'] = self.objectdata[self._namespace][self._object_id]['previous'].copy()
299 self.load_current_data()
300 self.update_gui()
301
302 def load_default_data(self):
303 """
304 - writes a copy of the default record back to the current record
305 - loads current data into class object
306 - updates gui
307 """
308 self.objectdata[self._namespace][self._object_id]['current'] = self.objectdata[self._namespace][self._object_id]['default'].copy()
309 self.load_current_data()
310 self.update_gui()
311
312 def load_current_data(self):
313 """
314 loads the current record into class object
315 """
316 self._image = self.objectdata[self._namespace][self._object_id]['current']['image']
317 self._animation = self.objectdata[self._namespace][self._object_id]['current']['animation']
318 self._rotation = self.objectdata[self._namespace][self._object_id]['current']['rotation']
319 self._fixed_rotation = self.objectdata[self._namespace][self._object_id]['current']['fixed_rotation']
320 self._avail_rotations = self.objectdata[self._namespace][self._object_id]['current']['avail_rotations']
321 self._blocking = self.objectdata[self._namespace][self._object_id]['current']['blocking']
322 self._static = self.objectdata[self._namespace][self._object_id]['current']['static']
323 self._instance_id = self.objectdata[self._namespace][self._object_id]['current']['instance_id']
324 self._image.setXShift( self.objectdata[self._namespace][self._object_id]['current']['xoffset'] )
325 self._image.setYShift( self.objectdata[self._namespace][self._object_id]['current']['yoffset'] )
326
327 self.write_current_guidata()
328
329 def write_current_data(self):
330 """
331 updates the current record
332 """
333 self.objectdata[self._namespace][self._object_id]['current']['instance'] = self._instances[0]
334 self.objectdata[self._namespace][self._object_id]['current']['image'] = self._image
335 self.objectdata[self._namespace][self._object_id]['current']['animation'] = self._animation
336 self.objectdata[self._namespace][self._object_id]['current']['rotation'] = self._rotation
337 self.objectdata[self._namespace][self._object_id]['current']['fixed_rotation'] = self._fixed_rotation
338 self.objectdata[self._namespace][self._object_id]['current']['avail_rotations'] = self._avail_rotations
339 self.objectdata[self._namespace][self._object_id]['current']['blocking'] = self._blocking
340 self.objectdata[self._namespace][self._object_id]['current']['static'] = self._static
341 self.objectdata[self._namespace][self._object_id]['current']['instance_id'] = self._instance_id
342 self.objectdata[self._namespace][self._object_id]['current']['xoffset'] = self._image.getXShift()
343 self.objectdata[self._namespace][self._object_id]['current']['yoffset'] = self._image.getYShift()
344
345 self.write_current_guidata()
346
347 def write_current_guidata(self):
348 """
349 updates the gui data with
350 """
351 self.guidata['instance_rotation'] = str( self._instances[0].getRotation() )
352 self.guidata['object_id'] = str( self._object_id )
353 self.guidata['instance_id'] = str( self._instance_id )
354 self.guidata['x_offset'] = str( self._image.getXShift() )
355 self.guidata['y_offset'] = str( self._image.getYShift() )
356 self.guidata['namespace'] = self._namespace
357 self.guidata['blocking'] = str( self._blocking )
358 self.guidata['static'] = str( self._static )
359
360 def get_instance_data(self, timestamp=None, frame=None, angle=-1, instance=None):
361 """
362 - grabs all available data from both object and instance
363 - checks if we already hold a record (namespace + object id)
364
365 FIXME:
366 1.) we need to fix the instance rotation / rotation issue
367 2.) use correct instance rotations to store data for _each_ available rotation
368 3.) move record code out of this method
369 """
370 visual = None
371 self._avail_rotations = []
372
373 if instance is None:
374 instance = self._instances[0]
375
376 object = instance.getObject()
377 self._namespace = object.getNamespace()
378 self._object_id = object.getId()
379
380 if angle != -1:
381 del self.objectdata[self._namespace][self._object_id]
382
383 if not self.objectdata.has_key(self._namespace):
384 self.objectdata[self._namespace] = {}
385
386 if not self.objectdata[self._namespace].has_key(self._object_id):
387 self.objectdata[self._namespace][self._object_id] = {}
388
389 # we hold 3 versions of the data: current, previous, default
390 # default is only set one time, current and previous are changing data
391 # due to the users actions
392 self.objectdata[self._namespace][self._object_id]['current'] = {}
393 self.objectdata[self._namespace][self._object_id]['previous'] = {}
394
395 self._instance_id = instance.getId()
396
397 if self._instance_id == '':
398 self._instance_id = 'None'
399
400 if angle == -1:
401 angle = int(instance.getRotation())
402 else:
403 angle = int(angle)
404
405 self._rotation = angle
406
407 if object.isBlocking():
408 self._blocking = 1
409
410 if object.isStatic():
411 self._static = 1
412
413 try:
414 visual = object.get2dGfxVisual()
415 except:
416 print 'Fetching visual of object - failed. :/'
417 raise
418
419 self._fixed_rotation = int(instance.getRotation() + abs( self._camera.getTilt() ) )
420 self._fixed_rotation = visual.getClosestMatchingAngle(self._fixed_rotation)
421
422 index = visual.getStaticImageIndexByAngle(self._fixed_rotation)
423
424 if index == -1:
425 # object is an animation
426 self._animation = True
427 # no static image available, try default action
428 action = object.getDefaultAction()
429 if action:
430 animation_id = action.get2dGfxVisual().getAnimationIndexByAngle(self._fixed_rotation)
431 animation = self.animationpool.getAnimation(animation_id)
432 if timestamp is None and frame is not None:
433 self._image = animation.getFrame(frame)
434 elif timestamp is not None and frame is None:
435 self._image = animation.getFrameByTimestamp(timestamp)
436 else:
437 self._image = animation.getFrameByTimestamp(0)
438 index = self._image.getPoolId()
439 elif index != -1:
440 # object is a static image
441 self._animation = False
442 self._image = self.imagepool.getImage(index)
443
444 if self._animation:
445 self._avail_rotations = Settings.RotAngles['animations']
446 else:
447 rotation_tuple = visual.getStaticImageAngles()
448 for angle in rotation_tuple:
449 self._avail_rotations.append( str(angle) )
450
451 # FIXME: see l. 40
452 self._mapedit._objectedit_rotations = self._avail_rotations
453 # end FIXME
454 self.write_current_data()
455
456 self.objectdata[self._namespace][self._object_id]['default'] = {}
457 self.objectdata[self._namespace][self._object_id]['default'] = self.objectdata[self._namespace][self._object_id]['current'].copy()
458 self.objectdata[self._namespace][self._object_id]['previous'] = self.objectdata[self._namespace][self._object_id]['current'].copy()
459
460 self.write_current_guidata()
461 else:
462 self.load_current_data()
463
464 def dump_objectdata(self):
465 """
466 just a useful dumper ^^
467 """
468 print "#"*4, "Dump of objectdata", "#"*4, "\n"
469 for namespace in self.objectdata:
470 print "namespace: ", namespace
471 for key in self.objectdata[namespace]:
472 print "\tkey: ", key
473 for item in self.objectdata[namespace][key]:
474 if len(item) >= 9:
475 tab = "\t"*1
476 else:
477 tab = "\t"*2
478 print "\t\t", item, " : ", tab, self.objectdata[namespace][key][item]
479
480 def input(self):
481 """
482 if called _and_ the user wishes to edit offsets,
483 gets instance data and show gui
484
485 (see run.py, pump() )
486 """
487 if self._mapedit._instances != self._instances:
488 if self.active is True:
489 self._instances = self._mapedit._instances
490
491 if self._camera is None:
492 self._camera = self._mapedit._camera
493 self.renderer = fife.InstanceRenderer.getInstance(self._camera)
494
495 self._layer = self._mapedit._layer
496
497 if self._instances != ():
498 self.highlight_selected_instance()
499 self.get_instance_data()
500
501 if self._animation is False:
502 self.update_gui()
503 self.container.adaptLayout()
504 self.container.show()
505 self._get_gui_size()
506 self.container._setPosition(self.position)
507 else:
508 self.container.hide()
509 print "Animation objects are not yet editable"
510 # self.dump_objectdata()
511 else:
512 self._reset()
513 self.container.hide()