0
$\begingroup$

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

enter image description here

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",
    "description": "Demo Addon",
    "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()
$\endgroup$
4
  • $\begingroup$ 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. $\endgroup$
    – brockmann
    Nov 29, 2019 at 9:31
  • $\begingroup$ @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. $\endgroup$
    – RDK
    Nov 29, 2019 at 20:44
  • 1
    $\begingroup$ 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. $\endgroup$
    – batFINGER
    Dec 1, 2019 at 15:23
  • $\begingroup$ @batFINGER I indeed ended up displaying my generated long text in the text editor. Your comment is the real answer to my situation. $\endgroup$
    – RDK
    Dec 1, 2019 at 23:19

1 Answer 1

4
$\begingroup$

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.

enter image description here

# ##### 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
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  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", ""),
            ('ADD', "Add", "")))

    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.action == 'ADD':
            if self.list_item:
                item = scn.custom.add()
                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 = row.operator(CUSTOM_OT_actions.bl_idname, icon="ADD", text="Add Log Line")
        add.list_item = "Error %d" % (len(scn.custom))
        add.action = 'ADD'

        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.

$\endgroup$
3
  • $\begingroup$ 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). $\endgroup$
    – RDK
    Dec 1, 2019 at 23:22
  • $\begingroup$ The reason why I asked for how the text looks like @RDK $\endgroup$
    – brockmann
    Dec 2, 2019 at 7:45
  • 2
    $\begingroup$ 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. $\endgroup$
    – batFINGER
    Dec 16, 2019 at 8:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.