# Is there a way to display a multi-lines message in panel / popup?

My add-on performs diverse measurements on an arbitrary number of selected objects. The measurements results are printed to the console and saved to drive as tabulated data and .csv files respectively. I'd like to also display them in the GUI in a pop-up window.

I have not found any way to display multi-lines messages in a panel and nothing in the API (v2.81) struck me as related.

I am currently going around by showing in the panel the 5 first and last measurements (head()-like and tail()-like previews).

Would anybody have an idea on how to display tabular data of arbitrary length in a panel?

EDIT (in response to @RayMairlot):

The message to be displayed has n-lines (each line being a set of measurements - fixed width - for one object). I can't know beforehand how many lines there will be (depends on the number of objects measured) and row.label() only display text on a single line (\n are not recognized). Thus, I am currently building my pop-up panel with a a fixed number of row.label()statements (e.g. 10), that I populate with the 5 first and last lines of the message (measurements of the 5 first and last objects). I am not satisfied with this solution, it is just a workaround (better a preview of the results than no results displayed at all :-) ).

What I would need is either:

• a way to dynamically add row = layout.row() and row.label(text) statements to the panel to match the number of rows to display as it is invoked
• OR a way to display multilines messages (containing \n) in a single row.label(text)or equivalent.

I hope this clarifies.

• Can you elaborate on why you currently have to display the 5 first and last messages instead of all the data? Dec 15 '19 at 0:16
• edited my original question to clarify this point. Dec 16 '19 at 7:51
• blender.stackexchange.com/questions/159503/… Dec 16 '19 at 8:37

Example using an UIList based on the answer of Create an interface which is similar to the material list box? and my modification for Displaying text inside an addon Panel to get some kind of log window as part of a popup. I've removed all superfluous operators and added a CUSTOM_OT_popup operator calling invoke_props_dialog in its invoke method as well as a check method for proper drawing:

# ##### 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
#
#  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 LICENSE BLOCK #####

bl_info = {
"name": "log-uilist-dev",
"description": "",
"author": "p2or, brockmann",
"version": (0, 1),
"blender": (2, 80, 0),
"location": "Text Editor",
"warning": "", # used for warning icon and text in addons panel
"wiki_url": "",
"tracker_url": "",
"category": "Development"
}

import bpy

from bpy.props import (IntProperty,
BoolProperty,
StringProperty,
EnumProperty,
CollectionProperty)

from bpy.types import (Operator,
Panel,
PropertyGroup,
UIList)

# -------------------------------------------------------------------
#   Operators
# -------------------------------------------------------------------

class CUSTOM_OT_actions(Operator):
"""Move items up and down, add and remove"""
bl_idname = "custom.list_action"
bl_label = "List Actions"
bl_description = "Move items up and down, add and remove"
bl_options = {'REGISTER'}

action: EnumProperty(
items=(
('UP', "Up", ""),
('DOWN', "Down", ""),
('REMOVE', "Remove", ""),

list_item: StringProperty()

def invoke(self, context, event):
scn = context.scene
idx = scn.custom_index

try:
item = scn.custom[idx]
except IndexError:
pass
else:
if self.action == 'DOWN' and idx < len(scn.custom) - 1:
item_next = scn.custom[idx+1].name
scn.custom.move(idx, idx+1)
scn.custom_index += 1
info = 'Item "%s" moved to position %d' % (item.name, scn.custom_index + 1)
self.report({'INFO'}, info)

elif self.action == 'UP' and idx >= 1:
item_prev = scn.custom[idx-1].name
scn.custom.move(idx, idx-1)
scn.custom_index -= 1
info = 'Item "%s" moved to position %d' % (item.name, scn.custom_index + 1)
self.report({'INFO'}, info)

elif self.action == 'REMOVE':
info = 'Item "%s" removed from list' % (scn.custom[idx].name)
scn.custom_index -= 1
scn.custom.remove(idx)
self.report({'INFO'}, info)

if self.list_item:
item.name = self.list_item
item.coll_id = len(scn.custom)
scn.custom_index = len(scn.custom)-1
info = '"%s" added to list' % (item.name)
self.report({'INFO'}, info)

return {"FINISHED"}

class CUSTOM_OT_clearList(Operator):
"""Clear all items of the list"""
bl_idname = "custom.clear_list"
bl_label = "Clear List"
bl_description = "Clear all items of the list"
bl_options = {'INTERNAL'}

@classmethod
def poll(cls, context):
return bool(context.scene.custom)

def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)

def execute(self, context):
if bool(context.scene.custom):
context.scene.custom.clear()
self.report({'INFO'}, "All items removed")
else:
self.report({'INFO'}, "Nothing to remove")
return{'FINISHED'}

# -------------------------------------------------------------------
#   Drawing
# -------------------------------------------------------------------

class CUSTOM_UL_items(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
split = layout.split(factor=0.2)
split.label(text="%d" % (index))
#split.prop(item, "name", text="", emboss=False, translate=False, icon=custom_icon)
split.label(text=item.name) # avoids renaming the item by accident

def invoke(self, context, event):
pass

class CUSTOM_OT_popup(Operator):
bl_idname = "custom.call_popup"
bl_label = "Log Window"
bl_options = {'REGISTER'}

@classmethod
def poll(cls, context):
return True

def execute(self, context):
return {'FINISHED'}

def check(self, context):
return True

def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=500)

def draw(self, context):
layout = self.layout
scn = bpy.context.scene

rows = 7
row = layout.row()
row.template_list("CUSTOM_UL_items", "", scn, "custom", scn, "custom_index", rows=rows)

row = layout.row()
add.list_item = "Error %d" % (len(scn.custom))

row = layout.row().operator("custom.clear_list", icon="X")
row = layout.row()

# -------------------------------------------------------------------
#   Collection
# -------------------------------------------------------------------

class CUSTOM_objectCollection(PropertyGroup):
#name: StringProperty() -> Instantiated by default
coll_type: StringProperty()
coll_id: IntProperty()

# -------------------------------------------------------------------
#   Register & Unregister
# -------------------------------------------------------------------

classes = (
CUSTOM_OT_actions,
CUSTOM_OT_clearList,
CUSTOM_UL_items,
CUSTOM_objectCollection,
CUSTOM_OT_popup
)

def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)

# Custom scene properties
bpy.types.Scene.custom = CollectionProperty(type=CUSTOM_objectCollection)
bpy.types.Scene.custom_index = IntProperty()

def unregister():
from bpy.utils import unregister_class
for cls in reversed(classes):
unregister_class(cls)

del bpy.types.Scene.custom
del bpy.types.Scene.custom_index

if __name__ == "__main__":
register()


Note: the naming of the classes and variables might be off a little.