Text Editor with Godot 4
Share on

Text Editor with Godot 4

Oh hi there 👋 dont miss out by subscribing.

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.

Godot Logo

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.

Scene for The File Editor with Godot

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.

Layout Options for Control Nodes in Godot

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.

Setup

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 @onready annotation.

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 Project and 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 get_window().title.

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 on_file_menu_clicked.

    var i = 0
    for menu_item in MENU_ITEMS:
        var popup : PopupMenu = menu_button.get_popup()

        popup.add_item(menu_item[0], i)

        var new_input = InputEventKey.new()
        new_input.keycode = menu_item[2]
        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][1])

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)

on_viewport_resize, create_default_dialog and on_text_change

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[0], file_filter[1])

    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]

Conclusion

The File Editor in Action

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.

No Comments

No Comments Found, you Could be the First ...

Leave a Reply

Your email address will not be published. Required fields are marked *

Other Posts you may like ...

Programming Project Ideas

Programming Project Ideas

Regular Expressions

Regular Expressions

Dropdown with SASS/CSS

Dropdown with SASS/CSS

To Top
© 2022 - 2024 Maxim Mäder
Enthusiast Shui Theme Version 47