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?
2 Answers
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')
-
$\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$– TakOct 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 withbgl
. $\endgroup$ Jun 28, 2021 at 0:45
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()
-
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$– p2orNov 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$– samblerNov 9, 2015 at 5:22
-
$\begingroup$ @batFINGER thanks - makes sense! I'll look into it and let you know :) $\endgroup$– p2orNov 10, 2015 at 11:47