# Displaying text inside an addon Panel

This question is not about displaying text-like objects or mesh in the 3D viewport but rather about displaying plain text in a panel created by an Addon.

I have a panel with two rows. On the first row is a button (Action 1) linked to an operator which generates some long text (say a ~1000 character error message). I want to display this generated text in the second row (with wrapping instead of truncation or ellipsis). I also want to allow the user to select some or all of that text.

My current attempt

I'm unable to come up with anything better than using the label attribute to display the long text (code below). But it doesnt satisfy my requirements: (1) Doesnt allow text selection (2) doesnt display full text (just displays first and last few characters with ellipsis in between)

Ideally I'd like to have a display similar to the box for "Vertex Groups" below

Of course, I'd want the ability to change the label "Vertex Groups" and to be able to populate the box with (wraparound) text.

code

from bpy.props import StringProperty
import bpy

class MESH_OT_action1(bpy.types.Operator):
bl_idname = 'mesh.action1'
bl_label = 'Action 1'
bl_options = {'REGISTER', 'UNDO'}

def execute(self, context):
try:
context.scene.long_string = 'x' * 1000
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, e.args)
return {'CANCELLED'}

class PANEL_PT_MyPanel(bpy.types.Panel):
bl_idname = 'PANEL_PT_MyPanel'
bl_label = 'mypanel'
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Panel test'

def draw(self, context):
row_action_1_btn = self.layout.row()
row_action_1_btn.operator('mesh.action1', icon='WORLD_DATA', text='Action 1')

# Text area
row_text = self.layout.row()
text = context.scene.long_string
row_text.label(text=text, icon='WORLD_DATA')

bl_info = {
"name": "Textbox",
"author": "author@gmail.com",
"version": (0, 0, 1),
"blender": (2, 80, 0),
"location": "View3D",
"category": "Development",
}

def register():
bpy.utils.register_class(MESH_OT_action1)
bpy.utils.register_class(PANEL_PT_MyPanel)
bpy.types.Scene.long_string = StringProperty(name='long_string', default='')

def unregister():
bpy.utils.unregister_class(MESH_OT_action1)
bpy.utils.unregister_class(PANEL_PT_MyPanel)
del bpy.types.Scene.long_string

if __name__ == "__main__":
register()

• Might be a good idea showing us how the actual text looks like. Otherwise have a look into Create an interface which is similar to the material list box. – brockmann Nov 29 '19 at 9:31
• @brockmann Thanks. I dont see how the exact text string is relevant to the solution (except perhaps its min/max length). It is dynamically generated by the operator but its format is Error <error code>: <error detail>. I dont want to display it as report({INFO}, ...) because I want the user to be able to select the error text. – RDK Nov 29 '19 at 20:44
• Consider displaying the generated text in the text editor. It has word wrap and is simple to select. The text box panel UI is IMO just not set up for this. For instance could extend @brockmann s answer to add button to pop a text editor window with the text selected (or not), wrapped and readable. – batFINGER Dec 1 '19 at 15:23
• @batFINGER I indeed ended up displaying my generated long text in the text editor. Your comment is the real answer to my situation. – RDK Dec 1 '19 at 23:19

Log Window

Example using an UIList based on the answer of Create an interface which is similar to the material list box as suggested in the comments. Removed all superfluous operators to get a kind of log window.

# ##### 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_PT_objectList(Panel):
"""Adds a custom panel to the TEXT_EDITOR"""
bl_idname = 'TEXT_PT_my_panel'
bl_space_type = "TEXT_EDITOR"
bl_region_type = "UI"
bl_label = "Log UIlist"

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")

# -------------------------------------------------------------------
#   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_PT_objectList
)

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.

• Sincere thank you for taking the efforts to create a code-based answer here. Unfortunately, I cant accept it since my situation requires displaying of a single long string of text with wraparound. I'd still selfishly request you to consider leaving your code here in the hope that it might help someone in the future (myself included). – RDK Dec 1 '19 at 23:22
• The reason why I asked for how the text looks like @RDK – brockmann Dec 2 '19 at 7:45
• Remembered this one .. Maybe a candidate for blender.stackexchange.com/questions/160859/… This is a good answer (and q IMO) sheets me no end to come back and find the only two upvotes on page are mine. Please start up-voting people. – batFINGER Dec 16 '19 at 8:43