23

Say we want to navigate a tree structure without knowing all the data ahead of time, is it possible to do this in Blender?

For example, a menu which dynamically loaded a directory hierarchy from the filesystem (without having to scan the filesystem and generate a menu type for every directory ahead of time first).

ideasman42
  • 47,387
  • 10
  • 141
  • 223
  • 1
    I think this question is really unclear and think you should edit it to make it more clear. – Skylumz Jun 07 '18 at 22:37
  • Added example . – ideasman42 Jun 08 '18 at 06:18
  • 3
    Why is it necessarily a menu, instead of, for instance, a separated popup panel called from a menu? – lemon Jun 08 '18 at 15:52
  • I can't gotcha what do you want :( – yhoyo Jun 11 '18 at 18:05
  • 2
    @yhoyo make a menu of the file system where a subfolder expands as another menu. The templates menu in text editor is an example of this, to one level. If you put a new file in blender_path/2.79/scripts/templates it is added automatically to the menu. As it is now, If you add a new folder into the templates folder, the files of the subfolder are added to the menu, but in the bottom level (along with the folder). The concept is to make the "New Folder" item, (and any folder) a menu so the menu > submenu> subsubmenu > item choice is: folder/subfolder/subsubfolder/file. – batFINGER Jun 12 '18 at 08:12
  • 1
    @batFINGER, I think this could be more simple if layout.menu was returning a menu object (so that a property value can be set on the corresponding class instance). Do you know why it is not the case (returns None)? – lemon Jun 14 '18 at 08:24
  • 2
    @lemon agreed.being able to pass properties in UI like you can to an operator would be bloody handy. On my linux build (the last time I spent too much time on this) I could register new menu classes (for subfolders) from the __init__ method, and set the filepath class property. Hopefully being able to pass props to menu, or have some ref to the parent menu, to submenu more effectively, is coming in 2.8. 8^) @yhoyo Here is a randomly expanding menu example – batFINGER Jun 14 '18 at 11:07
  • @batFINGER, so what about a patch at C level? Is it possible? (failed to locate the code for that so far, but if ideasman42 is asking, should consider this is not possible ?) – lemon Jun 14 '18 at 12:32
  • 1

2 Answers2

5

This can be done using by making use of the context pointer, mapping each row to some data which the menu can access, looking up the chain of open menus.

e.g.

import bpy
import os

Set these to dir and ID that make sense.

PATH_SEARCH = "~" CONTEXT_ID = "my_menu_id"

This operator is just an example, it only reports the filpath given to it.

class PathExampleOperator(bpy.types.Operator): bl_idname = "path.example" bl_label = "Path Example" filepath: bpy.props.StringProperty()

def execute(self, context):
    self.report({'INFO'}, "File: %r" % self.filepath)
    return {'FINISHED'}


class PathMenu(bpy.types.Menu): bl_label = "Directory Menu" bl_idname = "FILE_MT_dynamic_path_menu"

_parents = {}

@staticmethod
def _calc_path(layout):
    result = []
    while layout:
        layout, payload = PathMenu._parents.get(layout, (None, None))
        result.append(payload)
    result.reverse()
    return result

def draw(self, context):
    layout = self.layout
    parent_id = getattr(context, CONTEXT_ID, None)

    if parent_id is None:
        # This is the root level menu, use the base path.
        parent_path = os.path.expanduser(PATH_SEARCH)
        # Avoid accumulating indefinitely.
        PathMenu._parents.clear()
    else:
        parent_path = os.path.join(*PathMenu._calc_path(parent_id))

    for filename in sorted(os.listdir(parent_path)):
        # Skip hidden.
        if filename.startswith("."):
            continue

        filepath = os.path.join(parent_path, filename)
        if os.path.isdir(filepath):
            row = layout.row()
            row.context_pointer_set(CONTEXT_ID, row)
            # The payload could be anything, it so happens
            # in this case that the `filepath` makes sense to use.
            payload = filepath
            PathMenu._parents[row] = (parent_id, payload)
            row.menu(PathMenu.bl_idname, text=filename)
        else:
            # Not a directory, show a file.
            props = layout.operator(PathExampleOperator.bl_idname, text=filename, icon='FILE')
            props.filepath = filepath


def draw_item(self, context): layout = self.layout layout.menu(PathMenu.bl_idname)

def register(): bpy.utils.register_class(PathMenu) bpy.utils.register_class(PathExampleOperator)

# lets add ourselves to the main header
bpy.types.INFO_HT_header.append(draw_item)


def unregister(): bpy.utils.unregister_class(PathMenu) bpy.utils.unregister_class(PathExampleOperator)

bpy.types.INFO_HT_header.remove(draw_item)


if name == "main": register()

bpy.ops.wm.call_menu(name=PathMenu.bl_idname)

ideasman42
  • 47,387
  • 10
  • 141
  • 223
0

When you create a bpy.types.Menu object, it has a draw method (as can be seen here) which is called every time the menu is shown. Each submenu is it's own class, and every class needs to be registered before it can be displayed.

While the draw method would have been the ideal place to dynamically generate and register the submenus, attempting to call bpy.utils.register_class from inside the method will result in RuntimeError: register_class(...): can't run in readonly state 'TOPBAR_MT_custom_menu_<id>.

Unless I've missed something obvious here, I'd say the answer is a definitive "not possible". Blender just outright locks itself when displaying the menus.


That said, I came across here because I wanted to dynamically generate the menu classes. If it helps anyone wanting to do the same, here's the code I came up with.

Peter
  • 109
  • 3