3
$\begingroup$

My question is not about the File Browser modal window, but about the Editor. Screenshot of menu showing File Browser Editor

I found this other question that shows how to get the selected files from the File Browser Editor via Python, but I would like to know when the user clicks on a file to update my add-on dynamically. I can't find a handler that triggers a callback on file click, so I'm thinking this isn't possible. I could kludge it and poll for the currently selected file, but that is less than ideal. I could also go hard-core and build an entire Python UI for selecting files, but it just seems silly to go that route since the UI already exists in C.

Am I right in concluding that the only use for the File Browser Editor is for the user to see files and drag/drop them into other editors?

(Alternatively, I could use the Outliner Editor, filter for images, and use that for selection. But again, I can't find a handler that calls me back when the user clicks on an image.)


Edit

A solution was discovered after being inspired by the answers given:

import bpy
import os

def fbDraw(panel, context):
    layout: bpy.types.UILayout = panel.layout
    area = next((a for a in context.screen.areas if a.type == "FILE_BROWSER"), None)
    if area:
        params = area.spaces.active.params
        # Pretend code follows:
        info = params.filename if '_PT_' in panel.bl_idname else params.directory.decode()
        info = info.replace(os.path.sep, '/')
        layout.label(text=f'{panel.bl_idname}: {info}')

def register():
    # Either of these would work
    bpy.types.FILEBROWSER_HT_header.append(fbDraw)
    bpy.types.FILEBROWSER_PT_directory_path.prepend(fbDraw)
    

def unregister():
    bpy.types.FILEBROWSER_HT_header.remove(fbDraw)
    bpy.types.FILEBROWSER_PT_directory_path.remove(fbDraw)

(More info in my answer below)

$\endgroup$
3
  • $\begingroup$ have u tried the depsgraph app handler? also can u pls show a screenshot of the exact window? i know the asset browser is a file browser editor. is that what u mean? $\endgroup$
    – Harry McKenzie
    Commented May 6 at 23:04
  • $\begingroup$ @HarryMcKenzie - I did try the depsgraph handler, but that only updates when I drag/drop a file (it does not run when simply clicking a file). $\endgroup$ Commented May 7 at 3:30
  • $\begingroup$ I also thought of using the Asset Browser, but it doesn't update handlers unless I drag/drop the assets, or update their meta data (asset_data), i.e., clicking an asset doesn't trigger any handler. Note: I put a callback in every single handler available, for testing. $\endgroup$ Commented May 7 at 3:39

3 Answers 3

3
$\begingroup$

You can run a function every XX seconds using bpy.app.timers and check if the file has changed by comparing with a string property stored somewhere, like the current scene. Not ideal, but I don't know of a callback that is available to the API for that.

import bpy
from bpy.app.handlers import persistent


@persistent
def check_file_change():
    area = next((a for a in bpy.context.screen.areas if a.type == "FILE_BROWSER"), None)
    if area is not None:
        space = area.spaces[0]
        params = space.params
        directory = params.directory
        filename = params.filename
        filepath = str(directory) + str(filename)
        if filepath != bpy.context.scene.currently_selected_filepath:
            # Callback here
            print(f"Selected new file : {filepath}")
        bpy.context.scene.currently_selected_filepath = filepath
    return 0.1  # Check every XX seconds


bpy.types.Scene.currently_selected_filepath = bpy.props.StringProperty()
bpy.app.timers.register(check_file_change)
$\endgroup$
1
  • $\begingroup$ Oh, even though I thought the polling option wasn't ideal, somehow your comment of #Callback here gave me an idea: perhaps if I prepend a draw function to the bpy.types.FILEBROWSER_PT_directory_path, I could hijack its automatic UI update and have a de facto callback. I just tested it, and it worked! Since the UI updates when the mouse moves/clicks, it happens to work as a perfect callback. $\endgroup$ Commented May 8 at 6:22
3
$\begingroup$

You can also opt to use a modal operator that starts on registration and continually checks for LMB left mouse button release events within the FILE_BROWSER until the addon is unregistered.

import bpy
import os

def on_left_mouse_release():
    area = next((a for a in bpy.context.screen.areas if a.type == "FILE_BROWSER"), None)
    if not area:
        return
    space = area.spaces[0]
    params = space.params
    directory = params.directory.decode("utf-8")
    filename = params.filename
    filepath = os.path.join(directory, filename)
    if filepath != bpy.context.scene.currently_selected_filepath:
        print(f"File Select : {filepath}")
    bpy.context.scene.currently_selected_filepath = filepath

class FILE_OT_FileSelectOperator(bpy.types.Operator):
    bl_idname = "file.file_select_operator"
    bl_label = "My File Select Operator"

    cancel_modal_operator = False

    def modal(self, context, event):
        if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
            on_left_mouse_release()

        if self.cancel_modal_operator:
            self.cancel_modal_operator = False
            return {'CANCELLED'}

        return {'PASS_THROUGH'}

    def invoke(self, context, event):
        wm = context.window_manager
        wm.modal_handler_add(self)
        return {'RUNNING_MODAL'}

def register():
    bpy.utils.register_class(FILE_OT_FileSelectOperator)
    bpy.types.Scene.currently_selected_filepath = bpy.props.StringProperty()
    bpy.ops.file.file_select_operator('INVOKE_DEFAULT')

def unregister():
    FILE_OT_FileSelectOperator.cancel_modal_operator = True
    del bpy.types.Scene.currently_selected_filepath
    bpy.utils.unregister_class(FILE_OT_FileSelectOperator)

if __name__ == "__main__":
    register()
$\endgroup$
7
  • $\begingroup$ I still to test this, but do you know off-hand if a long-running modal operator like this will interfere with other modal operators? $\endgroup$ Commented May 8 at 6:06
  • 1
    $\begingroup$ as far as i know it should be fine. $\endgroup$
    – Harry McKenzie
    Commented May 8 at 6:23
  • 1
    $\begingroup$ Your mention of Blender continually checking for <kbd>LMB</kbd> events, combined with a hint from Gorgious' answer, gave me an idea that ended up working! See comment under that answer for details. $\endgroup$ Commented May 8 at 6:29
  • $\begingroup$ nice! i'm glad you found something that works out for you. I hope you can share the answer or post an answer so we can also learn something from you :D $\endgroup$
    – Harry McKenzie
    Commented May 8 at 6:33
  • 1
    $\begingroup$ Note modal operators de-activates the auto-save feature. Pretty big caveat IMO :) $\endgroup$
    – Gorgious
    Commented May 13 at 9:27
2
$\begingroup$

So between the two workarounds already posted, I was inspired to find a third. Somehow, while mulling over the ideas of creating our own callback, and hijacking the modal operator's ability to act as a callback, my brain jumped to creating a callback via the Panel draw function mechanism built in to Blender.

The idea is that since the Panel layout code is called on mouse moves/clicks, we can use that to determine when the user clicks on a file in the File Browser Editor.

Note: the example code appears like it doesn't work when clicking on a directory, but only because Blender doesn't update the params.filename in this case. The draw code is still called, however.

Unfortunately, since this function is called during the draw routine, ID blocks are read-only.

import bpy
import os

def fbDraw(panel, context):
    layout: bpy.types.UILayout = panel.layout
    area = next((a for a in context.screen.areas if a.type == "FILE_BROWSER"), None)
    if area:
        params = area.spaces.active.params
        # Pretend code follows:
        info = params.filename if '_PT_' in panel.bl_idname else params.directory.decode()
        info = info.replace(os.path.sep, '/')
        layout.label(text=f'{panel.bl_idname}: {info}')

def register():
    # Either of these would work
    bpy.types.FILEBROWSER_HT_header.append(fbDraw)
    bpy.types.FILEBROWSER_PT_directory_path.prepend(fbDraw)
    

def unregister():
    bpy.types.FILEBROWSER_HT_header.remove(fbDraw)
    bpy.types.FILEBROWSER_PT_directory_path.remove(fbDraw)
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .