7
$\begingroup$

I have seen in a video form cgcookies that they added a Help button and a Help text box right into the 3D View. How can this be achieved within an Addon?

enter image description here

$\endgroup$
1

2 Answers 2

7
$\begingroup$

Been playing around with bgl visualizations for sounds. Used a non modal approach akin to https://blender.stackexchange.com/a/40676/15543 Later on decided it would be nice to have some way to hover over and display information. Left clicking involved having to keep tabs on the 3dcursor.

Here is a stripped down but working version of where I got to with hover. Meant to get around to giving "buttons" a state eg mouse_over etc but... Also an alignment eg (TOP, MIDDLE, BOTTOM) (LEFT, CENTRE, RIGHT)

Uses screen area indexes to keep tabs on which area to show help screen. Button is white when initialised, blue when mouse in area and red when hovered over. The green help window appears when hovered over.

The modal operator is invoked with 'INVOKE_SCREEN'

Note: you can save settings to the screen datablock, which is a nicely away from prying.

import bpy
import blf
from bpy.props import IntProperty, FloatProperty
import bgl

class BGLWidget:
    handle = None

    def __init__(self, op, context, areatype):

        # Calculate scroller width, dpi and pixelsize dependent
        self.pixel_size = context.user_preferences.system.pixel_size
        self.dpi = context.user_preferences.system.dpi
        self.dpi_fac = self.pixel_size * self.dpi / 72
        # A normal widget unit is 20, but the scroller is apparently 16
        self.scroller_width = 16 * self.dpi_fac

        self.op = op
        self.areatype = areatype

        self.handle = self.create_handle(context)
        theme = context.user_preferences.themes[0]
        self.theme = theme


    def create_handle(self, context):
        handle = self.areatype.draw_handler_add(
            self.draw_region,
            (context,),
           'WINDOW', 'POST_PIXEL')  
        return handle     

    def remove_handle(self):

        if self.handle:
            self.areatype.draw_handler_remove(self.handle, 'WINDOW') 
            self.handle = None   

    def draw_region(self, context):
        # check validity

        self.visualise(context)


    def draw_box(self, x, y, w, h, color=(0.0, 0.0, 0.0, 1.0)):
        #bgl.glDepthRange (0.1, 1.0)
        bgl.glColor4f(*color)
        bgl.glBegin(bgl.GL_QUADS)

        bgl.glVertex2f(x+w, y+h)
        bgl.glVertex2f(x, y+h) 
        bgl.glVertex2f(x, y) 
        bgl.glVertex2f(x+w, y)      
        bgl.glEnd()

    def visualise(self, context):
        # used to draw override in class def
        pass

class Button:
    def __init__(self, x, y, w, h, color=(1,1,1,1)):
        #draw a box
        self.x = 0
        self.y = 0
        self.w = w
        self.h = h
        self.color = color   

    def __str__(self):
        return "Button %s" % str(self.color)

    def __repr__(self):
        return "Button %d %d color(%s)" % (self.x, self.y, str(self.color))

    def in_box(self, x, y):
        return (self.x < x < self.x + self.w
                and self.y < y < self.y + self.h)



class ButtonWidget(BGLWidget):
    help_screen = -1
    buttons = []
    screen_buttons = {}
    def button(self, w, h):
        # add a new button
        b = Button(0, 0, w, h)
        self.buttons.append(b)
        return b


    def visualise(self, context):
        if self.help_screen > -1:
            print("HELP", self.help_screen)
            if context.screen.areas[self.help_screen] == context.area:
                self.draw_box(0, 0, 10000, 10000, color=(0, 1, 0, 1))
                context.area.tag_redraw()
        for b in self.buttons:
            self.draw_button(b, context)

    def draw_button(self, box, context):
        m = [i for i, a in enumerate(context.screen.areas) if a == context.area]
        if not len(m):
            return None
        key = "area%d" % m[0]
        b = self.screen_buttons.setdefault(key, Button(box.x, box.y, box.w, box.h, color=box.color))
        b.x = context.region.width - b.w - self.scroller_width
        b.y = context.region.height - b.h - self.scroller_width
        #print(b.x, b.y, b.w, b.h)  # debug shows box coords on draw
        self.draw_box(b.x, b.y, b.w, b.h, color=b.color)
        #self.screen_buttons[key] = b

    def mouse_over(self, screen, area_index, x, y):
        key = "area%d" % area_index
        box = self.screen_buttons.get(key, None)
        if box:
            area = screen.areas[area_index]
            if box.in_box(x, y):
                box.color = (1, 0, 0, 0)
                self.help_screen = area_index
                area.tag_redraw()
            else:
                self.help_screen = -1
                box.color = (0, 0, 1, 0)
            #self.screen_buttons[key] = box
            area.tag_redraw()


class ModalOperator(bpy.types.Operator):
    """Move an object with the mouse, example"""
    bl_idname = "object.modal_operator"
    bl_label = "Simple Modal Operator"

    def modal(self, context, event):
        def in_area(area, x, y):

            return (area.x < x < area.x + area.width 
                and area.y < y < area.y + area.height)

        screen = context.screen
        mx = event.mouse_x
        my = event.mouse_y
        #print(mx, my)        
        areas = [i for i, a in enumerate(screen.areas) if a.type.startswith('VIEW_3D')
                 and in_area(a, mx, my)]        

        for i in areas:
            a = screen.areas[i]
            region = a.regions[-1]
            x = mx - region.x
            y = my - region.y
            ui.mouse_over(screen, i, x, y)
            if event.type == 'LEFTMOUSE':

                print('PRESS in screen["%s"].areas["%d"]' % (screen.name, i))
                #click events ???

        if event.type in {'ESC'}:
            # dont have to remove the UI here

            ui.remove_handle()
            return {'CANCELLED'}

        return {'PASS_THROUGH'}

    def invoke(self, context, event):
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}

def register():
    bpy.utils.register_class(ModalOperator)


def unregister():
    bpy.utils.unregister_class(ModalOperator)

if __name__ == "__main__":
    register()

    # create a UI
    context = bpy.context                 
    h = 50
    w = 200
    ui = ButtonWidget(None, context, bpy.types.SpaceView3D)
    button = ui.button(w, h)
    for a in context.screen.areas:
        if a.type == 'VIEW_3D':
            a.tag_redraw()
    bpy.ops.object.modal_operator('INVOKE_SCREEN')
$\endgroup$
2
  • $\begingroup$ would this work if it's an addon to be installed? and would these variables h, w, ui still be accessible anywhere in the script? as I have a problem when I do the same and try to enable the add-on, although it works fine if I executed the script from the text editor. So if you could please advise? $\endgroup$
    – Tak
    Oct 31, 2016 at 1:58
  • $\begingroup$ I don't suppose there'd be any way to get access to the usual draw methods (.row(), .prop(), .operator(), etc) from such a custom panel? It'd be nice to be able to keep a panel of regularly used operators and properties always open somewhere, without having to tinker with bgl. $\endgroup$
    – Will Chen
    Jun 28, 2021 at 0:45
3
$\begingroup$

Simple explanation is it uses opengl drawing functions from the bgl module and font drawing from blf.

One of the templates included in blenders text editor is operator_modal_draw.py, it shows drawing text and using GL_LINE_STRIP to draw on the 3dview. Starting with that, then adjusting draw_callback_px() we can draw a poly for the background and a line loop to outline it then add some text. With a bit more work you can add rounded corners to make it look much nicer.

This is drawn while a modal operator is active, to get a button that expands like retopoflow you need to consider click locations and expand/collapse as desired. You could also track mouse drags and reposition the panel.

bl_info = {
    "name": "Floating demo",
    "author": "sambler",
    "version": (1,0),
    "blender": (2, 75, 0),
    "location": "view3D",
    "description": "Demo floating panel in the 3dview",
    "category": "User Interface",
}

import bpy
import bgl
import blf

def draw_poly(points):
    for i in range(len(points)):
            bgl.glVertex2f(points[i][0],points[i][1])

def draw_callback_px(self, context):
    panel_points = [[10.0, 10.0],
                    [10.0, 100.0],
                    [150.0, 100.0],
                    [150.0, 10.0],
                    ]

    # draw poly for floating panel
    bgl.glColor4f(0.3, 0.3, 0.3, 1.0)
    bgl.glEnable(bgl.GL_BLEND)
    bgl.glBegin(bgl.GL_POLYGON)
    draw_poly(panel_points)
    bgl.glEnd()

    # draw outline
    bgl.glColor4f(0.1, 0.1, 0.1, 1.0)
    bgl.glLineWidth(2)
    bgl.glBegin(bgl.GL_LINE_LOOP)
    draw_poly(panel_points)
    bgl.glEnd()

    font_id = 0
    # draw some text
    bgl.glColor4f(0.8, 0.8, 0.8, 1.0)
    blf.position(font_id, 15, 80, 0)
    blf.size(font_id, 14, 72)
    blf.draw(font_id, "Hello World")

    blf.position(font_id, 15, 50, 0)
    blf.draw(font_id, "I am floating")

    # restore opengl defaults
    bgl.glLineWidth(1)
    bgl.glDisable(bgl.GL_BLEND)
    bgl.glColor4f(0.0, 0.0, 0.0, 1.0)

class ModalFloatyOperator(bpy.types.Operator):
    """Draw a floating panel"""
    bl_idname = "view3d.modal_floaty_operator"
    bl_label = "Demo floating panel Operator"

    def modal(self, context, event):
        context.area.tag_redraw()

        if event.type == 'MOUSEMOVE':
            self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))

        elif event.type == 'LEFTMOUSE':
            bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
            return {'FINISHED'}

        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
            return {'CANCELLED'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        if context.area.type == 'VIEW_3D':
            # the arguments we pass the the callback
            args = (self, context)
            # Add the region OpenGL drawing callback
            # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
            self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')

            self.mouse_path = []

            context.window_manager.modal_handler_add(self)
            return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "View3D not found, cannot run operator")
            return {'CANCELLED'}


def register():
    bpy.utils.register_class(ModalFloatyOperator)


def unregister():
    bpy.utils.unregister_class(ModalFloatyOperator)

if __name__ == "__main__":
    register()
$\endgroup$
3
  • 1
    $\begingroup$ Nice explanation - thanks sambler! I should learn more about blf :) Just out of interest: Is there any reasonable approach in order to convert the font into a real button? $\endgroup$
    – p2or
    Nov 8, 2015 at 14:08
  • 1
    $\begingroup$ I doubt there is a simple way to just use blender's buttons inside this floating panel. The 3dview is an open space for drawing, all the interface buttons are designed to be used within interface specific regions. To make this work like a button you need to test if the mouse click is located within the panel/text area. $\endgroup$
    – sambler
    Nov 9, 2015 at 5:22
  • $\begingroup$ @batFINGER thanks - makes sense! I'll look into it and let you know :) $\endgroup$
    – p2or
    Nov 10, 2015 at 11:47

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .