Skip to content

Commit 93a570d

Browse files
Timeline Editor: Add Replace functionality to search (#2550)
Ctrl+R now shows options to replace in the timeline editor (should work very close to the script editors implementation). It also has an option to add the replacement as an item in the Broken Reference Manager, which acts as a "Find and replace in all timelines" option.
1 parent 1618f6d commit 93a570d

File tree

7 files changed

+301
-54
lines changed

7 files changed

+301
-54
lines changed

addons/dialogic/Editor/Common/ReferenceManager_AddReplacementPanel.gd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ func open_existing(_item:TreeItem, info:Dictionary):
5252
%Old.text = info.what
5353
%New.text = info.forwhat
5454

55+
%MatchCase.button_pressed = info.case_sensitive
56+
%WholeWords.button_pressed = info.whole_words
57+
5558
func _on_type_item_selected(index:int) -> void:
5659
match index:
5760
0:

addons/dialogic/Editor/Common/reference_manager_window.gd

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,9 @@ func add_ref_change(old_name:String, new_name:String, type:Types, where:=Where.T
111111
'category':category_name,
112112
'character_names':character_names,
113113
'texts_only':where == Where.TEXTS_ONLY,
114-
'type':type
114+
'type':type,
115+
'case_sensitive':case_sensitive,
116+
'whole_words':whole_words,
115117
})
116118

117119
update_indicator()

addons/dialogic/Editor/TimelineEditor/TextEditor/timeline_editor_text.gd

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,23 @@ func _on_content_item_clicked(label:String) -> void:
205205
return
206206

207207

208-
func _search_timeline(search_text:String) -> bool:
208+
func _search_timeline(search_text:String, match_case := false, whole_words := false) -> bool:
209+
var flags := 0
210+
if match_case:
211+
flags = flags | SEARCH_MATCH_CASE
212+
if whole_words:
213+
flags = flags | SEARCH_WHOLE_WORDS
214+
set_meta("current_search", search_text)
215+
set_meta("current_search_flags", flags)
216+
209217
set_search_text(search_text)
218+
set_search_flags(flags)
210219
queue_redraw()
211-
set_meta("current_search", search_text)
212220

213-
return search(search_text, 0, 0, 0).y != -1
221+
var result := search(search_text, flags, get_selection_from_line(), get_selection_from_column())
222+
if result.y != -1:
223+
select.call_deferred(result.y, result.x, result.y, result.x + search_text.length())
224+
return result.y != -1
214225

215226

216227
func _search_navigate_down() -> void:
@@ -222,8 +233,18 @@ func _search_navigate_up() -> void:
222233

223234

224235
func search_navigate(navigate_up := false) -> void:
225-
if not has_meta("current_search"):
236+
var pos := get_next_search_position(navigate_up)
237+
if pos.x == -1:
226238
return
239+
select(pos.y, pos.x, pos.y, pos.x+len(get_meta("current_search")))
240+
set_caret_line(pos.y)
241+
center_viewport_to_caret()
242+
queue_redraw()
243+
244+
245+
func get_next_search_position(navigate_up := false) -> Vector2i:
246+
if not has_meta("current_search"):
247+
return Vector2i(-1, -1)
227248
var pos: Vector2i
228249
var search_from_line := 0
229250
var search_from_column := 0
@@ -244,11 +265,56 @@ func search_navigate(navigate_up := false) -> void:
244265
search_from_line = get_caret_line()
245266
search_from_column = get_caret_column()
246267

247-
pos = search(get_meta("current_search"), 4 if navigate_up else 0, search_from_line, search_from_column)
248-
select(pos.y, pos.x, pos.y, pos.x+len(get_meta("current_search")))
268+
var flags := get_meta("current_search_flags", 0)
269+
if navigate_up:
270+
flags = flags | SEARCH_BACKWARDS
271+
print()
272+
pos = search(get_meta("current_search"), flags, search_from_line, search_from_column)
273+
return pos
274+
275+
276+
func replace(replace_text:String) -> void:
277+
if has_selection():
278+
set_caret_line(get_selection_from_line())
279+
set_caret_column(get_selection_from_column())
280+
281+
var pos := get_next_search_position()
282+
if pos.x == -1:
283+
return
284+
285+
if not has_meta("current_search"):
286+
return
287+
288+
begin_complex_operation()
289+
insert_text("@@", pos.y, pos.x)
290+
if get_meta("current_search_flags") & SEARCH_MATCH_CASE:
291+
text = text.replace("@@"+get_meta("current_search"), replace_text)
292+
else:
293+
text = text.replacen("@@"+get_meta("current_search"), replace_text)
294+
end_complex_operation()
295+
249296
set_caret_line(pos.y)
250-
center_viewport_to_caret()
251-
queue_redraw()
297+
set_caret_column(pos.x)
298+
299+
timeline_editor.replace_in_timeline()
300+
301+
302+
func replace_all(replace_text:String) -> void:
303+
begin_complex_operation()
304+
var next_pos := get_next_search_position()
305+
var counter := 0
306+
while next_pos.y != -1:
307+
insert_text("@@", next_pos.y, next_pos.x)
308+
if get_meta("current_search_flags") & SEARCH_MATCH_CASE:
309+
text = text.replace("@@"+get_meta("current_search"), replace_text)
310+
else:
311+
text = text.replacen("@@"+get_meta("current_search"), replace_text)
312+
next_pos = get_next_search_position()
313+
set_caret_line(next_pos.y)
314+
set_caret_column(next_pos.x)
315+
end_complex_operation()
316+
317+
timeline_editor.replace_in_timeline()
252318

253319

254320
################################################################################

addons/dialogic/Editor/TimelineEditor/VisualEditor/timeline_editor_visual.gd

Lines changed: 94 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,11 +1228,13 @@ func get_previous_character(double_previous := false) -> DialogicCharacter:
12281228
################################################################################
12291229

12301230
var search_results := {}
1231-
func _search_timeline(search_text:String) -> bool:
1232-
#for event in search_results:
1233-
#if is_instance_valid(search_results[event]):
1234-
#search_results[event].set_search_text("")
1235-
#
1231+
func _search_timeline(search_text:String, match_case := false, whole_words := false) -> bool:
1232+
var flags := 0
1233+
if match_case:
1234+
flags = flags | TextEdit.SEARCH_MATCH_CASE
1235+
if whole_words:
1236+
flags = flags | TextEdit.SEARCH_WHOLE_WORDS
1237+
12361238
search_results.clear()
12371239

12381240
# This checks all text events for whether they contain the text.
@@ -1245,13 +1247,15 @@ func _search_timeline(search_text:String) -> bool:
12451247

12461248
text_field.deselect()
12471249
text_field.set_search_text(search_text)
1250+
text_field.set_search_flags(flags)
12481251

1249-
if text_field.search(search_text, 0, 0, 0).x != -1:
1252+
if text_field.search(search_text, flags, 0, 0).x != -1:
12501253
search_results[block] = text_field
12511254

12521255
text_field.queue_redraw()
12531256

12541257
set_meta("current_search", search_text)
1258+
set_meta("current_search_flags", flags)
12551259

12561260
search_navigate(false)
12571261

@@ -1267,10 +1271,24 @@ func _search_navigate_up() -> void:
12671271

12681272

12691273
func search_navigate(navigate_up := false) -> void:
1274+
var next_pos := get_next_search_position(navigate_up)
1275+
if next_pos:
1276+
var event: Node = next_pos[0]
1277+
var field: TextEdit = next_pos[1]
1278+
var result: Vector2i = next_pos[2]
1279+
if not event in selected_items:
1280+
select_item(next_pos[0], false)
1281+
%TimelineArea.ensure_control_visible(event)
1282+
event._on_ToggleBodyVisibility_toggled(true)
1283+
field.call_deferred("select", result.y, result.x, result.y, result.x+len(get_meta("current_search")))
1284+
1285+
1286+
func get_next_search_position(navigate_up:= false, include_current := false) -> Array:
12701287
var search_text: String = get_meta("current_search", "")
1288+
var search_flags: int = get_meta("current_search_flags", 0)
12711289

12721290
if search_results.is_empty() or %Timeline.get_child_count() == 0:
1273-
return
1291+
return []
12741292

12751293
# We start the search on the selected item,
12761294
# so these checks make sure something sensible is selected
@@ -1294,16 +1312,19 @@ func search_navigate(navigate_up := false) -> void:
12941312

12951313
var event: Node = selected_items[0]
12961314
var counter := 0
1315+
var first := true
12971316
while true:
12981317
counter += 1
12991318
var field: TextEdit = search_results[event]
13001319
field.queue_redraw()
13011320

13021321
# First locates the next result in this field
1303-
var result := search_text_field(field, search_text, navigate_up)
1322+
var result := search_text_field(field, search_text, search_flags, navigate_up, first and include_current)
13041323
var current_line := field.get_selection_from_line() if field.has_selection() else -1
13051324
var current_column := field.get_selection_from_column() if field.has_selection() else -1
13061325

1326+
first = false
1327+
13071328
# Determines if the found result is valid or navigation should continue into the next event
13081329
var next_is_in_this_event := false
13091330
if result.y == -1:
@@ -1313,17 +1334,14 @@ func search_navigate(navigate_up := false) -> void:
13131334
current_line = field.get_line_count()-1
13141335
current_column = field.get_line(current_line).length()
13151336
next_is_in_this_event = result.x < current_column or result.y < current_line
1337+
elif include_current:
1338+
next_is_in_this_event = true
13161339
else:
13171340
next_is_in_this_event = result.x > current_column or result.y > current_line
13181341

1319-
# If the next result was found, select it and break out of the loop
1342+
# If the next result was found return it
13201343
if next_is_in_this_event:
1321-
if not event in selected_items:
1322-
select_item(event, false)
1323-
%TimelineArea.ensure_control_visible(event)
1324-
event._on_ToggleBodyVisibility_toggled(true)
1325-
field.call_deferred("select", result.y, result.x, result.y, result.x+len(search_text))
1326-
break
1344+
return [event, field, result]
13271345

13281346
# Otherwise deselct this field and continue in the next/previous
13291347
field.deselect()
@@ -1333,9 +1351,10 @@ func search_navigate(navigate_up := false) -> void:
13331351
if counter > 5:
13341352
print("[Dialogic] Search failed.")
13351353
break
1354+
return []
13361355

13371356

1338-
func search_text_field(field:TextEdit, search_text := "", navigate_up:= false) -> Vector2i:
1357+
func search_text_field(field:TextEdit, search_text := "", flags:= 0, navigate_up:= false, include_current := false) -> Vector2i:
13391358
var search_from_line: int = 0
13401359
var search_from_column: int = 0
13411360
if field.has_selection():
@@ -1347,6 +1366,9 @@ func search_text_field(field:TextEdit, search_text := "", navigate_up:= false) -
13471366
if search_from_line == -1:
13481367
return Vector2i(-1, -1)
13491368
search_from_column = field.get_line(search_from_line).length()-1
1369+
elif include_current:
1370+
search_from_line = field.get_selection_from_line()
1371+
search_from_column = field.get_selection_from_column()
13501372
else:
13511373
search_from_line = field.get_selection_to_line()
13521374
search_from_column = field.get_selection_to_column()
@@ -1355,7 +1377,62 @@ func search_text_field(field:TextEdit, search_text := "", navigate_up:= false) -
13551377
search_from_line = field.get_line_count()-1
13561378
search_from_column = field.get_line(search_from_line).length()-1
13571379

1358-
var search := field.search(search_text, 4 if navigate_up else 0, search_from_line, search_from_column)
1380+
if navigate_up:
1381+
flags = flags | TextEdit.SEARCH_BACKWARDS
1382+
1383+
var search := field.search(search_text, flags, search_from_line, search_from_column)
13591384
return search
13601385

1386+
1387+
func replace(replace_text:String) -> void:
1388+
var next_pos := get_next_search_position(false, true)
1389+
var event: Node = next_pos[0]
1390+
var field: TextEdit = next_pos[1]
1391+
var result: Vector2i = next_pos[2]
1392+
1393+
if field.has_selection():
1394+
field.set_caret_column(field.get_selection_from_column())
1395+
field.set_caret_line(field.get_selection_from_line())
1396+
1397+
field.begin_complex_operation()
1398+
field.insert_text("@@", result.y, result.x)
1399+
if get_meta("current_search_flags") & TextEdit.SEARCH_MATCH_CASE:
1400+
field.text = field.text.replace("@@"+get_meta("current_search"), replace_text)
1401+
else:
1402+
field.text = field.text.replacen("@@"+get_meta("current_search"), replace_text)
1403+
field.end_complex_operation()
1404+
1405+
timeline_editor.replace_in_timeline()
1406+
1407+
1408+
func replace_all(replace_text:String) -> void:
1409+
var next_pos := get_next_search_position()
1410+
if not next_pos:
1411+
return
1412+
var event: Node = next_pos[0]
1413+
var field: TextEdit = next_pos[1]
1414+
var result: Vector2i = next_pos[2]
1415+
field.begin_complex_operation()
1416+
while next_pos:
1417+
event = next_pos[0]
1418+
if field != next_pos[1]:
1419+
field.end_complex_operation()
1420+
field = next_pos[1]
1421+
field.begin_complex_operation()
1422+
result = next_pos[2]
1423+
1424+
if field.has_selection():
1425+
field.set_caret_column(field.get_selection_from_column())
1426+
field.set_caret_line(field.get_selection_from_line())
1427+
1428+
field.insert_text("@@", result.y, result.x)
1429+
if get_meta("current_search_flags") & TextEdit.SEARCH_MATCH_CASE:
1430+
field.text = field.text.replace("@@"+get_meta("current_search"), replace_text)
1431+
else:
1432+
field.text = field.text.replacen("@@"+get_meta("current_search"), replace_text)
1433+
1434+
next_pos = get_next_search_position()
1435+
field.end_complex_operation()
1436+
timeline_editor.replace_in_timeline()
1437+
13611438
#endregion

addons/dialogic/Editor/TimelineEditor/shortcut_popup.gd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ var shortcuts := [
1717
{"shortcut":"Alt/Opt+Down", "text":"Move selected events/lines down"},
1818
{},
1919
{"shortcut":"Ctrl+F", "text":"Search"},
20+
{"shortcut":"Ctrl+R", "text":"Replace"},
2021
{},
2122
{"shortcut":"Ctrl+F5", "text":"Play timeline", "platform":"-macOS"},
2223
{"shortcut":"Ctrl+B", "text":"Play timeline", "platform":"macOS"},

0 commit comments

Comments
 (0)