Text Editor with Godot 4
It is finally here: Godot 4.0 just released, and I want to celebrate that by diving back into this excellent game engine and making a text file editor with it.
Bear in mind that I won’t go over everything this project includes. I will only go over the scene tree and the Code. If you want, head to my articles repository and download the project to see for yourself.
Scene Tree for the Text Editor
Let’s start with the scene tree! As you see, it consists of control nodes. We use a
ColorRect to change the background color and a
VBoxContainer to place the menu at the top and the text area below. For the menu, we use an
HBoxContainer with one menu button that can spawn a popup menu.
For the Text edit, we need to go into its
Layout/Container Sizing Properties to change expand to true. The expand property is a bit like
flex-grow: 1 in CSS, where this element will try to take up as much space as possible.
GDScript for the Text Editor
Now let’s get to the Hard Part: the Code.
Luckily we only need one script file in the root element of the scene. So all the Code you see below is in this single file.
As with any script, we need to say which node it extends. After that, I like to get a reference to all the nodes this script edits. Because the nodes aren’t available at this stage, we need to add the
extends Control @onready var menu_button := $vbox/menu/file_menu_button @onready var text_edit := $vbox/text_edit
Also, mind the
:=, which means we set the type of these variables to whatever is on the right. In this case, it means the variables will store Nodes.
Next, we define two variables that will change as the program continues.
var current_dialog : FileDialog var current_path := ''
Next up, we define a bunch of constant variables. The first one holds our menu buttons, a list of lists where the first item is the name, the second is the function name to call, and the last key can also trigger this button.
const MENU_ITEMS := [ ['Save', 'save_file', KEY_S], ['Save as', 'menu_save_file', KEY_Y], ['Open', 'menu_open_file', KEY_O], ]
Then we specify some File Filters because we don’t want the user to be overwhelmed with options that don’t work.
And we specify what kind of access the user has. For now, this is the User Data Folder, which is a special folder where you can store save files for your games. You can find its location by clicking on
Open User Data Folder.
# Dialog Options const FILE_FILTERS = [ ['*.md, *.txt, *.py', 'Text Files'], ['*.*', 'All'], ] const FILE_ACCESS = FileDialog.ACCESS_USERDATA
Then we also set the Application name and define string patterns for when the current file is saved and unsaved. We can later insert our Info into these strings via string formatting.
const APPLICATION_NAME = 'Maxim`s File Editor' const WINDOW_TITLE = '%s - %s' const WINDOW_TITLE_EDITED = '%s - [Unsaved Changes] %s' const UNSAVED_FILE_NAME = 'new.txt'
Then in the ready function, set the window title with
func _ready(): current_path = UNSAVED_FILE_NAME get_window().title = WINDOW_TITLE % [APPLICATION_NAME, current_path]
After that, we create the menu items in a loop. We also add a shortcut to each one to call it quickly. We also connect the click event of the popup to a function called
var i = 0 for menu_item in MENU_ITEMS: var popup : PopupMenu = menu_button.get_popup() popup.add_item(menu_item, i) var new_input = InputEventKey.new() new_input.keycode = menu_item new_input.ctrl_pressed = true var shortcut = Shortcut.new() shortcut.events = Array([new_input]) popup.add_shortcut(shortcut, i) i += 1 menu_button.get_popup().connect("id_pressed", on_file_menu_clicked)
Lastly, we connect the
sized_changed signal and
text_changed to some functions we later review.
get_viewport().connect("size_changed", on_viewport_resize) text_edit.connect('text_changed', on_text_change)
File Menu Actions
Let’s go over the three different actions called via the menu at the top left and how the system behind it works.
In the ready function, we connected this function below to the user clicks any of the menu buttons, and it will simply call the function specified in the array at the top.
func on_file_menu_clicked(id): self.call(MENU_ITEMS[id])
The functions that are subsequently called do almost the same thing. They use the
create_default_dialog function to create a dialog, set its file mode, and connect its
file_selected signal to their respective functions.
func menu_open_file(): var dialog = create_default_dialog() dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE dialog.connect('file_selected', open_file_selected) ... func menu_save_file(): var dialog = create_default_dialog() dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE dialog.connect('file_selected', save_file)
Now when opening a file, we use the
FileAcces Singleton to get the content of the given path and insert it into the text_edit. We also set the new window title and current_path variable.
func open_file_selected(path): var file = FileAccess.open(path, FileAccess.READ) text_edit.text = file.get_as_text() get_window().title = WINDOW_TITLE % [APPLICATION_NAME, path] current_path = path
And when we save a file, we first check if the path is equal to the unsaved file name, which means this is a new file. So we call the
menu_save_file function and return.
If that’s not the case, we set the window title, current_path variable and again use
FileAccess to put the content into the file.
func save_file(path = current_path): if path == UNSAVED_FILE_NAME: menu_save_file() return get_window().title = WINDOW_TITLE % [APPLICATION_NAME, path] current_path = path print('Saving to "%s"' % path) var file = FileAccess.open(path, FileAccess.WRITE) file.store_string(text_edit.text)
Let’s also quickly go over the three minor functions of this little project.
on_viewport_resize will change the current_dialog size to the viewport size every time it changes, so the dialog takes up all the space.
func on_viewport_resize(): if current_dialog: current_dialog.size = get_viewport().size
As its name implies, the
create_default_dialog function will create and return a dialog Node that conforms to the settings we made above. It will have the correct filter and access.
func create_default_dialog() -> FileDialog: var dialog := FileDialog.new() for file_filter in FILE_FILTERS: dialog.add_filter(file_filter, file_filter) dialog.access = FILE_ACCESS dialog.unresizable = true current_dialog = dialog on_viewport_resize() dialog.show() self.add_child(dialog) return dialog
Last but not least, we need to change the window title every time the user makes changes that arent saved.
func on_text_change(): get_window().title = WINDOW_TITLE_EDITED % [APPLICATION_NAME, current_path]
I know it’s a lot, and you may have better ideas than me, so feel free to try it out and build a program with Godot!
Or you could try to implement my wall jumping article in Godot 4.
Extra Tip: You can add a file called
.gdignore to any folder that the Godot Editor should ignore. So images in such a folder won’t be imported, which may be helpful in a folder containing the exports of your game of media for the itch.io page.
If you code in python you can take a look at this tutorial where we build a text editor with tkinter.