From 126acb0247fef01bbf8c8380c609be9e8b7416d0 Mon Sep 17 00:00:00 2001 From: Marc Gilleron Date: Tue, 20 Nov 2018 01:43:44 +0000 Subject: [PATCH] Added strings extractor, fix dialogs not freed on plugin disable --- .../tools/extractor.gd | 193 +++++++++++ .../tools/extractor_dialog.gd | 127 +++++++ .../tools/extractor_dialog.tscn | 311 ++++++++++++++++++ .../zylann.translation_editor/tools/plugin.gd | 1 + .../tools/string_edition_dialog.tscn | 7 +- .../tools/translation_editor.gd | 64 +++- 6 files changed, 693 insertions(+), 10 deletions(-) create mode 100644 addons/zylann.translation_editor/tools/extractor.gd create mode 100644 addons/zylann.translation_editor/tools/extractor_dialog.gd create mode 100644 addons/zylann.translation_editor/tools/extractor_dialog.tscn diff --git a/addons/zylann.translation_editor/tools/extractor.gd b/addons/zylann.translation_editor/tools/extractor.gd new file mode 100644 index 0000000..9b530ea --- /dev/null +++ b/addons/zylann.translation_editor/tools/extractor.gd @@ -0,0 +1,193 @@ + +const STATE_SEARCHING = 0 +const STATE_READING_TEXT = 1 + +signal finished(results) + +var _strings = {} +var _thread = null +var _time_before = 0.0 +var _ignored_paths = {} + + +func extract(root, ignored_paths=[]): + _time_before = OS.get_ticks_msec() + assert(_thread == null) + + _ignored_paths.clear() + for p in ignored_paths: + _ignored_paths[root.plus_file(p)] = true + + _thread = Thread.new() + _thread.start(self, "_extract", root) + + +func _extract(root): + _walk(root, funcref(self, "_process_file"), funcref(self, "_filter")) + call_deferred("_finished") + + +func _finished(): + _thread.wait_to_finish() + _thread = null + var elapsed = float(OS.get_ticks_msec() - _time_before) / 1000.0 + print("Extraction took ", elapsed, " seconds") + emit_signal("finished", _strings) + + +func _filter(path): + if path in _ignored_paths: + return false + return true + + +func _process_file(fpath): + var ext = fpath.get_extension() + #print("File ", fpath) + + if ext != "tscn" and ext != "gd": + return + + var f = File.new() + var err = f.open(fpath, File.READ) + if err != OK: + printerr("Could not open '", fpath, "', for read, error ", err) + return + + match ext: + "tscn": + _process_tscn(f, fpath) + "gd": + _process_gd(f, fpath) + + +func _process_tscn(f, fpath): + var pattern = "text =" + var text = "" + var state = STATE_SEARCHING + var line_number = 0 + + while not f.eof_reached(): + var line = f.get_line() + line_number += 1 + + if line == "": + continue + + match state: + + STATE_SEARCHING: + var i = line.find(pattern) + if i != -1: + var begin_quote_index = line.find('"', i + len(pattern)) + if begin_quote_index == -1: + printerr("Could not find begin quote after text property, in ", fpath, " line ", line_number) + continue + var end_quote_index = line.rfind('"') + if end_quote_index != -1 and end_quote_index > begin_quote_index and line[end_quote_index - 1] != '\\': + text = line.substr(begin_quote_index + 1, end_quote_index - begin_quote_index - 1) + if text != "": + _add_string(fpath, line_number, text) + text = "" + else: + # The text may be multiline + text = str(line.right(begin_quote_index + 1), "\n") + state = STATE_READING_TEXT + + STATE_READING_TEXT: + var end_quote_index = line.rfind('"') + if end_quote_index != -1 and line[end_quote_index - 1] != '\\': + text = str(text, line.left(end_quote_index)) + _add_string(fpath, line_number, text) + text = "" + state = STATE_SEARCHING + else: + text = str(text, line, "\n") + + +func _process_gd(f, fpath): + var pattern = "tr(" + var text = "" + var line_number = 0 + + while not f.eof_reached(): + var line = f.get_line().strip_edges() + line_number += 1 + + if line == "" or line[0] == "#": + continue + + # Search for one or multiple tr("...") in the same line + var search_index = 0 + var counter = 0 + while true: + var call_index = line.find(pattern, search_index) + if call_index == -1: + break + if call_index != 0: + if line.substr(call_index - 1, 3).is_valid_identifier(): + # not a tr( call + break + if line[call_index - 1] == '"': + break + # TODO There may be more cases to handle + # They may need regexes or a simplified GDScript parser to extract properly + + var begin_quote_index = line.find('"', call_index) + if begin_quote_index == -1: + # Multiline or procedural strings not supported + printerr("Begin quote not found in ", fpath, " line ", line_number) + break + var end_quote_index = find_unescaped_quote(line, begin_quote_index + 1) + if end_quote_index == -1: + # Multiline or procedural strings not supported + printerr("End quote not found in ", fpath, " line ", line_number) + break + text = line.substr(begin_quote_index + 1, end_quote_index - begin_quote_index - 1) + var end_bracket_index = line.find(')', end_quote_index) + if end_bracket_index == -1: + # Multiline or procedural strings not supported + printerr("End bracket not found in ", fpath, " line ", line_number) + break + _add_string(fpath, line_number, text) + search_index = end_bracket_index + + counter += 1 + assert(counter < 100) + + +static func find_unescaped_quote(s, from): + while true: + var i = s.find('"', from) + if i <= 0: + return i + if s[i - 1] != '\\': + return i + from = i + 1 + + +func _add_string(file, line_number, text): + if not _strings.has(file): + _strings[file] = {} + _strings[file][text] = line_number + + +static func _walk(folder_path, file_action, filter): + #print("Walking dir ", folder_path) + var d = Directory.new() + var err = d.open(folder_path) + if err != OK: + printerr("Could not open directory '", folder_path, "', error ", err) + return + d.list_dir_begin(true, true) + var fname = d.get_next() + while fname != "": + var fullpath = folder_path.plus_file(fname) + if filter == null or filter.call_func(fullpath) == true: + if d.current_is_dir(): + _walk(fullpath, file_action, filter) + else: + file_action.call_func(fullpath) + fname = d.get_next() + return + diff --git a/addons/zylann.translation_editor/tools/extractor_dialog.gd b/addons/zylann.translation_editor/tools/extractor_dialog.gd new file mode 100644 index 0000000..4fcaf12 --- /dev/null +++ b/addons/zylann.translation_editor/tools/extractor_dialog.gd @@ -0,0 +1,127 @@ +tool +extends WindowDialog + +const Extractor = preload("extractor.gd") + +signal import_selected(strings) + +onready var _root_path_edit = get_node("VBoxContainer/HBoxContainer/RootPathEdit") +onready var _summary_label = get_node("VBoxContainer/SummaryLabel") +onready var _results_list = get_node("VBoxContainer/Results") +onready var _progress_bar = get_node("VBoxContainer/ProgressBar") +onready var _extract_button = get_node("VBoxContainer/Buttons/ExtractButton") +onready var _import_button = get_node("VBoxContainer/Buttons/ImportButton") + +var _extractor = null +var _results = {} +var _registered_string_filter = null + + +func _ready(): + _import_button.disabled = true + + +func set_registered_string_filter(registered_string_filter): + assert(registered_string_filter is FuncRef) + _registered_string_filter = registered_string_filter + + +func _notification(what): + if what == NOTIFICATION_VISIBILITY_CHANGED: + if visible: + _summary_label.text = "" + _results.clear() + _update_import_button() + + +func _update_import_button(): + _import_button.disabled = (_results == null or len(_results) == 0) + + +func _on_ExtractButton_pressed(): + if _extractor != null: + return + + var root = _root_path_edit.text.strip_edges() + var d = Directory.new() + if not d.dir_exists(root): + printerr("Directory `", root, "` does not exist") + return + + _extractor = Extractor.new() + _extractor.connect("finished", self, "_on_Extractor_finished") + #_extractor.extract("res://", ["addons"]) + _extractor.extract("res://", []) + + # TODO Progress reporting + _progress_bar.value = 50 + + _extract_button.disabled = true + _import_button.disabled = true + + _summary_label.text = "Extracting..." + + +func _on_ImportButton_pressed(): + emit_signal("import_selected", _results) + _results.clear() + hide() + + +func _on_CancelButton_pressed(): + # TODO Cancel extraction? + hide() + + +func _on_Extractor_finished(results): + print("Extractor finished") + _progress_bar.value = 100 + + _results_list.clear() + + var registered_set = {} + var new_set = {} + + # TODO We might actually want to not filter, in order to update location comments + # Filter results + if _registered_string_filter != null: + + var fpaths = results.keys() + for fpath in fpaths: + var strings_dict = results[fpath] + + var strings = strings_dict.keys() + for text in strings: + if _registered_string_filter.call_func(text): + strings_dict.erase(text) + registered_set[text] = true + + if len(strings_dict) == 0: + results.erase(fpath) + + # Root + _results_list.create_item() + + for fpath in results: + #print(fpath) + var strings = results[fpath] + + for text in strings: + var line_number = strings[text] + #print(" ", line_number, ": `", text, "`") + + var item = _results_list.create_item() + item.set_text(0, text) + item.set_text(1, str(fpath, ": ", line_number)) + #item.set_tooltip( + item.set_metadata(1, fpath) + + new_set[text] = true + + _results = results + _extractor = null + + _update_import_button() + _extract_button.disabled = false + + _summary_label.text = "{0} new, {1} registered".format([len(new_set), len(registered_set)]) diff --git a/addons/zylann.translation_editor/tools/extractor_dialog.tscn b/addons/zylann.translation_editor/tools/extractor_dialog.tscn new file mode 100644 index 0000000..7932e20 --- /dev/null +++ b/addons/zylann.translation_editor/tools/extractor_dialog.tscn @@ -0,0 +1,311 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/zylann.translation_editor/tools/extractor_dialog.gd" type="Script" id=1] + +[node name="ExtractorDialog" type="WindowDialog"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 54.0 +margin_top = 68.0 +margin_right = 694.0 +margin_bottom = 548.0 +rect_min_size = Vector2( 640, 480 ) +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 1 +size_flags_vertical = 1 +popup_exclusive = false +window_title = "String extractor" +resizable = true +script = ExtResource( 1 ) +_sections_unfolded = [ "Rect" ] + +[node name="VBoxContainer" type="VBoxContainer" parent="." index="1"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = 8.0 +margin_top = 8.0 +margin_right = -8.0 +margin_bottom = -8.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 1 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 1 +size_flags_vertical = 1 +alignment = 0 +_sections_unfolded = [ "Margin" ] + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer" index="0"] + +editor/display_folded = true +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_right = 624.0 +margin_bottom = 24.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 1 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 1 +size_flags_vertical = 1 +alignment = 0 + +[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer" index="0"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_top = 5.0 +margin_right = 29.0 +margin_bottom = 19.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 2 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 1 +size_flags_vertical = 4 +text = "Root" +percent_visible = 1.0 +lines_skipped = 0 +max_lines_visible = -1 + +[node name="RootPathEdit" type="LineEdit" parent="VBoxContainer/HBoxContainer" index="1"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 33.0 +margin_right = 624.0 +margin_bottom = 24.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +focus_mode = 2 +mouse_filter = 0 +mouse_default_cursor_shape = 1 +size_flags_horizontal = 3 +size_flags_vertical = 1 +text = "res://" +focus_mode = 2 +context_menu_enabled = true +placeholder_alpha = 0.6 +caret_blink = false +caret_blink_speed = 0.65 +caret_position = 0 +_sections_unfolded = [ "Size Flags" ] + +[node name="SummaryLabel" type="Label" parent="VBoxContainer" index="1"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_top = 28.0 +margin_right = 624.0 +margin_bottom = 42.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 2 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 1 +size_flags_vertical = 4 +percent_visible = 1.0 +lines_skipped = 0 +max_lines_visible = -1 + +[node name="Results" type="Tree" parent="VBoxContainer" index="2"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_top = 46.0 +margin_right = 624.0 +margin_bottom = 396.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = true +focus_mode = 2 +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 1 +size_flags_vertical = 3 +columns = 2 +allow_reselect = false +allow_rmb_select = false +hide_folding = false +hide_root = true +drop_mode_flags = 0 +select_mode = 1 +_sections_unfolded = [ "Size Flags" ] + +[node name="ProgressBar" type="ProgressBar" parent="VBoxContainer" index="3"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_top = 400.0 +margin_right = 624.0 +margin_bottom = 416.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 1 +size_flags_vertical = 0 +min_value = 0.0 +max_value = 100.0 +step = 1.0 +page = 0.0 +value = 0.0 +exp_edit = false +rounded = false +percent_visible = true + +[node name="Spacer" type="Control" parent="VBoxContainer" index="4"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_top = 420.0 +margin_right = 624.0 +margin_bottom = 428.0 +rect_min_size = Vector2( 0, 8 ) +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 1 +size_flags_vertical = 1 +_sections_unfolded = [ "Rect" ] + +[node name="Buttons" type="HBoxContainer" parent="VBoxContainer" index="5"] + +editor/display_folded = true +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_top = 432.0 +margin_right = 624.0 +margin_bottom = 452.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 1 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 1 +size_flags_vertical = 1 +custom_constants/separation = 16 +alignment = 1 +_sections_unfolded = [ "custom_constants" ] + +[node name="ExtractButton" type="Button" parent="VBoxContainer/Buttons" index="0"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 184.0 +margin_right = 239.0 +margin_bottom = 20.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +focus_mode = 2 +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 1 +size_flags_vertical = 1 +toggle_mode = false +enabled_focus_mode = 2 +shortcut = null +group = null +text = "Extract" +flat = false +align = 1 + +[node name="ImportButton" type="Button" parent="VBoxContainer/Buttons" index="1"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 255.0 +margin_right = 370.0 +margin_bottom = 20.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +focus_mode = 2 +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 1 +size_flags_vertical = 1 +toggle_mode = false +enabled_focus_mode = 2 +shortcut = null +group = null +text = "Import selected" +flat = false +align = 1 + +[node name="CancelButton" type="Button" parent="VBoxContainer/Buttons" index="2"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_left = 386.0 +margin_right = 440.0 +margin_bottom = 20.0 +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +focus_mode = 2 +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 1 +size_flags_vertical = 1 +toggle_mode = false +enabled_focus_mode = 2 +shortcut = null +group = null +text = "Cancel" +flat = false +align = 1 + +[node name="Spacer2" type="Control" parent="VBoxContainer" index="6"] + +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 0.0 +anchor_bottom = 0.0 +margin_top = 456.0 +margin_right = 624.0 +margin_bottom = 464.0 +rect_min_size = Vector2( 0, 8 ) +rect_pivot_offset = Vector2( 0, 0 ) +rect_clip_content = false +mouse_filter = 0 +mouse_default_cursor_shape = 0 +size_flags_horizontal = 1 +size_flags_vertical = 1 +_sections_unfolded = [ "Rect" ] + +[connection signal="pressed" from="VBoxContainer/Buttons/ExtractButton" to="." method="_on_ExtractButton_pressed"] + +[connection signal="pressed" from="VBoxContainer/Buttons/ImportButton" to="." method="_on_ImportButton_pressed"] + +[connection signal="pressed" from="VBoxContainer/Buttons/CancelButton" to="." method="_on_CancelButton_pressed"] + + diff --git a/addons/zylann.translation_editor/tools/plugin.gd b/addons/zylann.translation_editor/tools/plugin.gd index 652fb6a..22d5fc9 100644 --- a/addons/zylann.translation_editor/tools/plugin.gd +++ b/addons/zylann.translation_editor/tools/plugin.gd @@ -19,6 +19,7 @@ func _enter_tree(): func _exit_tree(): + print("Translation editor plugin Exit tree") # The main control is not freed when the plugin is disabled _main_control.queue_free() _main_control = null diff --git a/addons/zylann.translation_editor/tools/string_edition_dialog.tscn b/addons/zylann.translation_editor/tools/string_edition_dialog.tscn index 5181b96..0679a82 100644 --- a/addons/zylann.translation_editor/tools/string_edition_dialog.tscn +++ b/addons/zylann.translation_editor/tools/string_edition_dialog.tscn @@ -2,9 +2,8 @@ [ext_resource path="res://addons/zylann.translation_editor/tools/string_edition_dialog.gd" type="Script" id=1] -[node name="StringEditionDialog" type="WindowDialog"] +[node name="StringEditionDialog" type="WindowDialog" index="0"] -visible = false anchor_left = 0.0 anchor_top = 0.0 anchor_right = 0.0 @@ -20,7 +19,7 @@ mouse_default_cursor_shape = 0 size_flags_horizontal = 1 size_flags_vertical = 1 popup_exclusive = false -window_title = "New string" +window_title = "New string ID" resizable = false script = ExtResource( 1 ) @@ -57,7 +56,6 @@ mouse_filter = 2 mouse_default_cursor_shape = 0 size_flags_horizontal = 1 size_flags_vertical = 4 -text = "Already existing" percent_visible = 1.0 lines_skipped = 0 max_lines_visible = -1 @@ -139,6 +137,7 @@ mouse_filter = 0 mouse_default_cursor_shape = 0 size_flags_horizontal = 1 size_flags_vertical = 1 +disabled = true toggle_mode = false enabled_focus_mode = 2 shortcut = null diff --git a/addons/zylann.translation_editor/tools/translation_editor.gd b/addons/zylann.translation_editor/tools/translation_editor.gd index db69c0c..32a04ca 100644 --- a/addons/zylann.translation_editor/tools/translation_editor.gd +++ b/addons/zylann.translation_editor/tools/translation_editor.gd @@ -6,6 +6,7 @@ const PoLoader = preload("po_loader.gd") const Locales = preload("locales.gd") const StringEditionDialog = preload("string_edition_dialog.tscn") const LanguageSelectionDialog = preload("language_selection_dialog.tscn") +const ExtractorDialog = preload("extractor_dialog.tscn") const MENU_FILE_OPEN = 0 const MENU_FILE_SAVE = 1 @@ -13,6 +14,7 @@ const MENU_FILE_SAVE_AS_CSV = 2 const MENU_FILE_SAVE_AS_PO = 3 const MENU_FILE_ADD_LANGUAGE = 4 const MENU_FILE_REMOVE_LANGUAGE = 5 +const MENU_FILE_EXTRACT = 6 const FORMAT_CSV = 0 const FORMAT_GETTEXT = 1 @@ -28,12 +30,14 @@ onready var _status_label = get_node("VBoxContainer/StatusBar/Label") var _string_edit_dialog = null var _language_selection_dialog = null var _remove_language_confirmation_dialog = null +var _extractor_dialog = null var _open_dialog = null var _save_file_dialog = null var _save_folder_dialog = null # This is set when integrated as a Godot plugin var _base_control = null var _translation_edits = {} +var _dialogs_to_free_on_exit = [] var _data = {} var _languages = [] @@ -54,6 +58,8 @@ func _ready(): _file_menu.get_popup().add_separator() _file_menu.get_popup().add_item("Add language...", MENU_FILE_ADD_LANGUAGE) _file_menu.get_popup().add_item("Remove language", MENU_FILE_REMOVE_LANGUAGE) + _file_menu.get_popup().add_separator() + _file_menu.get_popup().add_item("Extractor", MENU_FILE_EXTRACT) _file_menu.get_popup().set_item_disabled(_file_menu.get_popup().get_item_index(MENU_FILE_REMOVE_LANGUAGE), true) _file_menu.get_popup().connect("id_pressed", self, "_on_FileMenu_id_pressed") @@ -70,40 +76,62 @@ func _ready(): func _setup_dialogs(dialogs_parent): + # If this fails, something wrong is happening with parenting of the main view + assert(_open_dialog == null) + _open_dialog = FileDialog.new() _open_dialog.window_title = "Open translations" _open_dialog.add_filter("*.csv ; CSV files") _open_dialog.add_filter("*.po ; Gettext files") _open_dialog.mode = FileDialog.MODE_OPEN_FILE _open_dialog.connect("file_selected", self, "_on_OpenDialog_file_selected") - dialogs_parent.add_child(_open_dialog) + _add_dialog(dialogs_parent, _open_dialog) _save_file_dialog = FileDialog.new() _save_file_dialog.window_title = "Save translations as CSV" _save_file_dialog.add_filter("*.csv ; CSV files") _save_file_dialog.mode = FileDialog.MODE_SAVE_FILE _save_file_dialog.connect("file_selected", self, "_on_SaveFileDialog_file_selected") - dialogs_parent.add_child(_save_file_dialog) + _add_dialog(dialogs_parent, _save_file_dialog) _save_folder_dialog = FileDialog.new() _save_folder_dialog.window_title = "Save translations as gettext .po files" _save_folder_dialog.mode = FileDialog.MODE_OPEN_DIR _save_folder_dialog.connect("dir_selected", self, "_on_SaveFolderDialog_dir_selected") - dialogs_parent.add_child(_save_folder_dialog) + _add_dialog(dialogs_parent, _save_folder_dialog) _string_edit_dialog = StringEditionDialog.instance() _string_edit_dialog.set_validator(funcref(self, "_validate_new_string_id")) _string_edit_dialog.connect("submitted", self, "_on_StringEditionDialog_submitted") - dialogs_parent.add_child(_string_edit_dialog) + _add_dialog(dialogs_parent, _string_edit_dialog) _language_selection_dialog = LanguageSelectionDialog.instance() _language_selection_dialog.connect("language_selected", self, "_on_LanguageSelectionDialog_language_selected") - dialogs_parent.add_child(_language_selection_dialog) + _add_dialog(dialogs_parent, _language_selection_dialog) _remove_language_confirmation_dialog = ConfirmationDialog.new() _remove_language_confirmation_dialog.dialog_text = "Do you really want to remove this language? (There is no undo!)" _remove_language_confirmation_dialog.connect("confirmed", self, "_on_RemoveLanguageConfirmationDialog_confirmed") - dialogs_parent.add_child(_remove_language_confirmation_dialog) + _add_dialog(dialogs_parent, _remove_language_confirmation_dialog) + + _extractor_dialog = ExtractorDialog.instance() + _extractor_dialog.set_registered_string_filter(funcref(self, "_is_string_registered")) + _extractor_dialog.connect("import_selected", self, "_on_ExtractorDialog_import_selected") + _add_dialog(dialogs_parent, _extractor_dialog) + + +func _add_dialog(parent, dialog): + parent.add_child(dialog) + if parent != self: + _dialogs_to_free_on_exit.append(dialog) + + +func _exit_tree(): + # Free dialogs because in the editor they might not be child of the main view... + # Also this code runs in the edited scene view as a `tool` side-effect. + for dialog in _dialogs_to_free_on_exit: + dialog.queue_free() + _dialogs_to_free_on_exit.clear() func configure_for_godot_integration(base_control): @@ -137,6 +165,9 @@ func _on_FileMenu_id_pressed(id): var language = get_current_language() _remove_language_confirmation_dialog.window_title = str("Remove language `", language, "`") _remove_language_confirmation_dialog.popup_centered_minsize() + + MENU_FILE_EXTRACT: + _extractor_dialog.popup_centered_minsize() func _on_EditMenu_id_pressed(id): @@ -400,6 +431,8 @@ func add_new_string(strid): } _data[strid] = s _string_list.add_item(strid) + for language in _languages: + _set_language_modified(language) func rename_string(old_strid, new_strid): @@ -445,3 +478,22 @@ func _remove_language(language): func _on_RemoveLanguageConfirmationDialog_confirmed(): var language = get_current_language() _remove_language(language) + + +# Currently used as callback for filtering +func _is_string_registered(text): + if _data == null: + print("No data") + return false + return _data.has(text) + + +func _on_ExtractorDialog_import_selected(results): + for fpath in results: + var strings = results[fpath] + for text in strings: + # Checking because there might be duplicates, + # strings can be found in multiple places + if not _is_string_registered(text): + add_new_string(text) +