ya2 · news · projects · code · about

new scene
[pmachines.git] / pmachines / editor / scene.py
1 from copy import deepcopy
2 from json import dumps
3 from importlib import import_module
4 from inspect import isclass
5 from glob import glob
6 from os.path import basename
7 from logging import info
8 import hashlib
9 from panda3d.core import Texture, TextNode
10 from direct.gui.OnscreenImage import OnscreenImage
11 from direct.gui.DirectGui import DirectButton, DirectFrame, DirectEntry, \
12 YesNoDialog, DirectOptionMenu
13 from direct.gui.DirectGuiGlobals import FLAT, NORMAL
14 from direct.gui.OnscreenText import OnscreenText
15 from direct.showbase.DirectObject import DirectObject
16 from pmachines.items.item import Item
17 from pmachines.editor.scene_list import SceneList
18 from pmachines.editor.inspector import Inspector
19
20
21 class SceneEditor(DirectObject):
22
23 def __init__(self, json, json_name, context, add_item, items):
24 super().__init__()
25 self.__items = items
26 self.__json = json
27 if not json_name:
28 self.__json = json = {
29 'name': '',
30 'instructions': '',
31 'version': '',
32 'items': [],
33 'start_items': [],
34 'test_items': {
35 'pixel_space': [],
36 'world_space': []}}
37 self.__inspector = None
38 self.__context = context
39 self.__add_item = add_item
40 self._font = base.loader.load_font(
41 'assets/fonts/Hanken-Book.ttf')
42 self._font.clear()
43 self._font.set_pixels_per_unit(60)
44 self._font.set_minfilter(Texture.FTLinearMipmapLinear)
45 self._font.set_outline((0, 0, 0, 1), .8, .2)
46 self._common = {
47 'scale': .046,
48 'text_font': self._font,
49 'text_fg': (.9, .9, .9, 1),
50 'relief': FLAT,
51 'frameColor': (.4, .4, .4, .14),
52 'rolloverSound': loader.load_sfx(
53 'assets/audio/sfx/rollover.ogg'),
54 'clickSound': loader.load_sfx(
55 'assets/audio/sfx/click.ogg')}
56 w, h, tw, l = 1.8, 1, 30, .36
57 self._frm = DirectFrame(frameColor=(.4, .4, .4, .06),
58 frameSize=(0, w, 0, h),
59 parent=base.a2dBottomCenter,
60 pos=(-w/2, 0, 0))
61 OnscreenText(
62 _('Filename'), pos=(l - .03, h - .1), parent=self._frm,
63 font=self._common['text_font'],
64 scale=self._common['scale'],
65 fg=self._common['text_fg'],
66 align=TextNode.A_right)
67 self.__filenamename_entry = DirectEntry(
68 scale=self._common['scale'],
69 pos=(l, 1, h - .1),
70 entryFont=self._font,
71 width=tw,
72 frameColor=self._common['frameColor'],
73 initialText=json_name,
74 parent=self._frm,
75 text_fg=self._common['text_fg'])
76 OnscreenText(
77 _('Name'), pos=(l - .03, h - .2), parent=self._frm,
78 font=self._common['text_font'],
79 scale=self._common['scale'],
80 fg=self._common['text_fg'],
81 align=TextNode.A_right)
82 self.__name_entry = DirectEntry(
83 scale=self._common['scale'],
84 pos=(l, 1, h - .2),
85 entryFont=self._font,
86 width=tw,
87 frameColor=self._common['frameColor'],
88 initialText=json['name'],
89 parent=self._frm,
90 text_fg=self._common['text_fg'])
91 OnscreenText(
92 _('Description'), pos=(l - .03, h - .3), parent=self._frm,
93 font=self._common['text_font'],
94 scale=self._common['scale'],
95 fg=self._common['text_fg'],
96 align=TextNode.A_right)
97 def add_line_break(txt, entry):
98 curpos = entry.node().getCursorPosition()
99 entry.set(txt[:curpos]+ "\n" + txt[curpos:])
100 entry.node().setCursorPosition(curpos+1)
101 entry['focus']=1
102 self.__instructions_entry = DirectEntry(
103 scale=self._common['scale'],
104 pos=(l, 1, h - .3),
105 entryFont=self._font,
106 width=tw,
107 numLines=12,
108 cursorKeys=True,
109 frameColor=self._common['frameColor'],
110 initialText=json['instructions'].replace('\\n', '\n'),
111 parent=self._frm,
112 text_fg=self._common['text_fg'])
113 self.__instructions_entry['command'] = add_line_break
114 self.__instructions_entry['extraArgs'] = [self.__instructions_entry]
115 def load_images_btn(path, col):
116 colors = {
117 'gray': [
118 (.6, .6, .6, 1), # ready
119 (1, 1, 1, 1), # press
120 (.8, .8, .8, 1), # rollover
121 (.4, .4, .4, .4)],
122 'green': [
123 (.1, .68, .1, 1),
124 (.1, 1, .1, 1),
125 (.1, .84, .1, 1),
126 (.4, .1, .1, .4)]}[col]
127 return [self.__load_img_btn(path, col) for col in colors]
128 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
129 DirectButton(
130 image=load_images_btn('exitRight', 'gray'), scale=.05,
131 pos=(.06, 1, .06),
132 parent=self._frm, command=self.__on_close, state=NORMAL, relief=FLAT,
133 frameColor=fcols[0],
134 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
135 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
136 DirectButton(
137 image=load_images_btn('save', 'gray'), scale=.05,
138 pos=(.06, 1, .18),
139 parent=self._frm, command=self.__on_save, state=NORMAL, relief=FLAT,
140 frameColor=fcols[0],
141 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
142 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
143 DirectButton(
144 image=load_images_btn('menuList', 'gray'), scale=.05,
145 pos=(.06, 1, .30),
146 parent=self._frm, command=self.__on_scene_list, state=NORMAL, relief=FLAT,
147 frameColor=fcols[0],
148 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
149 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
150 item_modules = glob('pmachines/items/*.py')
151 item_modules = [basename(i)[:-3] for i in item_modules]
152 self.__new_items = {}
153 for item_module in item_modules:
154 mod_name = 'pmachines.items.' + item_module
155 for member in import_module(mod_name).__dict__.values():
156 if isclass(member) and issubclass(member, Item) and \
157 member != Item:
158 self.__new_items[member.__name__] = member
159 OnscreenText(
160 _('new item'), pos=(.02, .46), parent=self._frm,
161 font=self._common['text_font'],
162 scale=self._common['scale'],
163 fg=self._common['text_fg'],
164 align=TextNode.A_left)
165 DirectOptionMenu(
166 scale=.05,
167 text=_('new item'), pos=(.02, 1, .4), items=list(self.__new_items.keys()),
168 parent=self._frm, command=self.__on_new_item, state=NORMAL,
169 relief=FLAT, item_relief=FLAT,
170 frameColor=fcols[0], item_frameColor=fcols[0],
171 popupMenu_frameColor=fcols[0],
172 popupMarker_frameColor=fcols[0],
173 text_font=self._font, text_fg=(.9, .9, .9, 1),
174 highlightColor=(.9, .9, .9, .9),
175 item_text_font=self._font, item_text_fg=(.9, .9, .9, 1),
176 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
177 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
178 DirectButton(
179 image=load_images_btn('plus', 'gray'), scale=.05,
180 pos=(.06, 1, .58),
181 parent=self._frm, command=self.__on_new_scene, state=NORMAL, relief=FLAT,
182 frameColor=fcols[0],
183 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
184 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
185 messenger.send('editor-start')
186 self.accept('editor-item-click', self.__on_item_click)
187 self.accept('editor-inspector-destroy', self.__on_inspector_destroy)
188
189 def __on_close(self):
190 self.__json['name'] = self.__name_entry.get()
191 self.__json['instructions'] = self.__instructions_entry.get()
192 if self.__compute_hash() == self.__json['version']:
193 self._frm.destroy()
194 else:
195 self.__dialog = YesNoDialog(dialogName='Unsaved changes',
196 text=_('You have unsaved changes. Really quit?'),
197 command=self.__actually_close)
198 self.__dialog['frameColor'] = (.4, .4, .4, .14)
199 self.__dialog['relief'] = FLAT
200 self.__dialog.component('text0')['fg'] = (.9, .9, .9, 1)
201 self.__dialog.component('text0')['font'] = self._font
202 for b in self.__dialog.buttonList:
203 b['frameColor'] = (.4, .4, .4, .14)
204 b.component('text0')['fg'] = (.9, .9, .9, 1)
205 b.component('text0')['font'] = self._font
206 b.component('text1')['fg'] = (.9, .1, .1, 1)
207 b.component('text1')['font'] = self._font
208 b.component('text2')['fg'] = (.9, .9, .1, 1)
209 b.component('text2')['font'] = self._font
210
211 def __on_new_item(self, item):
212 info(f'new {item}')
213 item_json = {}
214 _item = self.__new_items[item](
215 self.__context.world,
216 self.__context.plane_node,
217 self.__context.cb_inst,
218 self.__context.curr_bottom,
219 self.__context.repos,
220 item_json)
221 _item._Item__editing = True
222 self.__add_item(_item)
223 item_json['class'] = _item.__class__.__name__
224 item_json['position'] = list(_item._np.get_pos())
225 self.__json['items'] += [item_json]
226
227 def __actually_close(self, arg):
228 if arg:
229 self.destroy()
230 messenger.send('editor-stop')
231 self.__dialog.cleanup()
232
233 def __on_save(self):
234 self.__json['name'] = self.__name_entry.get()
235 self.__json['instructions'] = self.__instructions_entry.get()
236 self.__json['version'] = self.__compute_hash()
237 json_name = self.__filenamename_entry.get()
238 with open('assets/scenes/%s.json' % json_name, 'w') as f:
239 f.write(dumps(self.__json, indent=2, sort_keys=True))
240
241 def __on_scene_list(self):
242 SceneList()
243
244 def __load_img_btn(self, path, col):
245 img = OnscreenImage('assets/images/buttons/%s.dds' % path)
246 img.set_transparency(True)
247 img.set_color(col)
248 img.detach_node()
249 return img
250
251 def __compute_hash(self):
252 new_dict = deepcopy(self.__json)
253 del new_dict['version']
254 new_dict_str = str(dumps(new_dict, indent=2, sort_keys=True))
255 h = hashlib.new('sha256')
256 h.update(new_dict_str.encode())
257 return h.hexdigest()[:12]
258
259 def __on_item_click(self, item):
260 if self.__inspector and self.__inspector.item == item: return
261 if self.__inspector:
262 self.__inspector.destroy()
263 self.__inspector = Inspector(item, self.__items)
264
265 def __on_inspector_destroy(self):
266 self.__inspector = None
267
268 def __on_new_scene(self):
269 self.destroy()
270 messenger.send('editor-stop')
271 messenger.send('new_scene')
272
273 def destroy(self):
274 self._frm.destroy()
275 if self.__inspector:
276 self.__inspector.destroy()
277 self.ignore('editor-item-click')
278 self.ignore('editor-inspector-destroy')