2

In the example below I have 3 instance of a same popover panel class i want to pass a string argument to these panels, so they each display different text, without creating 3 different panel

how can i do that?

import bpy

class TEST_PT_popover(bpy.types.Panel): bl_idname = "TEST_PT_popover" bl_label = "" bl_category = "" bl_space_type = "VIEW_3D" bl_region_type = "HEADER"

def __init__(self,):
    pass

def draw(self, context):
    self.layout.label(text="Contextual String?")     

class TEST_PT_1(bpy.types.Panel): bl_idname = "TEST_PT_1" bl_label = "Panel1" bl_category = "PASS_TXT" bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_context = "objectmode"

def draw(self, context):
    layout = self.layout

    row = layout.row()
    pass_this_string = "This an interesting text about One"
    row.popover(panel="TEST_PT_popover", text="One")

    row = layout.row()
    pass_this_string = "This is all about the number two"
    row.popover(panel="TEST_PT_popover", text="Two")

class TEST_PT_2(bpy.types.Panel): bl_idname = "TEST_PT_2" bl_label = "Panel2" bl_category = "PASS_TXT" bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_context = "objectmode"

def draw(self, context):
    layout = self.layout

    row = layout.row()
    pass_this_string = "And this is α, in another panel"
    row.popover(panel="TEST_PT_popover", text="Alpha") #the method should work on multiple interface too! some of the answers below will not support multiple panels

classes = (TEST_PT_1,TEST_PT_2,TEST_PT_popover,)

def register(): for cls in classes: if not cls.is_registered: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): if cls.is_registered: bpy.utils.unregister_class(cls)

unregister() register()

I wanted a behavior similar to what we do when we pass an argument to an operator :

op = layout.popover(panel="TEST_PT_popover", text="One") 
op.text = "this" #op will be None, this will not work!

I am aware of layout.context_pointer_set() function, however, we cannot pass simple string argument with this function, unfortunately, we are forced to create properties for each argument we want to pass, there should be an easy trick to pass a simple text argument from a UILayout to a Panel class, isn't it?

Fox
  • 1,892
  • 18
  • 48
  • IDEA: maybe there's a way to call an operator that will call a window_manager drawing popover function? from the operator we could pass the argument to the panel class easily. I do not recall that pop_over panels could be called from a typical window_manager function tho.. – Fox Jul 25 '22 at 19:35

4 Answers4

4

Just for the sake of it I'll add another solution. Warning, it's very hacky. You can abuse the fact that you can set any arbitrary named member to the context and use None as data.

It is highly dependent on the actual strings you want to display and I wouldn't rely on it too much since it can cause name collisions and such, but it is self contained in the draw methods.

I use a prefix my_string_ to recognize the label I want to display. But since the different members add up, I needed a way to take only the very last one I added, hence the integer counter to only pick the last one.

import bpy

class TEST_PT_popover(bpy.types.Panel): bl_idname = "TEST_PT_popover" bl_label = "" bl_category = "" bl_space_type = "VIEW_3D" bl_region_type = "HEADER"

def draw(self, context):
    attrs = [attr.split("my_string_")[1] for attr in dir(context) if attr.startswith("my_string_")]
    attrs.sort()
    self.layout.label(text=attrs[-1][2::])


class TEST_PT_test(bpy.types.Panel): bl_idname = "TEST_PT_test" bl_label = "test" bl_category = "test" bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_context = "objectmode"

def draw(self, context):
    layout = self.layout
    for label, text, i in zip(("Hello", "World", "!"), ("One", "Two", "Three"), range(3)):
        row = layout.row()
        row.context_pointer_set(f"my_string_{i}_{label}", None)
        row.popover(panel="TEST_PT_popover", text=text)


def register(): bpy.utils.register_class(TEST_PT_test) bpy.utils.register_class(TEST_PT_popover)

register()

Gorgious
  • 30,723
  • 2
  • 44
  • 101
  • Ingenious solution :D Indeed hacky but that could work – Fox Jul 20 '22 at 15:06
  • I marked your solution as resolved, until perhaps someone find something not hacky, i doubt there are elegant solution for this particular case – Fox Jul 20 '22 at 15:15
  • 1
    Hehe I'm a bit ashamed to have posted such a monstrosity TBH >< I hope I just missed something. Maybe worth pinging @ideasman42 who demonstrated a fancy recursive menu just yesterday https://blender.stackexchange.com/a/269716/86891 – Gorgious Jul 20 '22 at 15:20
2

Alright I swear this is the last one :)

The awesome recursive menus answer found there pushed me in the right direction to get a solution idea which is quite clean.

You can pass a bpy.types.UILayout.row as data in layout.context_pointer_set.

By dynamically populating a dictionary as a member of the panel class, we can use the row as a lookup key to retrieve the label.

import bpy

class TEST_PT_popover(bpy.types.Panel): bl_idname = "TEST_PT_popover" bl_label = "" bl_category = "" bl_space_type = "VIEW_3D" bl_region_type = "HEADER"

labels = {}

def draw(self, context):
    self.layout.label(text=self.labels[context.my_mapping_label])         

class TEST_PT_test(bpy.types.Panel): bl_idname = "TEST_PT_test" bl_label = "test" bl_category = "test" bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_context = "objectmode"

def draw(self, context):
    layout = self.layout
    for label, text in zip((&quot;Hello&quot;, &quot;World&quot;, &quot;!&quot;), (&quot;One&quot;, &quot;Two&quot;, &quot;Three&quot;)):
        row = layout.row()
        row.context_pointer_set(&quot;my_mapping_label&quot;, row)
        TEST_PT_popover.labels[row] = label
        row.popover(panel=&quot;TEST_PT_popover&quot;, text=text)


def register(): bpy.utils.register_class(TEST_PT_test) bpy.utils.register_class(TEST_PT_popover)

register()

Gorgious
  • 30,723
  • 2
  • 44
  • 101
  • this is much more elegant, however i tried on multiple T panel and it seems that the argument starts to get mixed up, because we rely on blender execution order, and i doubt it is something we can anticipate/rely upon – Fox Jul 20 '22 at 21:44
  • the more i look at it... it seems that row or any layout objects cannot be relied as an identifier, because the hash of the obj seems to repeat themselves from one main panel to another. We are really starting to flirt with the limit of bpy here i believe, reverse engineer how they deal memory adress and execution time lmao if you want to take a look https://wtools.io/paste-code/bD8M – Fox Jul 20 '22 at 22:20
  • Interesting. Yeah I figure it's pushing the limits a bit too far. I hope you find a middle ground that's not too hacky nor ugly :) – Gorgious Jul 21 '22 at 06:34
2

initial approach

One possible solution which I successfully used in a different context is to use Pythons type() function to dynamically instantiate a class.

This allows you to register Blender classes, like UI_Lists, Popovers, etc dynamically, and if you combine this with the concept of inheritance (mix-in classes), you should be able to re-use code efficiently. The beauty of this method is that it not only works from registration code as I did it here. In my own example I dynamically register UI_Lists from within an Operator as well, and this so far works beautifully.

I have commented the steps in the demo Add-on below. Just add random texts to the texts list as you wish. Of course you could also rework this using Python dictionaries etc. etc., but from here only you know what you want to achieve actually.

Complete Add-on example with comments below:

'''
Created on 25.07.2022

@author: r.trummer '''

import bpy from bpy.types import Panel

bl_info = { "name": "Popover Demo", "author": "Rainer Trummer", "version": (0, 1, 0), "blender": (3, 1, 0), "description": "draws variable popovers in the UI", "category": "Interface" }

holder variable for our dynamic classes which each will be a popup

popoverClasses = []

the texts you want to pass on to each class

texts = ['alpha', 'beta', 'gamma', 'delta', 'epsilon']

#===============================================================================

the TEST_PT_popover class is a mix in class which we will later on

combine with the Panel class using inheritance. In here we add our own

ui_text Propoerty, which in the dynamic instantiation we can fill with

our desired content. Of course you can define as many properties as you

like. Even lists will work

#=============================================================================== class TEST_PT_popover(): bl_category = "" bl_space_type = "VIEW_3D" bl_region_type = "UI"

#    note the bl_options has to be set to INSTANCED, otherwise the Panels will
#    draw twice in the UI. You'll see what I mean if you comment it out
bl_options = {'INSTANCED'}

#    hold the text info the user dynamically wants to change
ui_text = None

def draw(self, context):
    #    put your to be shared draw code in here
    self.layout.label(text = self.ui_text)


#===============================================================================

this class draws our main Panel, where the Popovers will live in

#=============================================================================== class TEST_PT_popover_holder(Panel): bl_idname = "TEST_PT_popover_holder" bl_label = "sucker" bl_category = "" bl_space_type = "VIEW_3D" bl_region_type = "UI"

def draw(self, context):
    layout = self.layout
    layout.label(text = 'HOLDER')

    #    iterate over all given texts and construct a unique name for the Panel
    for i, t in enumerate(texts):
        print(f'drawing popover myPanel_PT_{i}')
        layout.popover(panel = f'myPanel_PT_{i}', text = t)


classes = ( TEST_PT_popover_holder, )

def register(): # standard base class registration first # do NOT yet register our dynamic class for c in classes: bpy.utils.register_class(c)

#    now iterate over all texts...
for i, t in enumerate(texts):
    #    ...and using the type function create a panel class dynamically
    #    this class gets a unique name by adding an index, and inherits from
    #    TEST_PT_popover and Panel classes at the same time
    panelClass = type(f'myPanel_PT_{i}', (TEST_PT_popover, Panel), {})

    #    now give the new class a label
    panelClass.bl_label = t

    #    and finally, this is what you're here for, set the custom ui_text
    panelClass.ui_text = f'{t} is my favorite number {i+1}'

    #    appending the new class to a static variable helps us to unregister
    #    the new class afterwards when we exit Blender or reload Add-ons
    print(f'appending popover {panelClass} to class holder variable')
    popoverClasses.append(panelClass)

    #    now finally register the dynamic class
    print(f'registering class {panelClass}')
    bpy.utils.register_class(panelClass)


def unregister(): # iterate over all dynamic classes that we created and unregister them for poc in popoverClasses: print(f'unregistering class {poc}') bpy.utils.unregister_class(poc)

#    clear the variable too
popoverClasses.clear()

#    standard unregistration code
for c in reversed(classes):
    bpy.utils.unregister_class(c)


if name == 'main': register()

review after additional comment

After reviewing the additional question from the comment, I understand that the request is to create multiple Panels with multiple Popups each, and pass on Strings to them. I have updated the example above with the following approach, where instead of a text list I use a dictionary. Dictionaries are very flexible and can also hold lists as data, while using strings as keys. Only requirement is that the keys are unique. Also the Panel and Popup names need to be unique, so that's something to watch out for.

By constructing a dictionary, where the keys() are the Panel header names, and the items() are the lists of strings that shall become popups, then using dynamic registration twice, this becomes rather flexible. Just change the texts dictionary to whatever you require:

'''
Created on 25.07.2022

@author: r.trummer '''

import bpy from bpy.types import Panel

bl_info = { "name": "Popover Demo", "author": "Rainer Trummer", "version": (0, 1, 5), "blender": (3, 1, 0), "description": "draws variable popovers in the UI", "category": "Interface" }

holder variable for our dynamic classes which each will be a popup

popoverClasses = []

the texts you want to pass on to each class

texts = {'Panel A': ['alpha', 'beta', 'gamma', 'delta', 'epsilon'], 'Panel B': ['not', 'a', 'fan', 'of', 'Greek', 'nomenclature'], 'Some Dude At The End': ['latin', 'rocks']}

#===============================================================================

the TEST_PT_popover class is a mix in class which we will later on

combine with the Panel class using inheritance. In here we add our own

ui_text Propoerty, which in the dynamic instantiation we can fill with

our desired content. Of course you can define as many properties as you

like. Even lists will work

#=============================================================================== class TEST_PT_popover(): bl_category = "" bl_space_type = "VIEW_3D" bl_region_type = "UI"

#    note the bl_options has to be set to INSTANCED, otherwise the Panels will
#    draw twice in the UI. You'll see what I mean if you comment it out
bl_options = {'INSTANCED'}

#    hold the text info the user dynamically wants to change
ui_text = None

def draw(self, context):
    #    put your to be shared draw code in here
    self.layout.label(text = self.ui_text)


def generate_panel_name(number, caption): return f'myPanel_PT_{caption.replace(" ", "")}{number}'

#===============================================================================

this class draws our main Panel, where the Popovers will live in

#=============================================================================== class TEST_PT_popover_holder(Panel): bl_idname = "TEST_PT_popover_holder" bl_label = "Main Panel" bl_category = "" bl_space_type = "VIEW_3D" bl_region_type = "UI"

def draw(self, context):
    layout = self.layout
    layout.label(text = 'HOLDER')


#===============================================================================

this class will also be dynamically instantiated, and create the Panels

themselves, as childs of the main manel

#=============================================================================== class TEST_PT_popover_panel_item(Panel): bl_label = "subitem" bl_category = "" bl_space_type = "VIEW_3D" bl_region_type = "UI"

#    make this Panel a child of the holder Panel
bl_parent_id = TEST_PT_popover_holder.bl_idname
bl_options = {'INSTANCED'}

dictionaryIndex = None
customCaption = None

def draw(self, context):
    layout = self.layout
    layout.label(text = self.customCaption)

    #    now iterate, for the passed on dictionary element, over the given list of tests
    #    by doing this, we pick a certain text list from the dictionary to be displayed
    #    in this specific Panel
    for i, t in enumerate(texts[self.dictionaryIndex]):
        print(f'drawing popover myPanel_PT_{i} called {self.dictionaryIndex}')
        layout.popover(panel = generate_panel_name(i, self.dictionaryIndex), text = t)


classes = ( TEST_PT_popover_holder, )

def register(): # standard base class registration first # do NOT yet register our dynamic class for c in classes: bpy.utils.register_class(c)

#    construct unique names for Panel classes
for k, p in enumerate(texts):
    #    k is the index in the Dictionary
    #    p is the Panel name
    #    register a sub-Panel class first to hold the elements
    panelClassName = generate_panel_name(k, f'{p}_panel')

    print(f'creating Panel Class {panelClassName}')
    panelClass = type(panelClassName, (TEST_PT_popover_panel_item, Panel), {})

    #    pass the dictionary index so the subpanel finds the right text list from the dictionary
    panelClass.dictionaryIndex = p
    #    also change the Panel label while we're at it
    panelClass.customCaption = f'this is {p}'
    #    and if you like, the header too
    panelClass.bl_label = f'{p.upper()}'

    #    register the Panel now
    bpy.utils.register_class(panelClass)
    popoverClasses.append(panelClass)

    #    now iterate, for each dictionary element, over the given list of tests
    for i, t in enumerate(texts[p]):
        #    i is the index of the string list picked from the dictionary
        #    t is the text string itself

        pName = generate_panel_name(i, p)
        print(f'constructing popover Panel called {pName}')

        #    ...and using the type function create a panel class dynamically
        #    this class gets a unique name by adding an index, and inherits from
        #    TEST_PT_popover and Panel classes at the same time
        popoverClass = type(pName, (TEST_PT_popover, Panel), {})

        #    now give the new class a label
        popoverClass.bl_label = f'label of this class: {t}'

        #    and finally, this is what you're here for, set the custom ui_text
        popoverClass.ui_text = f'{t} belongs to Panel class {p}'

        #    appending the new class to a static variable helps us to unregister
        #    the new class afterwards when we exit Blender or reload Add-ons
        print(f'appending popover {popoverClass} to class holder variable')
        popoverClasses.append(popoverClass)

        #    now finally register the dynamic class
        print(f'registering class {popoverClass}')
        bpy.utils.register_class(popoverClass)


def unregister(): # iterate over all dynamic classes that we created and unregister them for poc in popoverClasses: print(f'unregistering class {poc}') bpy.utils.unregister_class(poc)

#    clear the variable too
popoverClasses.clear()

#    standard unregistration code
for c in reversed(classes):
    bpy.utils.unregister_class(c)


if name == 'main': register()

aliasguru
  • 11,231
  • 2
  • 35
  • 72
  • thanks for your input, Could this technique work on multiple panels? I feel that the global will get in the way! I currently rely on a similar procedural registering system, i search for commented markers on all py modules with regex such as #REGTIME_PROP:"my_prop";VALUE"this text" and create the classes/props i need at regtime.

    I'll leave the bounty active for a few days, I'm hoping maybe a bpy blender developer such as @ideasman42 or @dr. Sybren could give us a canonical statement on this problem

    – Fox Jul 25 '22 at 19:28
  • @DB3D I updated my answer with what I think fits your requirement, even though it's not utterly clear to me what you're looking for in the end. For questions such as this, it is sometimes better to supply a UI mockup with the desired outcome, and describe what you'd like to use as input – aliasguru Jul 29 '22 at 13:04
1

You can use a bpy.props.CollectionProperty and populate it with somme dummy instances of a PropertyGroup. It means you'll have to use some boilerplate code at register though.

import bpy

class TEST_PT_popover(bpy.types.Panel): bl_idname = "TEST_PT_popover" bl_label = "" bl_category = "" bl_space_type = "VIEW_3D" bl_region_type = "HEADER"

def draw(self, context):
    self.layout.label(text=context.my_string.name)


class TEST_PT_test(bpy.types.Panel): bl_idname = "TEST_PT_test" bl_label = "test" bl_category = "test" bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_context = "objectmode"

def draw(self, context):
    layout = self.layout
    for i, text in enumerate((&quot;One&quot;, &quot;Two&quot;, &quot;Three&quot;)):
        layout.context_pointer_set(&quot;my_string&quot;, context.scene.my_strings[i])
        layout.popover(panel=&quot;TEST_PT_popover&quot;, text=text)


class StringPG(bpy.types.PropertyGroup): pass # name is a default property, no need to redefine it

def register(): bpy.utils.register_class(StringPG) bpy.types.Scene.my_strings = bpy.props.CollectionProperty(type=StringPG) bpy.utils.register_class(TEST_PT_test) bpy.utils.register_class(TEST_PT_popover)

register() bpy.context.scene.my_strings.clear() bpy.context.scene.my_strings.add().name = "Hello" bpy.context.scene.my_strings.add().name = "World" bpy.context.scene.my_strings.add().name = "!"

enter image description here

Gorgious
  • 30,723
  • 2
  • 44
  • 101
  • 1
    Thanks a lot for your answer! That's what we are currently doing in our studio and we had to register 30+ additional properties, just to transfer a text arg from one panel to another.. i was wondering if there are a more elegant solutions, thus my question – Fox Jul 20 '22 at 15:02
  • I think this is the most robust one, but I agree that it would be nice to be able to seamlessly pass arbitrary arguments to UI elements like we can with operators... Cheers – Gorgious Jul 20 '22 at 15:17