Grease Pencil - QuickTool addon

Hello all.

I am creating an addon for the Grease Pencil interface to improve productivity. I frequently switch between draw, edit, and sculpt modes when creating a drawing and found that picking the mode from the menu is inefficient. So I put together a QuickTool addon for the Grease Pencil that allows toggling between modes with 1 click buttons below the toolbar.

Also included is a shortcut to “Sample Stroke” that allows for adding/removing vertices from the selected stroke with the middle mouse wheel.

My question is, how do I make the “button” icons match the button size in the toolbar?

Thanks in advance for your answers and feedback.

bl_info = {
    "name": "GPencil QuickTools",
    "author": "pongbuster",
    "version": (1, 0),
    "blender": (2, 80, 0),
    "location": "View3D > Toolbar",
    "description": "Adds shortcuts to toggle common modes to toolbar",
    "warning": "",
    "doc_url": "",
    "category": "Grease Pencil",
}

import bpy

class quickEditModeOperator(bpy.types.Operator):
    """
    Toggle edit mode
    """
    
    bl_idname = "stroke.editmode"
    bl_label = "Toggle Edit Mode"
    
    @classmethod
    def poll(self, context):
        if context.active_object == None: return False
        if context.active_object.type != 'GPENCIL': return False
        return context.mode != 'EDIT_GPENCIL'
    
    def execute(self, context):
        bpy.ops.gpencil.editmode_toggle()
        
        return {'FINISHED'}

class quickSculptOperator(bpy.types.Operator):    
    """
    Toggle sculpt mode
    """
    
    bl_idname = "stroke.sculptmode"
    bl_label = "Toggle Sculpt Mode"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(self, context):
        if context.active_object == None: return False
        if context.active_object.type != 'GPENCIL': return False
        return context.mode != 'SCULPT_GPENCIL'
    
    def execute(self, context):
        bpy.ops.gpencil.sculptmode_toggle()
        
        return {'FINISHED'}


class quickDrawOperator(bpy.types.Operator):    
    """
    Toggle draw mode
    """
    
    bl_idname = "stroke.drawmode"
    bl_label = "Toggle Draw Mode"
    bl_options = {'REGISTER', 'UNDO'}
    
    @classmethod
    def poll(self, context):
        if context.active_object == None: return False
        if context.active_object.type != 'GPENCIL': return False
        return context.mode != 'PAINT_GPENCIL'

    def execute(self, context):
        bpy.ops.gpencil.paintmode_toggle()
        
        return {'FINISHED'}

sample_length = 0

class quickStrokeSampleOperator(bpy.types.Operator):
    """
    Sample selected stroke with new points. Scroll mouse wheel to add / remove number of points.
    Left click to apply. Right click to cancel.
    """
    
    bl_idname = "stroke.sample"
    bl_label = "Sample Stroke"
    bl_options = {'REGISTER', 'UNDO'}
    
    @classmethod
    def poll(self, context):
        return (context.mode == 'SCULPT_GPENCIL' or context.mode == 'EDIT_GPENCIL')
    
    def modal(self, context, event):
        global sample_length

        if event.type == 'WHEELUPMOUSE':
            sample_length -= 0.01
            print("sample_length = " + str(sample_length))
            bpy.ops.gpencil.stroke_sample(length=sample_length)

        elif event.type == 'WHEELDOWNMOUSE':
            sample_length += 0.01
            print("sample_length = " + str(sample_length))
            bpy.ops.gpencil.stroke_sample(length=sample_length)

        elif event.type == "LEFTMOUSE":
            return {'FINISHED'}

        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            sample_length = 0.01
            bpy.ops.ed.undo()
            return {'CANCELLED'}

        return {'RUNNING_MODAL'}    
    
    def execute(self, context):
        global sample_length
        print("sample_length = " + str(sample_length))

        if sample_length < 0.0001:
        	sample_length = 0.01
            
        context.tool_settings.use_gpencil_select_mask_stroke=False
        context.tool_settings.use_gpencil_select_mask_point=True

        bpy.ops.gpencil.stroke_sample(length=sample_length)
        
        context.window_manager.modal_handler_add(self)
        
        return {'RUNNING_MODAL'}

class QUICK_PT_Tools(bpy.types.Panel):
    """QuickTools"""
    bl_label = "QuickTools"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'
    bl_options = {'HIDE_HEADER'}

    @classmethod 
    def poll(self, context):
        if len(context.selected_objects) == 0:
            return False

        if context.selected_objects[0].type == 'GPENCIL':
            return True

        return False
    
    def draw(self, context):
        layout = self.layout

        obj = context.object

        row = layout.row()
        row.operator(quickEditModeOperator.bl_idname, text="", icon = "EDITMODE_HLT")

        row = layout.row()
        row.operator(quickDrawOperator.bl_idname, text="", icon = "GPBRUSH_PENCIL")

        row = layout.row()
        row.operator(quickSculptOperator.bl_idname, text="", icon = "SCULPTMODE_HLT")
        
        layout.row().separator()
        row = layout.row()
        row.operator(quickStrokeSampleOperator.bl_idname, text="", icon = "STROKE")

# requires to register classes
from bpy.utils import register_class, unregister_class

# Class list to register
_classes = [
    quickEditModeOperator,
    quickDrawOperator,
    quickSculptOperator,
    quickStrokeSampleOperator,
    QUICK_PT_Tools
]

def register():
    for cls in _classes:
        register_class(cls)

def unregister():
    for cls in _classes:
        unregister_class(cls)

if __name__ == "__main__":
    register()

Hello brother , did you find any solution ?

I found that the sizing issue occurred in older versions of Blender. The icons in v3.3 are the same size as the main toolbar ones. Since it is working fine for my needs, I moved onto other projects.

Hello , I am on blender 3.3 but still facing the same problem .

I ended up not including a label and HIDE_HEADER in bl_options. This shows the tools in a panel which is the same width as the main toolbar because of the layout. The icons are still smaller but not as small. See the picture below.

I also tweaked the Quick Sample Stroke which I use all the time in sculpt mode. It is kind of like remeshing to restore point density.

bl_info = {
    "name": "GPencil QuickTools",
    "author": "pongbuster",
    "version": (1, 0),
    "blender": (2, 80, 0),
    "location": "View3D > Toolbar",
    "description": "Adds shortcuts to toggle common modes to toolbar",
    "warning": "",
    "doc_url": "",
    "category": "Grease Pencil",
}

import bpy
from bpy_extras import view3d_utils

class quickEditModeOperator(bpy.types.Operator):
    """
    Toggle edit mode
    """
    
    bl_idname = "stroke.editmode"
    bl_label = "Toggle Edit Mode"
    
    @classmethod
    def poll(self, context):
        if context.active_object == None: return False
        if context.active_object.type != 'GPENCIL': return False
        return context.mode != 'EDIT_GPENCIL'
    
    def execute(self, context):
        bpy.ops.gpencil.editmode_toggle()
        
        return {'FINISHED'}

class quickSculptOperator(bpy.types.Operator):    
    """
    Toggle sculpt mode
    """
    
    bl_idname = "stroke.sculptmode"
    bl_label = "Toggle Sculpt Mode"
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(self, context):
        if context.active_object == None: return False
        if context.active_object.type != 'GPENCIL': return False
        return context.mode != 'SCULPT_GPENCIL'
    
    def execute(self, context):
        bpy.ops.gpencil.sculptmode_toggle()
        
        return {'FINISHED'}


class quickDrawOperator(bpy.types.Operator):    
    """
    Toggle draw mode
    """
    
    bl_idname = "stroke.drawmode"
    bl_label = "Toggle Draw Mode"
    bl_options = {'REGISTER', 'UNDO'}
    
    @classmethod
    def poll(self, context):
        if context.active_object == None: return False
        if context.active_object.type != 'GPENCIL': return False
        return context.mode != 'PAINT_GPENCIL'

    def execute(self, context):
        bpy.ops.gpencil.paintmode_toggle()
        
        return {'FINISHED'}

g_sample_length = 0.01

class quickStrokeSampleOperator(bpy.types.Operator):
    """
    Sample selected stroke with new points. Scroll mouse wheel to add / remove number of points.
    Hold shift for higher detail. Left click to apply. Right click to cancel.
    """
    
    bl_idname = "stroke.sample"
    bl_label = "Sample Stroke"
    bl_options = {'REGISTER', 'UNDO'}

    sample_interval = 0.05
    sample_length_bak = g_sample_length
    
    @classmethod
    def poll(self, context):
        return (context.mode == 'SCULPT_GPENCIL' or context.mode == 'EDIT_GPENCIL')
    
    def modal(self, context, event):
        global g_sample_length

        if event.shift:
            self.sample_interval = 0.001
        else:
            self.sample_interval = 0.01

        if event.type == 'WHEELUPMOUSE':
            if g_sample_length - self.sample_interval > 0:
                g_sample_length -= self.sample_interval
            context.area.header_text_set("Sample Length: %.4f" % g_sample_length)
            bpy.ops.gpencil.stroke_sample(length=g_sample_length)

        elif event.type == 'WHEELDOWNMOUSE':
            g_sample_length += self.sample_interval
            context.area.header_text_set("Sample Length: %.4f" % g_sample_length)
            bpy.ops.gpencil.stroke_sample(length=g_sample_length)

        elif event.type == "LEFTMOUSE":
            context.area.header_text_set(None)
            return {'FINISHED'}

        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            g_sample_length = self.sample_length_bak
            context.area.header_text_set(None)
            bpy.ops.ed.undo()
            return {'CANCELLED'}
       
        return {'RUNNING_MODAL'}    
    
    def execute(self, context):
        global g_sample_length
        
        context.area.header_text_set("Sample Length: %.4f" % g_sample_length)
            
        context.tool_settings.use_gpencil_select_mask_stroke=False
        context.tool_settings.use_gpencil_select_mask_point=True

        bpy.ops.gpencil.stroke_sample(length=g_sample_length)
        
        context.window_manager.modal_handler_add(self)
        
        return {'RUNNING_MODAL'}

class QUICK_PT_Tools(bpy.types.Panel):
    """QuickTools"""
    bl_label = ""
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'
    #bl_options = {'HIDE_HEADER'}

    @classmethod 
    def poll(self, context):
        return True
        if context.active_object:
            return context.active_object.type == 'GPENCIL'

        return False
    
    def draw(self, context):
        layout = self.layout

        obj = context.object

        row = layout.row()
        row.operator(quickEditModeOperator.bl_idname, text="", icon = "EDITMODE_HLT")

        row = layout.row()
        row.operator(quickDrawOperator.bl_idname, text="", icon = "GPBRUSH_PENCIL")

        row = layout.row()
        row.operator(quickSculptOperator.bl_idname, text="", icon = "SCULPTMODE_HLT")
        
        layout.row().separator()
        row = layout.row()
        row.operator(quickStrokeSampleOperator.bl_idname, text="", icon = "STROKE")

        # here is your operator


# requires to register classes
from bpy.utils import register_class, unregister_class

# Class list to register
_classes = [
    quickEditModeOperator,
    quickDrawOperator,
    quickSculptOperator,
    quickStrokeSampleOperator,
    QUICK_PT_Tools
]

def register():
    for cls in _classes:
        register_class(cls)

def unregister():
    for cls in _classes:
        unregister_class(cls)

if __name__ == "__main__":
    register()

Actually it looks like the icons are the same size after all. The question remains. How to make the icons the same size as the main toolbar while having the “Tool Text” (i.e. Sculpt mode, Edit mode…etc) defined as well.