commit af2b69645f7517194c3e506714612651871ca2b1 Author: teebarjunk Date: Sun Oct 10 23:10:22 2021 -0400 1.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acdfd84 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Godot-specific ignores +.import/ +export.cfg +export_presets.cfg + +# Imported translations (automatically generated from CSV files) +*.translation + +# Mono-specific ignores +.mono/ +data_*/ + +# Text editor related +test_files/ +.trash/ +.trash_info.json diff --git a/TextEditor.tscn b/TextEditor.tscn new file mode 100644 index 0000000..dfc986d --- /dev/null +++ b/TextEditor.tscn @@ -0,0 +1,255 @@ +[gd_scene load_steps=10 format=2] + +[ext_resource path="res://addons/text_editor/file_buttons.gd" type="Script" id=1] +[ext_resource path="res://addons/text_editor/TextEditor.gd" type="Script" id=2] +[ext_resource path="res://addons/text_editor/list_files.gd" type="Script" id=3] +[ext_resource path="res://addons/text_editor/text_edit.gd" type="Script" id=4] +[ext_resource path="res://addons/text_editor/list_symbols.gd" type="Script" id=5] +[ext_resource path="res://addons/text_editor/tab_scroll.gd" type="Script" id=6] +[ext_resource path="res://addons/text_editor/list_tags.gd" type="Script" id=7] +[ext_resource path="res://addons/text_editor/line_edit.gd" type="Script" id=8] +[ext_resource path="res://addons/text_editor/meta_panel.gd" type="Script" id=9] + +[node name="text_editor" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 2 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="c" type="VBoxContainer" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +custom_constants/separation = 0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="c" type="PanelContainer" parent="c"] +margin_right = 1024.0 +margin_bottom = 34.0 + +[node name="c" type="HBoxContainer" parent="c/c"] +margin_left = 7.0 +margin_top = 7.0 +margin_right = 1017.0 +margin_bottom = 27.0 +script = ExtResource( 1 ) + +[node name="test" type="Button" parent="c/c/c"] +margin_right = 56.0 +margin_bottom = 20.0 +text = "update" + +[node name="file_button" type="MenuButton" parent="c/c/c"] +margin_left = 60.0 +margin_right = 92.0 +margin_bottom = 20.0 +text = "file" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="c3" type="HSplitContainer" parent="c"] +margin_top = 34.0 +margin_right = 1024.0 +margin_bottom = 600.0 +size_flags_vertical = 3 +split_offset = -300 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="c2" type="PanelContainer" parent="c/c3"] +margin_right = 206.0 +margin_bottom = 566.0 +rect_min_size = Vector2( 200, 0 ) +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="c" type="Panel" parent="c/c3/c2"] +margin_left = 7.0 +margin_top = 7.0 +margin_right = 199.0 +margin_bottom = 559.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="list_files" type="RichTextLabel" parent="c/c3/c2/c"] +anchor_right = 1.0 +anchor_bottom = 1.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +bbcode_enabled = true +meta_underlined = false +script = ExtResource( 3 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="popup" type="PopupMenu" parent="c/c3/c2/c/list_files"] +margin_right = 69.0 +margin_bottom = 68.0 + +[node name="drag_label" type="RichTextLabel" parent="c/c3/c2/c/list_files"] +visible = false +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +bbcode_enabled = true +fit_content_height = true + +[node name="c" type="HSplitContainer" parent="c/c3"] +margin_left = 218.0 +margin_right = 1024.0 +margin_bottom = 566.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +split_offset = -80 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="c" type="VBoxContainer" parent="c/c3/c"] +margin_right = 614.0 +margin_bottom = 566.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="line_edit" type="LineEdit" parent="c/c3/c/c"] +visible = false +margin_right = 614.0 +margin_bottom = 24.0 +script = ExtResource( 8 ) + +[node name="tab_container" type="TabContainer" parent="c/c3/c/c"] +margin_right = 614.0 +margin_bottom = 547.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +tab_align = 0 +drag_to_rearrange_enabled = true +script = ExtResource( 6 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="tab_prefab" type="TextEdit" parent="c/c3/c/c/tab_container"] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = 4.0 +margin_top = 32.0 +margin_right = -4.0 +margin_bottom = -4.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +highlight_current_line = true +syntax_highlighting = true +show_line_numbers = true +draw_tabs = true +breakpoint_gutter = true +fold_gutter = true +highlight_all_occurrences = true +minimap_draw = true +script = ExtResource( 4 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="meta" type="RichTextLabel" parent="c/c3/c/c"] +margin_top = 551.0 +margin_right = 614.0 +margin_bottom = 566.0 +bbcode_enabled = true +fit_content_height = true +script = ExtResource( 9 ) + +[node name="c2" type="PanelContainer" parent="c/c3/c"] +margin_left = 626.0 +margin_right = 806.0 +margin_bottom = 566.0 +rect_min_size = Vector2( 100, 0 ) +size_flags_vertical = 3 + +[node name="c" type="VSplitContainer" parent="c/c3/c/c2"] +margin_left = 7.0 +margin_top = 7.0 +margin_right = 173.0 +margin_bottom = 559.0 +custom_constants/autohide = 0 + +[node name="c" type="Panel" parent="c/c3/c/c2/c"] +margin_right = 166.0 +margin_bottom = 270.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="list_symbols" type="RichTextLabel" parent="c/c3/c/c2/c/c"] +anchor_right = 1.0 +anchor_bottom = 1.0 +size_flags_vertical = 3 +bbcode_enabled = true +bbcode_text = "tags" +meta_underlined = false +text = "tags" +script = ExtResource( 5 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="c2" type="Panel" parent="c/c3/c/c2/c"] +margin_top = 282.0 +margin_right = 166.0 +margin_bottom = 552.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="list_tags" type="RichTextLabel" parent="c/c3/c/c2/c/c2"] +anchor_right = 1.0 +anchor_bottom = 1.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +bbcode_enabled = true +bbcode_text = "tags" +meta_underlined = false +text = "tags" +script = ExtResource( 7 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="popup" type="ConfirmationDialog" parent="."] +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +margin_left = -100.0 +margin_top = -35.0 +margin_right = 100.0 +margin_bottom = 35.0 + +[node name="popup_unsaved" type="ConfirmationDialog" parent="."] +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +margin_left = -100.0 +margin_top = -35.0 +margin_right = 100.0 +margin_bottom = 35.0 +window_title = "Warning" +dialog_text = "Unsaved data will be lost." + +[node name="file_dialog" type="FileDialog" parent="."] +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +margin_left = -297.5 +margin_top = -157.0 +margin_right = 297.5 +margin_bottom = 157.0 +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/addons/text_editor/TE_RichTextLabel.gd b/addons/text_editor/TE_RichTextLabel.gd new file mode 100644 index 0000000..fcac2db --- /dev/null +++ b/addons/text_editor/TE_RichTextLabel.gd @@ -0,0 +1,21 @@ +extends RichTextLabel +class_name TE_RichTextLabel + + +func _ready(): + add_font_override("normal_font", owner.FONT_R) + add_font_override("bold_font", owner.FONT_B) + add_font_override("italics_font", owner.FONT_I) + add_font_override("bold_italics_font", owner.FONT_BI) + +func table(rows) -> String: + var cells = "" + var clr = Color.white.darkened(.5).to_html() + for i in len(rows): + if i == 0: + for item in rows[i]: + cells += "[cell][b]%s[/b][/cell][/color]" % item + else: + for item in rows[i]: + cells += "[cell][color=#%s]%s[/color][/cell]" % [clr, item] + return "[center][table=%s]%s[/table][/center]" % [len(rows[0]), cells] diff --git a/addons/text_editor/TE_Util.gd b/addons/text_editor/TE_Util.gd new file mode 100644 index 0000000..c6a575a --- /dev/null +++ b/addons/text_editor/TE_Util.gd @@ -0,0 +1,139 @@ +class_name TE_Util + +static func load_json(path:String) -> Dictionary: + var f:File = File.new() + if f.file_exists(path): + f.open(path, File.READ) + var out = JSON.parse(f.get_as_text()).result + f.close() + return out + return {} + +static func save_json(path:String, data:Dictionary): + var f:File = File.new() + f.open(path, File.WRITE) + f.store_string(JSON.print(data, "\t")) + f.close() + +static func is_wrapped(t:String, head:String, tail:String) -> bool: + t = t.strip_edges() + return t.begins_with(head) and t.ends_with(tail) + +static func unwrap(t:String, head:String, tail:String, keep_white:bool=false) -> String: + var stripped = t.strip_edges() + stripped = stripped.substr(len(head), len(stripped)-len(head)-len(tail)) + if keep_white: + var whead = get_whitespace_head(t) + var wtail = get_whitespace_tail(t) + return whead + stripped + wtail + else: + return t.substr(len(head), len(t)-len(head)-len(tail)) + +static func wrap(t:String, head:String, tail:String, keep_white:bool=false) -> String: + if keep_white: + var whead = get_whitespace_head(t) + var wtail = get_whitespace_tail(t) + return whead + head + t.strip_edges() + tail + wtail + else: + return head + t + tail + +static func get_whitespace_head(t:String): + var length = len(t) - len(t.strip_edges(true, false)) + return t.substr(0, length) + +static func get_whitespace_tail(t:String): + var length = len(t) - len(t.strip_edges(false, true)) + return t.substr(len(t)-length) + +static func dig(d, obj:Object, fname:String): + var f = funcref(obj, fname) + if d is Dictionary: + _dig_dict(d, f) + elif d is Node: + _dig_node(d, f) + +static func _dig_dict(d:Dictionary, f:FuncRef): + f.call_func(d) + for k in d: + if d[k] is Dictionary: + _dig_dict(d[k], f) + +static func _dig_node(d:Node, f:FuncRef): + f.call_func(d) + for i in d.get_child_count(): + _dig_node(d.get_child(i), f) + +static func sort(d:Dictionary, reverse:bool=false) -> Dictionary: + return Dict.new(d).sort(reverse) + +static func sort_value(d:Dictionary, reverse:bool=false) -> Dictionary: + return Dict.new(d).sort_value(reverse) + +static func sort_on_ext(d:Dictionary, reverse:bool=false) -> Dictionary: + return Dict.new(d).sort_ext(reverse) + +static func split_many(s:String, spliton:String, allow_empty:bool=true) -> PoolStringArray: + var parts := PoolStringArray() + var start := 0 + var i := 0 + while i < len(s): + if s[i] in spliton: + if allow_empty or start < i: + parts.append(s.substr(start, i - start)) + start = i + 1 + i += 1 + if allow_empty or start < i: + parts.append(s.substr(start, i - start)) + return parts + +static func commas(number) -> String: + number = str(number) + var mod = len(number) % 3 + var out = "" + for i in len(number): + if i and i % 3 == mod: + out += "," + out += number[i] + return out + +class Dict: + var d:Dictionary + var a:Array = [] + var i:int = 0 + + func _init(dict:Dictionary): + d = dict + + func _pop(): + for k in d: a.append([k, d[k]]) + + func _unpop() -> Dictionary: + d.clear() + for i in a: d[i[0]] = i[1] + return d + + func sort(reverse:bool=false) -> Dictionary: + _pop() + a.sort_custom(self, "_sort_reverse" if reverse else "_sort") + return _unpop() + + func sort_value(reverse:bool=false) -> Dictionary: + _pop() + i = 1 + a.sort_custom(self, "_sort_reverse" if reverse else "_sort") + return _unpop() + + func sort_ext(reverse:bool=false) -> Dictionary: + for k in d: + if "." in k: + var p = k.split(".", true, 1) + p = p[1] + p[0] + a.append([k, d[k], p + "." + k]) + else: + a.append([k, d[k], "." + k]) + i = 2 + a.sort_custom(self, "_sort_reverse" if reverse else "_sort") + return _unpop() + + func _sort(a, b): return a[i] > b[i] + func _sort_reverse(a, b): return a[i] < b[i] diff --git a/addons/text_editor/TextEditor.gd b/addons/text_editor/TextEditor.gd new file mode 100644 index 0000000..df1b553 --- /dev/null +++ b/addons/text_editor/TextEditor.gd @@ -0,0 +1,442 @@ +extends Node +class_name TextEditor + +const FONT:DynamicFont = preload("res://addons/text_editor/fonts/font.tres") + +const FONT_R:DynamicFont = preload("res://addons/text_editor/fonts/font_r.tres") +const FONT_B:DynamicFont = preload("res://addons/text_editor/fonts/font_b.tres") +const FONT_I:DynamicFont = preload("res://addons/text_editor/fonts/font_i.tres") +const FONT_BI:DynamicFont = preload("res://addons/text_editor/fonts/font_bi.tres") + +const SHOW_EXT:PoolStringArray = PoolStringArray([ + ".txt", ".md", ".json", ".csv", ".ini", ".cfg", ".yaml" +]) +const FILE_FILTERS:PoolStringArray = PoolStringArray([ + "*.txt ; Text", + "*.md ; Markdown", + "*.json ; JSON", + "*.csv ; Comma Seperated Values", + "*.cfg ; Config", + "*.ini ; Config", + "*.yaml ; YAML" +]) + +var color_text:Color = Color.white +var color_comment:Color = Color.darkolivegreen +var color_symbol:Color = Color.white.darkened(.5) +var color_var:Color = Color.orange +var color_varname:Color = Color.white.darkened(.25) + + +onready var test_button:Node = $c/c/c/test +onready var tab_parent:TabContainer = $c/c3/c/c/tab_container +onready var tab_prefab:Node = $c/c3/c/c/tab_container/tab_prefab +onready var popup:ConfirmationDialog = $popup +onready var popup_unsaved:ConfirmationDialog = $popup_unsaved +onready var file_dialog:FileDialog = $file_dialog +onready var menu_file:MenuButton = $c/c/c/file_button +onready var line_edit:LineEdit = $c/c3/c/c/line_edit + +signal updated_file_list() +signal file_opened(file_path) +signal file_closed(file_path) +signal file_selected(file_path) +signal file_saved(file_path) +signal file_renamed(old_path, new_path) +#signal file_symbols_updated(file_path) +signal symbols_updated() +signal tags_updated() +signal save_files() + +var current_directory:String = "" +var dirs:Array = [] +var file_list:Dictionary = {} +var extensions:Dictionary = {} +var symbols:Dictionary = {} +var tags:Array = [] +var tags_enabled:Dictionary = {} +var tag_counts:Dictionary = {} + +var opened:Array = [] +var closed:Array = [] + +func _ready(): + var _e + _e = test_button.connect("pressed", self, "_debug_pressed") + + # popup unsaved + popup_unsaved.get_ok().text = "Ok" + popup_unsaved.get_cancel().text = "Cancel" + var btn = popup_unsaved.add_button("Save and Close", false, "save_and_close") + btn.modulate = Color.yellowgreen + btn.connect("pressed", popup_unsaved, "hide") + TE_Util.dig(popup_unsaved, self, "_apply_fonts") + + # menu + var p = menu_file.get_popup() + p.add_font_override("font", FONT_R) + p.add_item("New File") + _e = p.connect("index_pressed", self, "_menu_file") + + # file dialog + _e = file_dialog.connect("file_selected", self, "_file_dialog_file") + file_dialog.add_font_override("title_font", FONT_R) + TE_Util.dig(file_dialog, self, "_apply_fonts") + + # tab control + _e = tab_parent.connect("tab_changed", self, "_tab_changed") + tab_parent.remove_child(tab_prefab) + + # + tab_parent.add_font_override("font", FONT_R) + + set_directory() + +func _input(e): + if e is InputEventMouseButton and e.control: + if e.button_index == BUTTON_WHEEL_DOWN: + FONT.size = int(max(8, FONT.size - 1)) + get_tree().set_input_as_handled() + + elif e.button_index == BUTTON_WHEEL_UP: + FONT.size = int(min(64, FONT.size + 1)) + get_tree().set_input_as_handled() + +func _apply_fonts(n:Node): + if n is Control: + if n.has_font("font"): + n.add_font_override("font", FONT_R) + +func _menu_file(a): + match menu_file.get_popup().items[a]: + "New File": popup_create_file() + +func _file_dialog_file(file_path:String): + match file_dialog.get_meta("mode"): + "create": create_file(file_path) + +var tab_index:int = -1 +func _tab_changed(index:int): + tab_index = index + var node = tab_parent.get_child(index) + if node: + _selected_file_changed(get_selected_file()) + else: + _selected_file_changed("") + +var last_selected_file:String = "" +func _selected_file_changed(file_path:String): + if file_path != last_selected_file: + last_selected_file = file_path + emit_signal("file_selected", last_selected_file) + +func is_tag_enabled(tag:String) -> bool: + return tags_enabled[tag] + +func enable_tag(tag:String, enabled:bool=true): + tags_enabled[tag] = enabled + tags.clear() + for t in tags_enabled: + if tags_enabled[t]: + tags.append(t) + emit_signal("tags_updated") + +func is_tagged_or_visible(file_tags:Array) -> bool: + if not len(tags): + return true + for t in tags: + if not t in file_tags: + return false + return true + +func is_tagged(file_path:String) -> bool: + if not len(tags): + return true + var tab = get_tab(file_path) + if tab: + return is_tagged_or_visible(tab.tags.keys()) + return false + +func popup_create_file(dir:String="res://"): + file_dialog.set_meta("mode", "create") + file_dialog.current_dir = dir + file_dialog.current_path = "new_file.txt" + file_dialog.window_title = "Create File" + file_dialog.mode = FileDialog.MODE_SAVE_FILE + file_dialog.filters = FILE_FILTERS + file_dialog.show() + +func create_file(file_path:String): + var f:File = File.new() + if f.open(file_path, File.WRITE) == OK: + f.store_string("") + f.close() + refresh_files() + open_file(file_path) + select_file(file_path) + else: + push_error("couldnt create %s" % file_path) + +func _debug_pressed(): + set_directory() + +func _unhandled_key_input(e:InputEventKey): + if not e.pressed: + return + + if e.control: + # save + if e.scancode == KEY_S: + emit_signal("save_files") + + # close/unclose tab + elif e.scancode == KEY_W: + if e.shift: + print("open last tab") + open_last_file() + else: + print("close tab ") + close_selected() + + elif e.scancode == KEY_R: + sort_files() + + else: + return + + get_tree().set_input_as_handled() + +func sort_files(): + TE_Util.dig(file_list, self, "_sort_files") + emit_signal("updated_file_list") + +func _sort_files(d:Dictionary): + return TE_Util.sort_on_ext(d) + +func get_selected_file() -> String: + var node = get_selected_tab() + return node.file_path if node else "" + +func get_tab(file_path:String) -> TextEdit: + for child in tab_parent.get_children(): + if child.file_path == file_path: + return child + return null + +func get_selected_tab() -> TextEdit: + var i = tab_parent.current_tab + if i >= 0 and i < tab_parent.get_child_count(): + return tab_parent.get_child(i) as TextEdit + return null + +func get_temporary_tab() -> TextEdit: + for child in tab_parent.get_children(): + if child.temporary: + return child + return null + +func save_file(file_path:String, text:String): + var f:File = File.new() + var _err = f.open(file_path, File.WRITE) + f.store_string(text) + f.close() + emit_signal("file_saved", file_path) + +func open_last_file(): + if closed: + closed.pop_back() + +func close_selected(): + var file = get_selected_file() + if file: + close_file(file) + +func close_file(file_path:String): + var tab = get_tab(file_path) + if tab: + tab.close() + +func _close_file(file_path, remember:bool=true): + if remember: + closed.append(opened.pop_back()) + + var tab = get_tab(file_path) + tab_parent.remove_child(tab) + tab.queue_free() + emit_signal("file_closed", file_path) + + if opened: + select_file(opened[-1]) + +func open_file(file_path:String, temporary:bool=false): + var tab = get_tab(file_path) + if tab: + return tab + + else: + tab = tab_prefab.duplicate() + tab_parent.add_child(tab) + tab.set_owner(self) + tab.load_file(file_path) + if temporary: + tab.temporary = true + else: + opened.append(file_path) + emit_signal("file_opened", file_path) + return tab + +func is_opened(file_path:String) -> bool: + return get_tab(file_path) != null + +func is_selected(file_path:String) -> bool: + return get_selected_file() == file_path + +func recycle_file(file_path:String): + var old_base:String = file_path.substr(len("res://")).get_base_dir() + var p = file_path.get_file().split(".", true, 1) + var old_name:String = p[0] + var old_ext:String = p[1] + var tab = get_tab(file_path) + + var new_file = "%s_%s.%s" % [old_name, OS.get_system_time_secs(), old_ext] + var new_path:String = "res://.trash".plus_file(old_base).plus_file(new_file) + + # create directory + var new_dir = new_path.get_base_dir() + if Directory.new().make_dir_recursive(new_dir) != OK: + print("couldn't remove %s" % file_path) + return + + # save recovery information + var trash_info = TE_Util.load_json("res://.trash_info.json") + trash_info[new_path] = file_path + TE_Util.save_json("res://.trash_info.json", trash_info) + + # remove by renaming + rename_file(file_path, new_path) + print("Send to " + new_path) + + if tab: + tab_parent.remove_child(tab) + tab.queue_free() + + if opened: + select_file(opened[-1]) + + +func rename_file(old_path:String, new_path:String): + if old_path == new_path or not old_path or not new_path: + return + + if File.new().file_exists(new_path): + push_error("can't rename %s to %s. file already exists." % [old_path, new_path]) + return + + var selected = get_selected_file() + if Directory.new().rename(old_path, new_path) == OK: + refresh_files() + if selected == old_path: + _selected_file_changed(new_path) + emit_signal("file_renamed", old_path, new_path) + + else: + push_error("couldn't rename %s to %s." % [old_path, new_path]) + +func select_file(file_path:String): + var temp = get_temporary_tab() + if temp: + if temp.file_path == file_path: + temp.temporary = false + else: + temp.close() + + if not is_opened(file_path): + open_file(file_path, true) + + # select current tab + tab_parent.current_tab = get_tab(file_path).get_index() + _selected_file_changed(file_path) + +func set_directory(path:String="res://test_files"): + var gpath = ProjectSettings.globalize_path(path) + var dname = gpath.get_file() + OS.set_window_title("%s (%s)" % [dname, gpath]) + current_directory = path + file_dialog.current_dir = path + refresh_files() + +func _file_symbols_updated(file_path:String): + var tg = get_tab(file_path).tags + for tag in tg: + if not tag in tags_enabled: + tags_enabled[tag] = false + + tag_counts.clear() + for child in get_all_tabs(): + for t in child.tags: + if not t in tag_counts: + tag_counts[t] = child.tags[t] + else: + tag_counts[t] += child.tags[t] + + emit_signal("symbols_updated") + +func get_all_tabs() -> Array: + return tab_parent.get_children() + +func refresh_files(): + extensions.clear() + dirs.clear() + file_list.clear() + var dir = Directory.new() + if dir.open(current_directory) == OK: + _scan_dir("", current_directory, dir, file_list) + else: + push_error("error trying to load %s." % current_directory) + + sort_files() + +func _scan_dir(id:String, path:String, dir:Directory, list:Dictionary): + var _e = dir.list_dir_begin(true, false) + dirs.append(path) + var files = {} + list[id] = { file_path=path, files=files, open=true } + + var fname = dir.get_next() + + while fname: + var file_path = dir.get_current_dir().plus_file(fname) + + if dir.current_is_dir(): + # ignore folders with a .gdignore file. + if not fname == ".import" and not File.new().file_exists(file_path.plus_file(".gdignore")): + var sub_dir = Directory.new() + sub_dir.open(file_path) + _scan_dir(fname, file_path, sub_dir, files) + + else: + # ignore .import files + if not file_path.ends_with(".import"): + files[fname] = file_path + + var ext = get_extension(file_path) + if not ext in extensions: + extensions[ext] = 1 + else: + extensions[ext] += 1 + + fname = dir.get_next() + dir.list_dir_end() + +static func get_extension(file_path:String) -> String: + var file = file_path.get_file() + if "." in file: + return file.split(".", true, 1)[1] + return "" + +static func get_extension_helper(file_path:String) -> TE_ExtensionHelper: + var ext:String = get_extension(file_path).replace(".", "_") + var ext_path:String = "res://addons/text_editor/ext/ext_%s.gd" % ext + if File.new().file_exists(ext_path): + return load(ext_path).new() + return load("res://addons/text_editor/ext/TE_ExtensionHelper.gd").new() diff --git a/addons/text_editor/ext/TE_ExtensionHelper.gd b/addons/text_editor/ext/TE_ExtensionHelper.gd new file mode 100644 index 0000000..3fb0b52 --- /dev/null +++ b/addons/text_editor/ext/TE_ExtensionHelper.gd @@ -0,0 +1,67 @@ +extends Resource +class_name TE_ExtensionHelper + +var symbols:Dictionary = {} + +func generate_meta(t:TextEdit, r:TE_RichTextLabel): + var chars = TE_Util.commas(len(t.text)) + var words = TE_Util.commas(len(t.text.split(" ", false))) + var lines = TE_Util.commas(len(TE_Util.split_many(t.text, ".?!\n", false))) + r.add_constant_override("table_hseparation", int(r.rect_size.x / 4.0)) + r.set_bbcode(r.table([ + ["chars", "words", "lines"], + [chars, words, lines] + ])) + +func toggle_comment(t:TextEdit, head:String="", tail:String=""): + var wasnt_selected:bool = false + var cursor_l + var cursor_c + + if not t.is_selection_active(): + var l = t.cursor_get_line() + var lt = t.get_line(l) + wasnt_selected = lt.strip_edges() == "" + cursor_l = t.cursor_get_line() + cursor_c = t.cursor_get_column() + var s = len(lt) - len(lt.strip_edges(true, false)) + t.select(l, s, l, len(t.get_line(l))) + + var l1 = t.get_selection_from_line() + var c1 = t.get_selection_from_column() + var old = t.get_selection_text() + var new + + if TE_Util.is_wrapped(old, head, tail): + new = TE_Util.unwrap(old, head, tail) + else: + new = TE_Util.wrap(old, head, tail) + + t.insert_text_at_cursor(new) + + if wasnt_selected: + t.deselect() + t.cursor_set_line(cursor_l) + t.cursor_set_column(cursor_c+len(head)) + + else: + var l = new.split("\n") + var l2 = l1 + len(l)-1 + var c2 = c1 + len(l[-1]) + t.select(l1, c1, l2, c2) + + return [old, new] + +func add_symbol(line:int=-1, deep:int=0, name:String="") -> Dictionary: + var symbol = { deep=deep, name=name, tags=[] } + symbols[line] = symbol + return symbol + +func get_symbols(t:String) -> Dictionary: + symbols = {} + return symbols + +func apply_colors(e, t:TextEdit): + t.add_color_override("font_color", e.color_text) + t.add_color_override("number_color", e.color_var) + t.add_color_override("member_variable_color", e.color_var) diff --git a/addons/text_editor/ext/ext_csv.gd b/addons/text_editor/ext/ext_csv.gd new file mode 100644 index 0000000..4616724 --- /dev/null +++ b/addons/text_editor/ext/ext_csv.gd @@ -0,0 +1,2 @@ +extends TE_ExtensionHelper + diff --git a/addons/text_editor/ext/ext_ini.gd b/addons/text_editor/ext/ext_ini.gd new file mode 100644 index 0000000..7e75de3 --- /dev/null +++ b/addons/text_editor/ext/ext_ini.gd @@ -0,0 +1,34 @@ +extends TE_ExtensionHelper + +func apply_colors(e:TextEditor, t:TextEdit): + .apply_colors(e, t) + # symbols + t.add_color_region("[", "]", e.color_symbol, false) + + # string + t.add_color_region('"', '"', e.color_var, false) + + # comment + t.add_color_region(';', '', e.color_comment, true) + +func get_symbols(t:String) -> Dictionary: + var out = .get_symbols(t) + var last = add_symbol() + var lines = t.split("\n") + var i = 0 + + while i < len(lines): + # symbols + if lines[i].begins_with("["): + var name = lines[i].split("[", true, 1)[1].split("]", true, 1)[0] + last = add_symbol(i, 0, name) + + # tags + elif lines[i].begins_with(";") and "#" in lines[i]: + for t in lines[i].substr(1).split("#"): + if t: + last.tags.append(t) + + i += 1 + + return out diff --git a/addons/text_editor/ext/ext_json.gd b/addons/text_editor/ext/ext_json.gd new file mode 100644 index 0000000..ab97f89 --- /dev/null +++ b/addons/text_editor/ext/ext_json.gd @@ -0,0 +1,41 @@ +extends TE_ExtensionHelper + +func toggle_comment(t:TextEdit, head:String="/*", tail:String="*/"): + return .toggle_comment(t, head, tail) + +func get_symbols(t:String): + var out = .get_symbols(t) + var last = add_symbol() + var lines = t.split("\n") + var i = 0 + + while i < len(lines): + # symbols + if "\": {" in lines[i]: + var key = lines[i].split("\": {", true, 1)[0].rsplit("\"", true, 0)[1] + var deep = max(0, len(lines[i]) - len(lines[i].strip_edges(true, false)) - 1) + last = add_symbol(i, deep, key) + + # tags + elif "/* #" in lines[i]: + for tag in lines[i].split("/* #", true, 1)[1].split("*/", true, 1)[0].split("#"): + tag = tag.strip_edges() + if tag: + last.tags.append(tag) + + i += 1 + + return out + +func apply_colors(e:TextEditor, t:TextEdit): + .apply_colors(e, t) + + # vars + t.add_color_region(' "', '"', e.color_var) + t.add_color_region('"', '"', e.color_varname) + t.add_keyword_color("true", e.color_var) + t.add_keyword_color("false", e.color_var) + + # comments + t.add_color_region("/*", "*/", e.color_comment) + t.add_color_region("//", "", e.color_comment, true) diff --git a/addons/text_editor/ext/ext_md.gd b/addons/text_editor/ext/ext_md.gd new file mode 100644 index 0000000..2db9a5e --- /dev/null +++ b/addons/text_editor/ext/ext_md.gd @@ -0,0 +1,84 @@ +extends TE_ExtensionHelper + +func toggle_comment(t:TextEdit, head:String=""): + return .toggle_comment(t, head, tail) + +func apply_colors(e:TextEditor, t:TextEdit): + .apply_colors(e, t) + var code:Color = Color.aquamarine.darkened(.5) + + t.add_keyword_color("true", e.color_var) + t.add_keyword_color("false", e.color_var) + + # bold italic + t.add_color_region("***", "***", Color.tomato.lightened(.3), false) + # bold + t.add_color_region("**", "**", Color.tomato, false) + # italic + t.add_color_region("*", "*", Color.tomato.darkened(.3), false) + + # quote + t.add_color_region("> ", "", Color.white.darkened(.6), true) + + # comment + t.add_color_region("", e.color_comment, false) + + # headings + var head = e.color_symbol + t.add_color_region("# *", "*", Color.yellowgreen, true) + t.add_color_region("# \"", "\"", Color.yellowgreen, true) + t.add_color_region("# ", "", head, true) + t.add_color_region("## ", "", head, true) + t.add_color_region("### ", "", head, true) + t.add_color_region("#### ", "", head, true) + t.add_color_region("##### ", "", head, true) + t.add_color_region("###### ", "", head, true) + + # url links + t.add_color_region("[", ")", Color.purple) + + # lists + t.add_color_region("- [x", "]", Color.yellowgreen, false) + t.add_color_region("- [", " ]", Color.white.darkened(.6), false) + + + # code blocks + t.add_color_region("```", "```", code, false) + t.add_color_region("~~~", "~~~", code, false) + # strikeout + t.add_color_region("~~", "~~", Color.tomato, false) + # code + t.add_color_region("`", "`", code, false) + # at/mention + t.add_color_region("@", " ", Color.yellowgreen, false) + + t.add_color_region(": ", "", Color.white.lightened(.4), true) + + # tables + t.add_color_region("|", "", Color.tan, true) + + +func get_symbols(t:String) -> Dictionary: + var out = .get_symbols(t) + var last = add_symbol() + var lines = t.split("\n") + var i = 0 + + while i < len(lines): + # symbols + if lines[i].begins_with("#"): + var p = lines[i].split(" ", true, 1) + var deep = len(p[0])-1 + var name = p[1].strip_edges() + last = add_symbol(i, deep, name) + + # tags + elif "", true, 1)[0].split("#"): + tag = tag.strip_edges() + if tag: + last.tags.append(tag) + + i += 1 + + return out diff --git a/addons/text_editor/ext/ext_yaml.gd b/addons/text_editor/ext/ext_yaml.gd new file mode 100644 index 0000000..1360e2d --- /dev/null +++ b/addons/text_editor/ext/ext_yaml.gd @@ -0,0 +1,91 @@ +extends TE_ExtensionHelper + +func _is_commented(lines) -> bool: + for i in len(lines): + if not lines[i].strip_edges(): + continue + if not lines[i].strip_edges(true, false).begins_with("# "): + return false + return true + +func toggle_comment(t:TextEdit, head:String="", tail:String=""): + if not t.is_selection_active(): + var l = t.cursor_get_line() + var lt = t.get_line(l) + var s = len(lt) - len(lt.strip_edges(true, false)) + t.select(l, s, l, len(t.get_line(l))) + + var l1 = t.get_selection_from_line() + var c1 = t.get_selection_from_column() + var old = t.get_selection_text() + var new = old.split("\n") + + if _is_commented(new): + for i in len(new): + if "# " in new[i]: + var p = new[i].split("# ", true, 1) + new[i] = p[0] + p[1] + else: + for i in len(new): + if not new[i].strip_edges(): + continue + var space = TE_Util.get_whitespace_head(new[i]) + new[i] = space + "# " + new[i].strip_edges(true, false) + + new = new.join("\n") + + t.insert_text_at_cursor(new) + var l = new.split("\n") + var l2 = l1 + len(l)-1 + var c2 = c1 + len(l[-1]) + t.select(l1, c1, l2, c2) + + return [old, new] + +func apply_colors(e:TextEditor, t:TextEdit): + .apply_colors(e, t) + # strings + t.add_color_region('"', '"', e.color_var) + # bools + t.add_keyword_color("true", e.color_var) + t.add_keyword_color("false", e.color_var) + + # array element + t.add_color_region("- ", "", Color.webgray, true) + + # comments + t.add_color_region("#", "", e.color_comment, true) + + +func get_symbols(t:String) -> Dictionary: + var out = .get_symbols(t) + var last = add_symbol() + var lines = t.split("\n") + var i = 0 + + while i < len(lines): + # find objects to use as symbols + if ":" in lines[i]: + var p = lines[i].split(":", true, 1) + var r = p[1].strip_edges() + if not r or r.begins_with("{") or r.begins_with("#"): + var name = p[0].strip_edges() + var deep = max(0, len(lines[i]) - len(lines[i].strip_edges(true, false))) + last = add_symbol(i, deep, name) + + # find tags inside comments + if "# " in lines[i]: + var p = lines[i].split("# ", true, 1) + if p[0].count("\"") % 2 != 0: + pass + + elif "#" in p[1]: + for tag in p[1].split("#", true, 1)[1].split("#"): + tag = tag.strip_edges() + if tag: + last.tags.append(tag) + + + i += 1 + + return out diff --git a/addons/text_editor/file_buttons.gd b/addons/text_editor/file_buttons.gd new file mode 100644 index 0000000..1eccaec --- /dev/null +++ b/addons/text_editor/file_buttons.gd @@ -0,0 +1,16 @@ +extends Node + + +# Declare member variables here. Examples: +# var a = 2 +# var b = "text" + + +# Called when the node enters the scene tree for the first time. +func _ready(): + pass # Replace with function body. + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +#func _process(delta): +# pass diff --git a/addons/text_editor/fonts/RobotoMono-Bold.ttf b/addons/text_editor/fonts/RobotoMono-Bold.ttf new file mode 100644 index 0000000..900fce6 Binary files /dev/null and b/addons/text_editor/fonts/RobotoMono-Bold.ttf differ diff --git a/addons/text_editor/fonts/RobotoMono-BoldItalic.ttf b/addons/text_editor/fonts/RobotoMono-BoldItalic.ttf new file mode 100644 index 0000000..4bfe29a Binary files /dev/null and b/addons/text_editor/fonts/RobotoMono-BoldItalic.ttf differ diff --git a/addons/text_editor/fonts/RobotoMono-Italic.ttf b/addons/text_editor/fonts/RobotoMono-Italic.ttf new file mode 100644 index 0000000..4ee4dc4 Binary files /dev/null and b/addons/text_editor/fonts/RobotoMono-Italic.ttf differ diff --git a/addons/text_editor/fonts/RobotoMono-Regular.ttf b/addons/text_editor/fonts/RobotoMono-Regular.ttf new file mode 100644 index 0000000..7c4ce36 Binary files /dev/null and b/addons/text_editor/fonts/RobotoMono-Regular.ttf differ diff --git a/addons/text_editor/fonts/font.tres b/addons/text_editor/fonts/font.tres new file mode 100644 index 0000000..434cb0c --- /dev/null +++ b/addons/text_editor/fonts/font.tres @@ -0,0 +1,10 @@ +[gd_resource type="DynamicFont" load_steps=4 format=2] + +[ext_resource path="res://addons/text_editor/fonts/RobotoMono-Regular.ttf" type="DynamicFontData" id=1] +[ext_resource path="res://addons/text_editor/fonts/unifont_upper-13.0.01.ttf" type="DynamicFontData" id=2] +[ext_resource path="res://addons/text_editor/fonts/unifont-13.0.01.ttf" type="DynamicFontData" id=3] + +[resource] +font_data = ExtResource( 1 ) +fallback/0 = ExtResource( 3 ) +fallback/1 = ExtResource( 2 ) diff --git a/addons/text_editor/fonts/font_b.tres b/addons/text_editor/fonts/font_b.tres new file mode 100644 index 0000000..89666f3 --- /dev/null +++ b/addons/text_editor/fonts/font_b.tres @@ -0,0 +1,10 @@ +[gd_resource type="DynamicFont" load_steps=4 format=2] + +[ext_resource path="res://addons/text_editor/fonts/RobotoMono-Bold.ttf" type="DynamicFontData" id=1] +[ext_resource path="res://addons/text_editor/fonts/unifont_upper-13.0.01.ttf" type="DynamicFontData" id=2] +[ext_resource path="res://addons/text_editor/fonts/unifont-13.0.01.ttf" type="DynamicFontData" id=3] + +[resource] +font_data = ExtResource( 1 ) +fallback/0 = ExtResource( 3 ) +fallback/1 = ExtResource( 2 ) diff --git a/addons/text_editor/fonts/font_bi.tres b/addons/text_editor/fonts/font_bi.tres new file mode 100644 index 0000000..ca19d40 --- /dev/null +++ b/addons/text_editor/fonts/font_bi.tres @@ -0,0 +1,10 @@ +[gd_resource type="DynamicFont" load_steps=4 format=2] + +[ext_resource path="res://addons/text_editor/fonts/RobotoMono-BoldItalic.ttf" type="DynamicFontData" id=1] +[ext_resource path="res://addons/text_editor/fonts/unifont_upper-13.0.01.ttf" type="DynamicFontData" id=2] +[ext_resource path="res://addons/text_editor/fonts/unifont-13.0.01.ttf" type="DynamicFontData" id=3] + +[resource] +font_data = ExtResource( 1 ) +fallback/0 = ExtResource( 3 ) +fallback/1 = ExtResource( 2 ) diff --git a/addons/text_editor/fonts/font_i.tres b/addons/text_editor/fonts/font_i.tres new file mode 100644 index 0000000..314dea7 --- /dev/null +++ b/addons/text_editor/fonts/font_i.tres @@ -0,0 +1,10 @@ +[gd_resource type="DynamicFont" load_steps=4 format=2] + +[ext_resource path="res://addons/text_editor/fonts/RobotoMono-Italic.ttf" type="DynamicFontData" id=1] +[ext_resource path="res://addons/text_editor/fonts/unifont_upper-13.0.01.ttf" type="DynamicFontData" id=2] +[ext_resource path="res://addons/text_editor/fonts/unifont-13.0.01.ttf" type="DynamicFontData" id=3] + +[resource] +font_data = ExtResource( 1 ) +fallback/0 = ExtResource( 3 ) +fallback/1 = ExtResource( 2 ) diff --git a/addons/text_editor/fonts/font_r.tres b/addons/text_editor/fonts/font_r.tres new file mode 100644 index 0000000..434cb0c --- /dev/null +++ b/addons/text_editor/fonts/font_r.tres @@ -0,0 +1,10 @@ +[gd_resource type="DynamicFont" load_steps=4 format=2] + +[ext_resource path="res://addons/text_editor/fonts/RobotoMono-Regular.ttf" type="DynamicFontData" id=1] +[ext_resource path="res://addons/text_editor/fonts/unifont_upper-13.0.01.ttf" type="DynamicFontData" id=2] +[ext_resource path="res://addons/text_editor/fonts/unifont-13.0.01.ttf" type="DynamicFontData" id=3] + +[resource] +font_data = ExtResource( 1 ) +fallback/0 = ExtResource( 3 ) +fallback/1 = ExtResource( 2 ) diff --git a/addons/text_editor/fonts/unifont-13.0.01.ttf b/addons/text_editor/fonts/unifont-13.0.01.ttf new file mode 100644 index 0000000..d4f0376 Binary files /dev/null and b/addons/text_editor/fonts/unifont-13.0.01.ttf differ diff --git a/addons/text_editor/fonts/unifont_upper-13.0.01.ttf b/addons/text_editor/fonts/unifont_upper-13.0.01.ttf new file mode 100644 index 0000000..1c2797e Binary files /dev/null and b/addons/text_editor/fonts/unifont_upper-13.0.01.ttf differ diff --git a/addons/text_editor/line_edit.gd b/addons/text_editor/line_edit.gd new file mode 100644 index 0000000..01e28db --- /dev/null +++ b/addons/text_editor/line_edit.gd @@ -0,0 +1,32 @@ +extends LineEdit + +onready var editor:TextEditor = owner +var fr:FuncRef + +func _ready(): + var _e + _e = connect("text_entered", self, "_enter") + _e = connect("focus_exited", self, "_lost_focus") + + add_font_override("font", TextEditor.FONT_R) + +func _unhandled_key_input(e): + if e.scancode == KEY_ESCAPE and e.pressed: + fr = null + hide() + get_tree().set_input_as_handled() + +func display(t:String, obj:Object, fname:String): + text = t + fr = funcref(obj, fname) + show() + call_deferred("grab_focus") + +func _lost_focus(): + print("lost focus") + fr = null + hide() + +func _enter(t:String): + fr.call_func(t) + hide() diff --git a/addons/text_editor/list_files.gd b/addons/text_editor/list_files.gd new file mode 100644 index 0000000..b24856e --- /dev/null +++ b/addons/text_editor/list_files.gd @@ -0,0 +1,267 @@ +extends RichTextLabel + +onready var editor:TextEditor = owner +onready var popup:PopupMenu = $popup +onready var drag_label:RichTextLabel = $drag_label + +var files:Array = [] +var dirs:Array = [] +var selected +var hovered:String = "" +var dragging:String = "" +var drag_start:Vector2 + +func _ready(): + var _e + _e = editor.connect("updated_file_list", self, "_redraw") + _e = editor.connect("tags_updated", self, "_redraw") + _e = editor.connect("file_opened", self, "_file_opened") + _e = editor.connect("file_closed", self, "_file_closed") + _e = editor.connect("file_selected", self, "_file_selected") + _e = editor.connect("file_renamed", self, "_file_renamed") +# _e = connect("meta_clicked", self, "_clicked") + _e = connect("meta_hover_started", self, "_meta_entered") + _e = connect("meta_hover_ended", self, "_meta_exited") + + # popup + popup.add_item("Rename") + popup.add_separator() + popup.add_item("Remove") + _e = popup.connect("index_pressed", self, "_popup") + popup.add_font_override("font", TextEditor.FONT) + + for n in [self, drag_label]: + n.add_font_override("normal_font", editor.FONT_R) + n.add_font_override("bold_font", editor.FONT_B) + n.add_font_override("italics_font", editor.FONT_I) + n.add_font_override("bold_italics_font", editor.FONT_BI) + + set_process(false) + +func _popup(index:int): + var p = selected.split(":", true, 1) + var type = p[0] + var file = files[int(p[1])] if type == "f" else dirs[int(p[1])] if type == "d" else "" + + match popup.get_item_text(index): + "Rename": + var fname:String = selected.file_path.get_file() + var i:int = fname.find(".") + editor.line_edit.display(fname, self, "_renamed") + editor.line_edit.select(0, i) + + "Remove": + if type == "f": + editor.recycle_file(file) + + _: + selected = {} + +func _renamed(new_file:String): + var old_path:String = selected.file_path + var old_file:String = old_path.get_file() + if new_file != old_file: + var new_path:String = old_path.get_base_dir().plus_file(new_file) + editor.rename_file(old_path, new_path) + selected = {} + +func _process(_delta): + var mp = get_global_mouse_position() + if mp.distance_to(drag_start) > 16: + drag_label.visible = true + drag_label.set_global_position(mp) + +func end_drag(): + dragging = "" + drag_label.visible = false + set_process(false) + +func _input(e:InputEvent): + if e is InputEventMouseButton and hovered: + var m = hovered.split(":", true, 1) + var type = m[0] + var index = int(m[1]) + + if e.button_index == BUTTON_LEFT: + if e.pressed: + dragging = hovered + + if type == "f": + drag_label.set_bbcode(files[index].get_file()) +# drag_label.visible = true + drag_start = get_global_mouse_position() + set_process(true) + + else: + if dragging and dragging != hovered: + m = dragging.split(":", true, 1) + var drag_type = m[0] + var drag_index = int(m[1]) + if drag_type == "f" and type == "d": + var dir:String = dirs[index].file_path + var old_path:String = files[drag_index] + var new_path:String = dir.plus_file(old_path.get_file()) + editor.rename_file(old_path, new_path) + + else: + if type == "d": + dirs[index].open = not dirs[index].open + _redraw() + + elif type == "f": + editor.select_file(files[index]) + + end_drag() + get_tree().set_input_as_handled() + return + + elif e.button_index == BUTTON_RIGHT: + if e.pressed: + selected = hovered + popup.set_global_position(get_global_mouse_position()) + popup.popup() + get_tree().set_input_as_handled() + return + + if e is InputEventMouseButton: + if dragging and (e.button_index == BUTTON_LEFT and not e.pressed) or (e.button_index == BUTTON_RIGHT): + end_drag() + get_tree().set_input_as_handled() + return + + +func _meta_entered(m): hovered = m + +func _meta_exited(_m): hovered = "" + +func _file_opened(_file_path:String): _redraw() +func _file_closed(_file_path:String): _redraw() +func _file_selected(_file_path:String): _redraw() +func _file_renamed(_op:String, _np:String): _redraw() + +#func updated_file_list(): +# items.clear() +# _updated_file_list(editor.file_list, 0) +# redraw() + +var lines:PoolStringArray = PoolStringArray() + +func _redraw(): + lines = PoolStringArray() + lines.append("[url=add_file:0][color=#%s]+[/color][/url]" % [Color.green.to_html()]) + dirs.clear() + _draw_dir(editor.file_list[""], 0) + set_bbcode(lines.join("\n")) + +func clr(s:String, c:Color) -> String: return "[color=#%s]%s[/color]" % [c.to_html(), s] +func i(s:String) -> String: return "[i]%s[/i]" % s +func b(s:String) -> String: return "[b]%s[/b]" % s +func url(s:String, url:String) -> String: return "[url=%s]%s[/url]" % [url, s] + +const FOLDER:String = "🗀" # not visible in godot +func _draw_dir(dir:Dictionary, deep:int): + var space = clr("┃ ".repeat(deep), Color.white.darkened(.8)) + var file:String = dir.file_path + var name:String = b(file.get_file()) + var head:String = "▼" if dir.open else "▶" + var link:String = url("%s%s%s%s" % [space, FOLDER, head, name], "d:%s" % len(dirs)) + lines.append(clr(link, Color.white.darkened(.7))) + dirs.append(dir) +# var add = "[url=add_file:%s][color=#%s]+[/color][/url]" % [dindex, Color.green.to_html()] +# name = "[color=#%s]%s[/color] %s" % [Color.darkslategray.to_html(), item.name, add] + + var sel = editor.get_selected_tab() + sel = sel.file_path if sel else "" + + if dir.open: + var i = 0 + var last = len(dir.files)-1 + for path in dir.files: + var file_path = dir.files[path] + # dir + if file_path is Dictionary: + _draw_dir(file_path, deep+1) + + # file + else: + file = path.get_file() + var is_selected = file_path == sel + head = "┣╸" if i != last else "┗╸" + if is_selected: + head = clr(head, Color.white.darkened(.5)) + else: + head = clr(head, Color.white.darkened(.8)) + var p = file.split(".", true, 1) + file = p[0] + + var color = Color.white if editor.is_tagged(file_path) else Color.white.darkened(.5) + + if editor.is_selected(file_path): + file = clr(file, color) + elif editor.is_opened(file_path): + file = clr(file, color.darkened(.5)) + else: + file = i(clr(file, color.darkened(.75))) + + var ext = clr(p[1], Color.white.darkened(.6)) + var line = space + head + file + "." + ext + lines.append(url(line, "f:%s" % len(files))) + files.append(file_path) + i += 1 + + + + # file +# else: +# var p = item.name.split(".", true, 1) +# name = p[0] +# var ext = p[1] +# var clr = Color.white +# var dim = .75 +# +# if editor.is_selected(item.file_path): +# dim = 0.0 +# +# elif editor.is_open(item.file_path): +# dim = .5 +# +# name = "[color=#%s]%s[/color]" % [clr.darkened(dim).to_html(), name] +# name += "[color=#%s].%s[/color]" % [clr.darkened(.75).to_html(), ext] +# +# var tab = editor.get_tab(item.file_path) +# if tab and editor.is_tagged_or_visible(tab.tags.keys()): +# name = "[b]%s[/b]" % name +# +# var space = "" +# if item.deep: +# wide += item.deep * 2 +# +# if item.deep > 1: +# space += "┃ " + " ".repeat(int(max(0, item.deep-2))) +# else: +# space += " ".repeat(int(max(0, item.deep-1))) +# +# if item.last: +# space += "┗╸" +# else: +# space += "┣╸" +# +# space = "[color=#%s]%s[/color]" % [Color.darkslategray.to_html(), space] +# +# # add extra space to make clicking easier +# var extra = max(0, 16 - wide) +# name += " ".repeat(extra) +# text.append("%s[url=f:%s]%s[/url]" % [space, i, name]) +# +# set_bbcode(text.join("\n")) + +#func _updated_file_list(data:Dictionary, deep:int): +# var total = len(data) +# var i = 0 +# for k in data: +# if data[k] is Dictionary: +# items.append({ last=i==total-1, type="D", name=k, file_path=data[k].file_path, deep=deep, open=true }) +# _updated_file_list(data[k].files, deep+1) +# else: +# items.append({ last=i==total-1, type="F", name=k, file_path=data[k], deep=deep }) +# i += 1 diff --git a/addons/text_editor/list_symbols.gd b/addons/text_editor/list_symbols.gd new file mode 100644 index 0000000..3d4919c --- /dev/null +++ b/addons/text_editor/list_symbols.gd @@ -0,0 +1,49 @@ +extends RichTextLabel + +onready var editor:TextEditor = owner + +func _ready(): + var _e + _e = connect("meta_hover_started", self, "_hovered") + _e = connect("meta_clicked", self, "_clicked") + _e = editor.connect("symbols_updated", self, "_redraw") + _e = editor.connect("tags_updated", self, "_redraw") + + add_font_override("normal_font", editor.FONT_R) + add_font_override("bold_font", editor.FONT_B) + add_font_override("italics_font", editor.FONT_I) + add_font_override("bold_italics_font", editor.FONT_BI) + +func _hovered(_id): + pass + +func _clicked(id): + var p = id.split(":", true, 1) + var i = int(p[1]) + match p[0]: + "l": + var te:TextEdit = editor.get_selected_tab() + te.cursor_set_line(te.get_line_count()) # force scroll to bottom so selected line will be at top + te.cursor_set_line(i) + +func _redraw(): + var tab = editor.get_selected_tab() + var symbols = {} if not tab else tab.symbols + + # no symbols + if not symbols or len(symbols) == 1: + set_bbcode("[color=#%s][i][center]*No symbols*" % [Color.webgray.to_html()]) + + else: + var t = PoolStringArray() + + for line_index in symbols: + if line_index == -1: + continue # special file chapter + var symbol_info = symbols[line_index] + var space = "" if not symbol_info.deep else " ".repeat(symbol_info.deep) + var tagged = editor.is_tagged_or_visible(symbol_info.tags) + var clr = Color.white.darkened(0.0 if tagged else 0.75).to_html() + t.append(space + "[color=#%s][url=l:%s]%s[/url][/color]" % [clr, line_index, symbol_info.name]) + + set_bbcode(t.join("\n")) diff --git a/addons/text_editor/list_tags.gd b/addons/text_editor/list_tags.gd new file mode 100644 index 0000000..89def28 --- /dev/null +++ b/addons/text_editor/list_tags.gd @@ -0,0 +1,67 @@ +extends RichTextLabel + +onready var editor:TextEditor = owner + +var tag_indices:Array = [] # safer to use int in [url=] than str. + +func _ready(): + var _e + _e = connect("meta_hover_started", self, "_hovered") + _e = connect("meta_clicked", self, "_clicked") + _e = editor.connect("symbols_updated", self, "_redraw") + _e = editor.connect("tags_updated", self, "_redraw") + + add_font_override("normal_font", editor.FONT_R) + add_font_override("bold_font", editor.FONT_B) + add_font_override("italics_font", editor.FONT_I) + add_font_override("bold_italics_font", editor.FONT_BI) + +func _hovered(_id): + pass + +func _clicked(id): + var tag = tag_indices[int(id)] + editor.enable_tag(tag, not editor.is_tag_enabled(tag)) + +func _redraw(): + var tab = editor.get_selected_tab() + var tags = editor.tag_counts + var tab_tags = {} if not tab else tab.tags + + TE_Util.sort_value(tags) + + if not tags: + set_bbcode("[color=#%s][i][center]*No tags*" % [Color.webgray.to_html()]) + + else: + var t:PoolStringArray = PoolStringArray() + var count_color1 = Color.tomato.to_html() + var count_color2 = Color.tomato.darkened(.75).to_html() + for tag in tags: + var count = editor.tag_counts[tag] + var enabled = editor.is_tag_enabled(tag) + + var x + if count > 1: + x = "[color=#%s][i]%s[/i][/color]%s" % [count_color1 if enabled else count_color2, count, tag] + else: + x = tag + + var color = Color.white + var dim = 0.75 + + if tag in tab_tags: + color = Color.greenyellow + x = "[b]%s[/b]" % x + dim = 0.6 + + if enabled: + x = x + else: + x = "[color=#%s]%s[/color]" % [color.darkened(dim).to_html(), x] + + x = "[url=%s]%s[/url]" % [len(tag_indices), x] + t.append(x) + tag_indices.append(tag) + + set_bbcode(t.join(" ")) diff --git a/addons/text_editor/meta_panel.gd b/addons/text_editor/meta_panel.gd new file mode 100644 index 0000000..0b5f312 --- /dev/null +++ b/addons/text_editor/meta_panel.gd @@ -0,0 +1,23 @@ +extends TE_RichTextLabel + +onready var editor:TextEditor = owner + +func _ready(): + var _e + _e = editor.connect("file_selected", self, "_file_selected") + _e = editor.connect("file_saved", self, "_file_saved") + +func _unhandled_key_input(e): + if e.scancode == KEY_M and e.pressed: + visible = not visible + +func _file_selected(_file_path:String): + yield(get_tree(), "idle_frame") + _redraw() + +func _file_saved(_file_path:String): + _redraw() + +func _redraw(): + var tab = editor.get_selected_tab() + tab.helper.generate_meta(tab, self) diff --git a/addons/text_editor/tab_scroll.gd b/addons/text_editor/tab_scroll.gd new file mode 100644 index 0000000..8f34840 --- /dev/null +++ b/addons/text_editor/tab_scroll.gd @@ -0,0 +1,30 @@ +extends TabContainer + +var mouse:bool = false + +func _ready(): + var _e + _e = connect("mouse_entered", self, "set", ["mouse", true]) + _e = connect("mouse_exited", self, "set", ["mouse", false]) + +func _input(e): + if mouse and e is InputEventMouseButton and e.pressed: + if e.button_index == BUTTON_WHEEL_DOWN: + prev() + get_tree().set_input_as_handled() + + elif e.button_index == BUTTON_WHEEL_UP: + next() + get_tree().set_input_as_handled() + + if e is InputEventKey and e.pressed and e.control and e.scancode == KEY_TAB: + if e.shift: + prev() + get_tree().set_input_as_handled() + else: + next() + get_tree().set_input_as_handled() + +func prev(): current_tab = wrapi(current_tab - 1, 0, get_child_count()) +func next(): current_tab = wrapi(current_tab + 1, 0, get_child_count()) + diff --git a/addons/text_editor/text_edit.gd b/addons/text_editor/text_edit.gd new file mode 100644 index 0000000..a42a0ed --- /dev/null +++ b/addons/text_editor/text_edit.gd @@ -0,0 +1,191 @@ +extends TextEdit + +onready var tabs:TabContainer = get_parent() +onready var editor:TextEditor = get_parent().owner + +var helper:TE_ExtensionHelper +var temporary:bool = false setget set_temporary +var modified:bool = false +var file_path:String = "" + +var symbols:Dictionary = {} +var tags:Dictionary = {} +var last_key:int +var last_shift:bool +var last_selected:bool +var last_selection:Array = [0, 0, 0, 0] + +func _ready(): + var _e + _e = editor.connect("save_files", self, "save_file") + _e = editor.connect("file_selected", self, "_file_selected") + _e = editor.connect("file_renamed", self, "_file_renamed") + _e = connect("text_changed", self, "text_changed") + add_font_override("font", editor.FONT) + get_menu().add_font_override("font", editor.FONT) + +func _file_renamed(old_path:String, new_path:String): + if old_path == file_path: + file_path = new_path + update_name() + +func _input(e): + if not visible: + return + + if e is InputEventKey and e.pressed: + last_key = e.scancode + last_shift = e.shift + if is_selection_active(): + last_selected = true + last_selection[0] = get_selection_from_line() + last_selection[1] = get_selection_from_column() + last_selection[2] = get_selection_to_line() + last_selection[3] = get_selection_to_column() + else: + last_selected = false + + if e is InputEventKey and e.control and e.shift and e.pressed: + var f + var t + if is_selection_active(): + f = get_selection_from_line() + t = get_selection_to_line() + else: + f = cursor_get_line() + t = cursor_get_line() + + # move selected text up or down + if e.scancode == KEY_UP and f > 0: + var lines = [] + for i in range(f-1, t+1): lines.append(get_line(i)) + lines.push_back(lines.pop_front()) + for i in len(lines): set_line(f-1+i, lines[i]) + select(f-1, 0, t-1, len(get_line(t-1))) + cursor_set_line(cursor_get_line()-1, false) + + if e.scancode == KEY_DOWN and t < get_line_count()-1: + var lines = [] + for i in range(f, t+2): lines.append(get_line(i)) + lines.push_front(lines.pop_back()) + for i in len(lines): set_line(f+i, lines[i]) + select(f+1, 0, t+1, len(get_line(t+1))) + cursor_set_line(cursor_get_line()+1, false) + + +func _unhandled_key_input(e): + if not visible: + return + + # comment code + if e.scancode == KEY_SLASH and e.control and e.pressed: + helper.toggle_comment(self) + get_tree().set_input_as_handled() + + +func _file_selected(p:String): + if p and p == file_path: + grab_focus() + update_symbols() + +func text_changed(): + if last_selected: + match last_key: + KEY_APOSTROPHE: + undo() + select(last_selection[0], last_selection[1], last_selection[2], last_selection[3]) + if last_shift: + insert_text_at_cursor("\"%s\"" % get_selection_text()) + else: + insert_text_at_cursor("'%s'" % get_selection_text()) + + KEY_QUOTELEFT: + undo() + select(last_selection[0], last_selection[1], last_selection[2], last_selection[3]) + insert_text_at_cursor("`%s`" % get_selection_text()) + + _: + print(last_key) + + if not modified: + if temporary: + temporary = false + modified = true + update_name() + +func set_temporary(t): + temporary = t + update_name() + +func update_symbols(): + symbols.clear() + tags.clear() + + # symbol getter + symbols = helper.get_symbols(text) + + # collect tags + for line_index in symbols: + var line_info = symbols[line_index] + for tag in line_info.tags: + if not tag in tags: + tags[tag] = 1 + else: + tags[tag] += 1 + + var _e = TE_Util.sort(tags, true) + editor._file_symbols_updated(file_path) + +func close(): + if modified: + var _e + _e = editor.popup_unsaved.connect("confirmed", self, "_popup", ["close"], CONNECT_ONESHOT) + _e = editor.popup_unsaved.connect("custom_action", self, "_popup", [], CONNECT_ONESHOT) + editor.popup_unsaved.show() + else: + editor._close_file(file_path) + +func _popup(msg): + match msg: + "close": + editor._close_file(file_path) + "save_and_close": + save_file() + editor._close_file(file_path) + +func load_file(path:String): + file_path = path + var f:File = File.new() + var _err = f.open(path, File.READ) + text = f.get_as_text() + f.close() + update_name() + + # update colors + clear_colors() + + helper = TextEditor.get_extension_helper(file_path) + helper.apply_colors(editor, self) + print("helper ", helper) + +func save_file(): + if modified: + if not file_path.begins_with("res://"): + push_error("can't save to %s" % file_path) + return + + modified = false + editor.save_file(file_path, text) + update_name() + update_symbols() + +func update_name(): + var n = file_path.get_file().split(".", true, 1)[0] + if temporary: n = "?" + n + if modified: n = "*" + n + + tabs.set_tab_title(get_index(), n) + +func needs_save() -> bool: + return modified or not File.new().file_exists(file_path) + diff --git a/default_env.tres b/default_env.tres new file mode 100644 index 0000000..20207a4 --- /dev/null +++ b/default_env.tres @@ -0,0 +1,7 @@ +[gd_resource type="Environment" load_steps=2 format=2] + +[sub_resource type="ProceduralSky" id=1] + +[resource] +background_mode = 2 +background_sky = SubResource( 1 ) diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..c98fbb6 Binary files /dev/null and b/icon.png differ diff --git a/icon.png.import b/icon.png.import new file mode 100644 index 0000000..a4c02e6 --- /dev/null +++ b/icon.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.png" +dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..98ce9ed --- /dev/null +++ b/project.godot @@ -0,0 +1,54 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=4 + +_global_script_classes=[ { +"base": "Resource", +"class": "TE_ExtensionHelper", +"language": "GDScript", +"path": "res://addons/text_editor/ext/TE_ExtensionHelper.gd" +}, { +"base": "RichTextLabel", +"class": "TE_RichTextLabel", +"language": "GDScript", +"path": "res://addons/text_editor/TE_RichTextLabel.gd" +}, { +"base": "Reference", +"class": "TE_Util", +"language": "GDScript", +"path": "res://addons/text_editor/TE_Util.gd" +}, { +"base": "Node", +"class": "TextEditor", +"language": "GDScript", +"path": "res://addons/text_editor/TextEditor.gd" +} ] +_global_script_class_icons={ +"TE_ExtensionHelper": "", +"TE_RichTextLabel": "", +"TE_Util": "", +"TextEditor": "" +} + +[application] + +config/name="TextEdit" +run/main_scene="res://TextEditor.tscn" +config/icon="res://icon.png" + +[physics] + +common/enable_pause_aware_picking=true + +[rendering] + +quality/driver/driver_name="GLES2" +vram_compression/import_etc=true +vram_compression/import_etc2=false +environment/default_environment="res://default_env.tres"