Skip to content

Instantly share code, notes, and snippets.

@Andrej730
Last active March 3, 2025 20:31
Show Gist options
  • Save Andrej730/ffe91117fea88c84f28df000463447a5 to your computer and use it in GitHub Desktop.
Save Andrej730/ffe91117fea88c84f28df000463447a5 to your computer and use it in GitHub Desktop.
Blender - Automatically reload texts which has changed externally
# ***** BEGIN GPL LICENSE BLOCK *****
#
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ***** END GPL LICENCE BLOCK *****
bl_info = {
"name": "Reload if Modified",
"author": "Dany Lebel (Axon_D), updated by @Andrej730",
"version": (1,0),
"blender": (2, 80, 0),
"location": "Text Editor -> Header Bar -> Reload if Modified",
"description": "Determine if a text datablock must be reloaded from file in the case that it has changed",
"warning": "",
"wiki_url": "http://wiki.blender.org/index.php/Extensions:2.5/Py/Scripts/Text_Editor/Reload_if_Modified",
"tracker_url": "http://projects.blender.org/tracker/index.php?func=detail&aid=25277&group_id=153&atid=467",
"category": "Text Editor"
}
# https://archive.blender.org/wiki/index.php/Extensions:2.5/Py/Scripts/Text_Editor/Reload_if_Modified/
"""
This addon will automatically reload a text datablock if its source file
does not corresponds to the last blendfile version. This must be checked
for each file wanted to be updated automatically. It has no effect on internal datablock.
"""
import bpy
def check_texts_every_second():
for text in bpy.data.texts:
if text.is_in_memory:
continue
if text.is_modified and text.reload_if_modified:
# using reloading operators causes crashes, therefore we reload the text manually
# saving text to file afterwards as it's the only way to flip `.is_modified`
# since reload doesn't work - https://projects.blender.org/blender/blender/issues/113644
with open(text.filepath, 'r') as fi:
new_text = fi.read()
text.from_string(new_text)
def get_area():
for screen in bpy.data.screens:
for area in screen.areas:
if area.type == "TEXT_EDITOR":
return area
context_override = {}
context_override["edit_text"] = text
space = next(space for space in get_area().spaces if space.type == "TEXT_EDITOR")
context_override["space_data"] = space
with bpy.context.temp_override(**context_override):
bpy.ops.text.save()
return 1 # every second
def draw_header(self, context):
text = context.space_data.text
if not text:
return
if text.is_in_memory:
return
layout = self.layout
row = layout.row()
row.prop(text, "reload_if_modified")
def register():
bpy.types.Text.reload_if_modified = bpy.props.BoolProperty(
name='Reload if Modified',
description=
'Automatically reload text file if it has changed',
default=False,
)
# tried `bpy.msgbus.subscribe_rna(key=bpy.data.texts.path_resolve("is_modified", False))`
# and didn't worked, therefore going to timer approach as depsgraph update won't be often enough
# maybe there is more clever way to do this
bpy.types.TEXT_HT_header.append(draw_header)
bpy.app.timers.register(check_texts_every_second)
def unregister():
del bpy.types.Text.reload_if_modified
bpy.types.TEXT_HT_header.remove(draw_header)
bpy.app.timers.register(check_texts_every_second)
if __name__ == '__main__':
register()
@Richardn2002
Copy link

This is very helpful, thank you!

I am new to Blender so I am not sure what is going on: This plugin has a minor minor problem of, it does not function (the "reload if modified" checkbox is present on the top right of the script area, but script never reloads.) after a Blender restart. I have to tick and re-tick the "enable" box in the add-ons list after a restart to restore the function. Other than this the plugin is working perfectly.

@Andrej730
Copy link
Author

@Richardn2002 check out console errors, maybe it has some clues.

@Richardn2002
Copy link

Richardn2002 commented Mar 3, 2025

I did not see any error messages on blender --debug-all, so I turned to good ol' print debugging. I injected:

import bpy

import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
logger.addHandler(logging.StreamHandler())

def check_texts_every_second():
    logger.debug("reload_if_modified heartbeat")
    ...

and restarted blender with blender --debug-python. I found out that, on blender restart, the heartbeat is there, meaning the scheduled event is running every second. but the moment any project file is loaded, the heartbeat disappeared, with no error messages.

Then if I proceed to disable the add-on, messages come:

        addon_utils.disable text_editor_reload_if_modified
reload_if_modified heartbeat
Traceback (most recent call last):
  File "/home/richardn/.config/blender/4.3/scripts/addons/text_editor_reload_if_modified.py", line 54, in check_texts_every_second
    if text.is_modified and text.reload_if_modified:
                            ^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'Text' object has no attribute 'reload_if_modified'

(Update: There is a typo on line 104, register should be unregister xd. But if I made the change, error message on disabling will change into "function not registered", which indicates a same problem: Python context is reset (update update: see below, all timers are cancelled) and register() is not called on project load.)

Then finally I re-enable the add-on,

        addon_utils.enable text_editor_reload_if_modified
reload_if_modified heartbeat
Info: Saved text "<path-to-my-open-script-file-in-my-project>"
reload_if_modified heartbeat
reload_if_modified heartbeat
...

@Richardn2002
Copy link

@Richardn2002
Copy link

Alright I made it. Change everything below draw_header() into:

@bpy.app.handlers.persistent
def load_handler(dummy):
    bpy.app.timers.register(check_texts_every_second)


def register():
    # build UI once per plugin load
    bpy.types.Text.reload_if_modified = bpy.props.BoolProperty(
                            name='Reload if Modified',
                            description=
                            'Automatically reload text file if it has changed',
                            default=False,
    )
    # tried `bpy.msgbus.subscribe_rna(key=bpy.data.texts.path_resolve("is_modified", False))`
    # and didn't worked, therefore going to timer approach as depsgraph update won't be often enough
    # maybe there is more clever way to do this
    bpy.types.TEXT_HT_header.append(draw_header)

    # register timer once per plugin load
    bpy.app.timers.register(check_texts_every_second)

    # loading a file cancels all timers, register timer once more per file load
    bpy.app.handlers.load_post.append(load_handler)


def unregister():
    del bpy.types.Text.reload_if_modified
    bpy.types.TEXT_HT_header.remove(draw_header)
    bpy.app.timers.unregister(check_texts_every_second)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment