]> Untitled Git - wolf-seeking-sheep.git/blob - addons/dialogic/Editor/Common/broken_reference_manager.gd
Initial Godot project with Dialogic 2.0-Alpha-17
[wolf-seeking-sheep.git] / addons / dialogic / Editor / Common / broken_reference_manager.gd
1 @tool
2 extends VSplitContainer
3
4 ## This manager shows a list of changed references and allows searching for them and replacing them.
5
6 var reference_changes: Array[Dictionary] = []:
7         set(changes):
8                 reference_changes = changes
9                 update_indicator()
10
11 var search_regexes: Array[Array]
12 var finder_thread: Thread
13 var progress_mutex: Mutex
14 var progress_percent: float = 0.0
15 var progress_message: String = ""
16
17
18 func _ready() -> void:
19         if owner.get_parent() is SubViewport:
20                 return
21
22         %TabA.text = "Broken References"
23         %TabA.icon = get_theme_icon("Unlinked", "EditorIcons")
24
25         owner.get_parent().visibility_changed.connect(func(): if is_visible_in_tree(): open())
26
27         %ReplacementSection.hide()
28
29         %CheckButton.icon = get_theme_icon("Search", "EditorIcons")
30         %Replace.icon = get_theme_icon("ArrowRight", "EditorIcons")
31
32         %State.add_theme_color_override("font_color", get_theme_color("warning_color", "Editor"))
33         visibility_changed.connect(func(): if !visible: close())
34         await get_parent().ready
35
36         var tab_button: Control = %TabA
37         var dot := Sprite2D.new()
38         dot.texture = get_theme_icon("GuiGraphNodePort", "EditorIcons")
39         dot.scale = Vector2(0.8, 0.8)
40         dot.z_index = 10
41         dot.position = Vector2(tab_button.size.x, tab_button.size.y*0.25)
42         dot.modulate = get_theme_color("warning_color", "Editor").lightened(0.5)
43
44         tab_button.add_child(dot)
45         update_indicator()
46
47
48 func open() -> void:
49         %ReplacementEditPanel.hide()
50         %ReplacementSection.hide()
51         %ChangeTree.clear()
52         %ChangeTree.create_item()
53         %ChangeTree.set_column_expand(0, false)
54         %ChangeTree.set_column_expand(2, false)
55         %ChangeTree.set_column_custom_minimum_width(2, 50)
56         var categories := {null:%ChangeTree.get_root()}
57         for i in reference_changes:
58                 var parent: TreeItem = null
59                 if !i.get('category', null) in categories:
60                         parent = %ChangeTree.create_item()
61                         parent.set_text(1, i.category)
62                         parent.set_custom_color(1, get_theme_color("disabled_font_color", "Editor"))
63                         categories[i.category] = parent
64                 else:
65                         parent = categories[i.get('category')]
66
67                 var item: TreeItem = %ChangeTree.create_item(parent)
68                 item.set_text(1, i.what+" -> "+i.forwhat)
69                 item.add_button(1, get_theme_icon("Edit", "EditorIcons"), 1, false, 'Edit')
70                 item.add_button(1, get_theme_icon("Remove", "EditorIcons"), 0, false, 'Remove Change from List')
71                 item.set_cell_mode(0, TreeItem.CELL_MODE_CHECK)
72                 item.set_checked(0, true)
73                 item.set_editable(0, true)
74                 item.set_metadata(0, i)
75         %CheckButton.disabled = reference_changes.is_empty()
76
77
78 func _on_change_tree_button_clicked(item:TreeItem, column:int, id:int, mouse_button_index:int) -> void:
79         if id == 0:
80                 reference_changes.erase(item.get_metadata(0))
81                 if item.get_parent().get_child_count() == 1:
82                         item.get_parent().free()
83                 else:
84                         item.free()
85                 update_indicator()
86                 %CheckButton.disabled = reference_changes.is_empty()
87
88         if id == 1:
89                 %ReplacementEditPanel.open_existing(item, item.get_metadata(0))
90
91         %ReplacementSection.hide()
92
93
94 func _on_change_tree_item_edited() -> void:
95         if !%ChangeTree.get_selected():
96                 return
97         %CheckButton.disabled = false
98
99
100 func _on_check_button_pressed() -> void:
101         var to_be_checked: Array[Dictionary]= []
102         var item: TreeItem = %ChangeTree.get_root()
103         while item.get_next_visible():
104                 item = item.get_next_visible()
105
106                 if item.get_child_count():
107                         continue
108
109                 if item.is_checked(0):
110                         to_be_checked.append(item.get_metadata(0))
111                         to_be_checked[-1]['item'] = item
112                         to_be_checked[-1]['count'] = 0
113
114         open_finder(to_be_checked)
115         %CheckButton.disabled = true
116
117
118 func open_finder(replacements:Array[Dictionary]) -> void:
119         %ReplacementSection.show()
120         %Progress.show()
121         %ReferenceTree.hide()
122
123         search_regexes = []
124         for i in replacements:
125                 if i.has('character_names') and !i.character_names.is_empty():
126                         i['character_regex'] = RegEx.create_from_string("(?m)^(join|update|leave)?\\s*("+str(i.character_names).replace('"', '').replace(', ', '|').trim_suffix(']').trim_prefix('[').replace('/', '\\/')+")(?(1).*|.*:)")
127
128                 for regex_string in i.regex:
129                         var regex := RegEx.create_from_string(regex_string)
130                         search_regexes.append([regex, i])
131
132         finder_thread = Thread.new()
133         progress_mutex = Mutex.new()
134         finder_thread.start(search_timelines.bind(search_regexes))
135
136
137 func _process(delta: float) -> void:
138         if finder_thread and finder_thread.is_started():
139                 if finder_thread.is_alive():
140                         progress_mutex.lock()
141                         %State.text = progress_message
142                         %Progress.value = progress_percent
143                         progress_mutex.unlock()
144                 else:
145                         var finds: Variant = finder_thread.wait_to_finish()
146                         display_search_results(finds)
147
148
149
150 func display_search_results(finds:Array[Dictionary]) -> void:
151         %Progress.hide()
152         %ReferenceTree.show()
153         for regex_info in search_regexes:
154                 regex_info[1]['item'].set_text(2, str(regex_info[1]['count']))
155
156         update_count_coloring()
157         %State.text = str(len(finds))+ " occurrences found"
158
159         %ReferenceTree.clear()
160         %ReferenceTree.set_column_expand(0, false)
161         %ReferenceTree.create_item()
162
163         var timelines := {}
164         var height := 0
165         for i in finds:
166                 var parent: TreeItem = null
167                 if !i.timeline in timelines:
168                         parent = %ReferenceTree.create_item()
169                         parent.set_text(1, i.timeline)
170                         parent.set_custom_color(1, get_theme_color("disabled_font_color", "Editor"))
171                         timelines[i.timeline] = parent
172                         height += %ReferenceTree.get_item_area_rect(parent).size.y+10
173                 else:
174                         parent = timelines[i.timeline]
175
176                 var item: TreeItem = %ReferenceTree.create_item(parent)
177                 item.set_text(1, 'Line '+str(i.line_number)+': '+i.line)
178                 item.set_tooltip_text(1, i.info.what+' -> '+i.info.forwhat)
179                 item.set_cell_mode(0, TreeItem.CELL_MODE_CHECK)
180                 item.set_checked(0, true)
181                 item.set_editable(0, true)
182                 item.set_metadata(0, i)
183                 height += %ReferenceTree.get_item_area_rect(item).size.y+10
184                 var change_item: TreeItem = i.info.item
185                 change_item.set_meta('found_items', change_item.get_meta('found_items', [])+[item])
186
187         %ReferenceTree.custom_minimum_size.y = min(height, 200)
188
189         %ReferenceTree.visible = !finds.is_empty()
190         %Replace.disabled = finds.is_empty()
191         if finds.is_empty():
192                 %State.text = "Nothing found"
193         else:
194                 %Replace.grab_focus()
195
196
197 func search_timelines(regexes:Array[Array]) -> Array[Dictionary]:
198         var finds: Array[Dictionary] = []
199
200         var timeline_paths := DialogicResourceUtil.list_resources_of_type('.dtl')
201
202         var progress := 0
203         var progress_max: float = len(timeline_paths)*len(regexes)
204
205         for timeline_path:String in timeline_paths:
206
207                 var timeline_file := FileAccess.open(timeline_path, FileAccess.READ)
208                 var timeline_text: String = timeline_file.get_as_text()
209                 var timeline_event: PackedStringArray = timeline_text.split('\n')
210                 timeline_file.close()
211
212                 for regex_info in regexes:
213                         progress += 1
214                         progress_mutex.lock()
215                         progress_percent = 1/progress_max*progress
216                         progress_message = "Searching '"+timeline_path+"' for "+regex_info[1].what+' -> '+regex_info[1].forwhat
217                         progress_mutex.unlock()
218                         for i in regex_info[0].search_all(timeline_text):
219                                 if regex_info[1].has('character_regex'):
220                                         if regex_info[1].character_regex.search(get_line(timeline_text, i.get_start()+1)) == null:
221                                                 continue
222
223                                 var line_number := timeline_text.count('\n', 0, i.get_start()+1)+1
224                                 var line := timeline_text.get_slice('\n', line_number-1)
225                                 finds.append({
226                                 'match':i,
227                                 'timeline':timeline_path,
228                                 'info': regex_info[1],
229                                 'line_number': line_number,
230                                 'line': line,
231                                 'line_start': timeline_text.rfind('\n', i.get_start())
232                                 })
233                                 regex_info[1]['count'] += 1
234         return finds
235
236
237 func _exit_tree() -> void:
238         # Shutting of
239         if finder_thread and finder_thread.is_alive():
240                 finder_thread.wait_to_finish()
241
242
243 func get_line(string:String, at_index:int) -> String:
244         return string.substr(max(string.rfind('\n', at_index), 0), string.find('\n', at_index)-string.rfind('\n', at_index))
245
246
247 func update_count_coloring() -> void:
248         var item: TreeItem = %ChangeTree.get_root()
249         while item.get_next_visible():
250                 item = item.get_next_visible()
251
252                 if item.get_child_count():
253                         continue
254                 if int(item.get_text(2)) > 0:
255                         item.set_custom_bg_color(1, get_theme_color("warning_color", "Editor").darkened(0.8))
256                         item.set_custom_color(1, get_theme_color("warning_color", "Editor"))
257                         item.set_custom_color(2, get_theme_color("warning_color", "Editor"))
258                 else:
259                         item.set_custom_color(2, get_theme_color("success_color", "Editor"))
260                         item.set_custom_color(1, get_theme_color("readonly_font_color", "Editor"))
261                         if item.get_button_count(1):
262                                 item.erase_button(1, 1)
263                         item.add_button(1, get_theme_icon("Eraser", "EditorIcons"), -1, true, "This reference was not found anywhere and will be removed from this list.")
264
265
266 func _on_replace_pressed() -> void:
267         var to_be_replaced: Array[Dictionary]= []
268         var item: TreeItem = %ReferenceTree.get_root()
269         var affected_timelines: Array[String]= []
270
271         while item.get_next_visible():
272                 item = item.get_next_visible()
273
274                 if item.get_child_count():
275                         continue
276
277                 if item.is_checked(0):
278                         to_be_replaced.append(item.get_metadata(0))
279                         to_be_replaced[-1]['f_item'] = item
280                         if !item.get_metadata(0).timeline in affected_timelines:
281                                 affected_timelines.append(item.get_metadata(0).timeline)
282         replace(affected_timelines, to_be_replaced)
283
284
285 func replace(timelines:Array[String], replacement_info:Array[Dictionary]) -> void:
286         var reopen_timeline := ""
287         var timeline_editor: DialogicEditor = find_parent('EditorView').editors_manager.editors['Timeline'].node
288         if timeline_editor.current_resource != null and timeline_editor.current_resource.resource_path in timelines:
289                 reopen_timeline = timeline_editor.current_resource.resource_path
290                 find_parent('EditorView').editors_manager.clear_editor(timeline_editor)
291
292         replacement_info.sort_custom(func(a,b): return a.match.get_start() < b.match.get_start())
293
294         for timeline_path in timelines:
295                 %State.text = "Loading '"+timeline_path+"'"
296
297                 var timeline_file := FileAccess.open(timeline_path, FileAccess.READ_WRITE)
298                 var timeline_text: String = timeline_file.get_as_text()
299                 var timeline_events := timeline_text.split('\n')
300                 timeline_file.close()
301
302                 var idx := 1
303                 var offset_correction := 0
304                 for replacement in replacement_info:
305                         if replacement.timeline != timeline_path:
306                                 continue
307
308                         %State.text = "Replacing in '"+timeline_path + "' ("+str(idx)+"/"+str(len(replacement_info))+")"
309                         var group := 'replace'
310                         if not 'replace' in replacement.match.names:
311                                 group = ''
312
313
314                         timeline_text = timeline_text.substr(0, replacement.match.get_start(group) + offset_correction) + \
315                                                         replacement.info.regex_replacement + \
316                                                         timeline_text.substr(replacement.match.get_end(group) + offset_correction)
317                         offset_correction += len(replacement.info.regex_replacement)-len(replacement.match.get_string(group))
318
319                         replacement.info.count -= 1
320                         replacement.info.item.set_text(2, str(replacement.info.count))
321                         replacement.f_item.set_custom_bg_color(1, get_theme_color("success_color", "Editor").darkened(0.8))
322
323                 timeline_file = FileAccess.open(timeline_path, FileAccess.WRITE)
324                 timeline_file.store_string(timeline_text.strip_edges(false, true))
325                 timeline_file.close()
326
327                 if ResourceLoader.has_cached(timeline_path):
328                         var tml := load(timeline_path)
329                         tml.from_text(timeline_text)
330
331         if !reopen_timeline.is_empty():
332                 find_parent('EditorView').editors_manager.edit_resource(load(reopen_timeline), false, true)
333
334         update_count_coloring()
335
336         %Replace.disabled = true
337         %CheckButton.disabled = false
338         %State.text = "Done Replacing"
339
340
341 func update_indicator() -> void:
342         %TabA.get_child(0).visible = !reference_changes.is_empty()
343
344
345 func close() -> void:
346         var item: TreeItem = %ChangeTree.get_root()
347         if item:
348                 while item.get_next_visible():
349                         item = item.get_next_visible()
350
351                         if item.get_child_count():
352                                 continue
353                         if item.get_text(2) != "" and int(item.get_text(2)) == 0:
354                                 reference_changes.erase(item.get_metadata(0))
355         for i in reference_changes:
356                 i.item = null
357         DialogicUtil.set_editor_setting('reference_changes', reference_changes)
358         update_indicator()
359         find_parent("ReferenceManager").update_indicator()
360
361
362 func _on_add_button_pressed() -> void:
363         %ReplacementEditPanel._on_add_pressed()