OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / ui_framework / operator_ui / __init__.py

602 lines · python ref:
import bpy, gpu, time
from mathutils import Vector
from gpu_extras.batch import batch_for_shader

from ..graphics.draw import render_text, render_quad, draw_border_lines
from ..utils.geo import get_blf_text_dims

from ... utility import addon
from ...utility import method_handler
from ...utils.blender_ui import get_dpi_factor


HANDLE = None
SPACE = None
ACTIVE_UI_TYPE = None


class Data():
    '''Global communications from operator to ui modal.'''
    # Fade Variables
    start_time = 0
    time_difference = 0
    hold_complete = False

    # Transitions
    active_count = 0

    # Display Data
    draw_data = []

    draw_bg = False
    draw_border = False
    compact_mode = False

    # Trigger a global reset on the running modal
    reset_all = False

    # Bad space
    close_now = False


class Master():
    '''Used from the modal to launch the drawing.'''

    def receive_draw_data(self, draw_data=[]):
        Data.draw_data = draw_data


    def draw(self, draw_bg=False, draw_border=False, compact_mode=False):

        global HANDLE, SPACE, ACTIVE_UI_TYPE
        context = bpy.context

        ACTIVE_UI_TYPE = context.area.ui_type

        if context.area.ui_type in {'GeometryNodeTree', 'ShaderNodeTree'}:
            if SPACE != bpy.types.SpaceNodeEditor:
                if HANDLE:
                    HANDLE = SPACE.draw_handler_remove(HANDLE, "WINDOW")
                SPACE = bpy.types.SpaceNodeEditor

        elif context.area.ui_type == 'VIEW_3D':
            if SPACE != bpy.types.SpaceView3D:
                if HANDLE:
                    HANDLE = SPACE.draw_handler_remove(HANDLE, "WINDOW")
                SPACE = bpy.types.SpaceView3D

        else:
            if SPACE and HANDLE:
                SPACE.draw_handler_remove(HANDLE, "WINDOW")
            HANDLE = None
            SPACE = None
            if Data.active_count > 0:
                Data.close_now = True
            return

        Data.close_now = False

        prefs = addon.preference()

        if prefs.ui.Hops_operator_display:
            Data.draw_bg = draw_bg
            Data.draw_border = draw_border
            Data.compact_mode = compact_mode

            # Reuse the same modal check
            if Data.active_count == 0:
                Data.active_count += 1
                Data.reset_all = False
                bpy.ops.hops.ui_draw('INVOKE_DEFAULT')
            else:
                Data.reset_all = True


class HOPS_OT_UI_Draw(bpy.types.Operator):
    '''UI Modal'''

    bl_idname = "hops.ui_draw"
    bl_label = "Drawing for operators"
    bl_options = {"INTERNAL"}


    def start(self):
        self.shader = None
        self.timer = None
        self.prefs = addon.preference()
        self.first_draw = False


    def invoke(self, context, event):
        self.start()
        # Globals
        Data.start_time = time.time()
        Data.hold_complete = False
        Data.time_difference = 0

        # Fail Safe
        self.start_time = time.time()
        self.max_time = self.prefs.ui.Hops_operator_display_time + self.prefs.ui.Hops_operator_display_time + 3

        #UI System
        self.shader = Shader(context)
        self.timer = context.window_manager.event_timer_add(0.025, window=context.window)

        context.window_manager.modal_handler_add(self)

        return {'RUNNING_MODAL'}


    def modal(self, context, event):

        # Called from bad space : remove self
        if Data.close_now:
            Data.close_now = False
            self.finished(context)
            return {'FINISHED'}

        # Reuse this modal if a new draw data is invoked
        if Data.reset_all == True:
            Data.reset_all = False
            Data.start_time = time.time()
            Data.hold_complete = False
            Data.time_difference = 0
            self.start_time = time.time()
            self.max_time = self.prefs.ui.Hops_operator_display_time + self.prefs.ui.Hops_operator_display_time + 3
            self.first_draw = False
            self.shader.setup_handle(context)

        # Fail Safe
        if time.time() - self.start_time > self.max_time:
            self.finished(context)
            return {'FINISHED'}

        # Get the time difference
        Data.time_difference = time.time() - Data.start_time

        # Remove modal if another starts
        if Data.active_count > 1:
            self.finished(context)
            return {'FINISHED'}

        # Hold the UI on screen until display time is completed
        if Data.hold_complete == False:

            if Data.time_difference >= self.prefs.ui.Hops_operator_display_time:
                Data.hold_complete = True
                Data.time_difference = 0
                Data.start_time = time.time()

            elif Data.time_difference <= self.prefs.ui.Hops_operator_display_time:

                # Redraw the viewport
                if context.area != None:
                    if self.first_draw == False:
                        self.first_draw = True
                        context.area.tag_redraw()
                return {'PASS_THROUGH'}

        # Fade is finished
        if Data.time_difference > self.prefs.ui.Hops_operator_fade:
            self.finished(context)
            return {'FINISHED'}

        # Redraw the viewport
        if context.area != None:
            context.area.tag_redraw()

        return {'PASS_THROUGH'}


    def finished(self, context):
        '''Remove the timer, shader, and reset Data'''

        Data.time_difference = 0
        Data.active_count -= 1

        if self.timer != None:
            context.window_manager.event_timer_remove(self.timer)

        if self.shader != None:
            self.shader.destroy()
            del self.shader

        if context.area != None:
            context.area.tag_redraw()


class Shader():

    def __init__(self, context):
        self.context = context
        self.banner = Banner(context)
        self.setup_handle(context)


    def setup_handle(self, context):
        '''Setup the draw handle for the UI'''

        global HANDLE, SPACE
        # Error
        if SPACE == None:
            return
        # Create new handle
        if HANDLE == None:
            HANDLE = SPACE.draw_handler_add(self.safe_draw, (context, ), "WINDOW", "POST_PIXEL")


    def safe_draw(self, context):
        method_handler(self.draw,
            arguments = (context,),
            identifier = 'UI Framework',
            exit_method = self.destroy)


    def draw(self, context):
        global ACTIVE_UI_TYPE
        if context.area.ui_type != ACTIVE_UI_TYPE:
            return

        self.banner.draw()


    def destroy(self):

        global HANDLE, SPACE, ACTIVE_UI_TYPE

        if SPACE == None:
            HANDLE = None

        if HANDLE != None:
            HANDLE = SPACE.draw_handler_remove(HANDLE, "WINDOW")
            SPACE = None
            ACTIVE_UI_TYPE = None

            if self.banner:
                del self.banner
            return True

        else:
            return False


class Banner():
    '''Drawing class for operator UI.'''

    def __init__(self, context):

        self.context = context
        self.prefs = addon.preference()
        self.scale_factor = get_dpi_factor()
        self.setup_colors()

        # Font sizes
        self.header_font_size = 24
        self.body_font_size = 14

        # Font dims
        self.header_font_dims = get_blf_text_dims(text="Sample", size=self.header_font_size)
        self.body_font_dims = get_blf_text_dims(text="Sample", size=self.body_font_size)

        # Paddings
        self.text_padding = 6 * self.scale_factor
        self.column_gap = 150 * self.scale_factor
        self.side_padding = 15 * self.scale_factor
        self.padding_to_bottom_line = 10 * self.scale_factor
        self.line_width = 2

        # Offsets
        self.prefs_offset = (
            self.prefs.ui.Hops_operator_ui_offset[0] * self.scale_factor,
            self.prefs.ui.Hops_operator_ui_offset[1] * self.scale_factor)

        self.height_to_bottom = self.prefs_offset[1]

        # Totals
        self.total_width = self.get_total_width()
        self.total_height = self.get_total_height()
        self.left_offset = (self.context.area.width * .5) - (self.total_width * .5) + self.prefs_offset[0]

    #######################################
    # SETUPS
    #######################################

    def get_total_width(self):
        '''Get the total wdth of the box.'''

        # Get the widest text width
        if Data.draw_data == []: return

        # Only containts header
        if len(Data.draw_data) == 1:
            text = Data.draw_data[0][0]
            return get_blf_text_dims(text=text, size=self.header_font_size)[0] + self.side_padding * 2


        if Data.compact_mode:
            self.column_gap = 40 * self.scale_factor

            max_width = 0
            compare = 0
            for i, data in enumerate(Data.draw_data):
                # Set first
                if i == 0:
                    if type(data) == str:
                        max_width = get_blf_text_dims(text=data, size=self.header_font_size)[0]
                    else:
                        total = 0
                        for item in data:
                            if type(item) == str:
                                total += get_blf_text_dims(text=item, size=self.header_font_size)[0]
                        max_width = total
                    compare = max_width
                    continue

                # See if the descriptions are wider
                if type(data) == str:
                    compare = get_blf_text_dims(text=data, size=self.body_font_size)[0] + self.column_gap
                else:
                    total = 0
                    for item in data:
                        if type(item) == str:
                            total += get_blf_text_dims(text=item, size=self.body_font_size)[0]
                    total += self.column_gap
                    compare = total

                if compare > max_width:
                    max_width = compare

            return max_width + self.side_padding * 2

        # Header plus other data
        longest = ""
        for i in range(len(Data.draw_data)):

            # Only one item in list
            if len(Data.draw_data[i]) == 1:
                if len(Data.draw_data[i][0]) > len(longest):
                    longest = Data.draw_data[i][0]

            # Two items in list
            elif len(Data.draw_data[i]) == 2:
                compare = str(Data.draw_data[i][0]) + str(Data.draw_data[i][1])
                if len(compare) > len(longest):
                    longest = compare

        return get_blf_text_dims(text=str(longest), size=self.body_font_size)[0] + self.column_gap


    def get_total_height(self):
        '''Get the total height of the box.'''

        # Single line label only
        if len(Data.draw_data) == 1:
            return self.header_font_dims[1] + self.text_padding * 3

        # Full label
        else:

            # For each text add padding for top
            txt_padding_total = self.text_padding * (len(Data.draw_data) - 2)

            # Both divisor lines
            divisor_total = self.line_width * 2
            divisor_total += self.text_padding * 2
            divisor_total += self.padding_to_bottom_line * 2

            # All the text heights
            text_height_total = self.header_font_dims[1]
            text_height_total += self.body_font_dims[1] * (len(Data.draw_data) - 1)

            return text_height_total + txt_padding_total + divisor_total

    #######################################
    # COLORS
    #######################################

    def setup_colors(self):

        # Original
        self.prefs_colors = []
        self.prefs_colors.append(self.prefs.color.Hops_UI_text_color)
        self.prefs_colors.append(self.prefs.color.Hops_UI_secondary_text_color)
        self.prefs_colors.append(self.prefs.color.Hops_UI_text_highlight_color)
        self.prefs_colors.append(self.prefs.color.Hops_UI_cell_background_color)
        self.prefs_colors.append(self.prefs.color.Hops_UI_border_color)


    def set_colors(self):

        self.Hops_UI_text_color            = Vector((self.prefs_colors[0][0], self.prefs_colors[0][1], self.prefs_colors[0][2], self.prefs_colors[0][3]))
        self.Hops_UI_secondary_text_color  = Vector((self.prefs_colors[1][0], self.prefs_colors[1][1], self.prefs_colors[1][2], self.prefs_colors[1][3]))
        self.Hops_UI_text_highlight_color  = Vector((self.prefs_colors[2][0], self.prefs_colors[2][1], self.prefs_colors[2][2], self.prefs_colors[2][3]))
        self.Hops_UI_cell_background_color = Vector((self.prefs_colors[3][0], self.prefs_colors[3][1], self.prefs_colors[3][2], self.prefs_colors[3][3]))
        self.Hops_UI_border_color          = Vector((self.prefs_colors[4][0], self.prefs_colors[4][1], self.prefs_colors[4][2], self.prefs_colors[4][3]))


    def handle_fade(self):

        if Data.hold_complete == True:
            if self.prefs.ui.Hops_operator_fade > 0:

                original_alpha = self.prefs_colors[0][3]
                alpha = self.Hops_UI_text_color[3] - ((original_alpha / self.prefs.ui.Hops_operator_fade) * Data.time_difference)
                self.Hops_UI_text_color = Vector((self.prefs_colors[0][0], self.prefs_colors[0][1], self.prefs_colors[0][2], alpha))

                original_alpha = self.prefs_colors[1][3]
                alpha = self.Hops_UI_secondary_text_color[3] - ((original_alpha / self.prefs.ui.Hops_operator_fade) * Data.time_difference)
                self.Hops_UI_secondary_text_color = Vector((self.prefs_colors[1][0], self.prefs_colors[1][1], self.prefs_colors[1][2], alpha))

                original_alpha = self.prefs_colors[2][3]
                alpha = self.Hops_UI_text_highlight_color[3] - ((original_alpha / self.prefs.ui.Hops_operator_fade) * Data.time_difference)
                self.Hops_UI_text_highlight_color  = Vector((self.prefs_colors[2][0], self.prefs_colors[2][1], self.prefs_colors[2][2], alpha))

                original_alpha = self.prefs_colors[3][3]
                alpha = self.Hops_UI_cell_background_color[3] - ((original_alpha / self.prefs.ui.Hops_operator_fade) * Data.time_difference)
                self.Hops_UI_cell_background_color = Vector((self.prefs_colors[3][0], self.prefs_colors[3][1], self.prefs_colors[3][2], alpha))

                original_alpha = self.prefs_colors[4][3]
                alpha = self.Hops_UI_border_color[3] - ((original_alpha / self.prefs.ui.Hops_operator_fade) * Data.time_difference)
                self.Hops_UI_border_color = Vector((self.prefs_colors[4][0], self.prefs_colors[4][1], self.prefs_colors[4][2], alpha))

    #######################################
    # DRAW
    #######################################

    def draw(self):

        # Resets
        self.height_to_bottom = self.prefs_offset[1]
        self.total_width = self.get_total_width()
        self.total_height =self.get_total_height()
        self.left_offset = (self.context.area.width * .5) - (self.total_width * .5) + self.prefs_offset[0]

        if Data.draw_data != []:

            # Colors
            self.set_colors()
            self.handle_fade()

            # Draw header and lines
            if len(Data.draw_data) > 1:
                self.draw_background()
                self.draw_divisor_line()
                self.draw_body_text()
                self.draw_divisor_line()
                self.draw_header_text()

            # Draw only header
            else:
                self.draw_background()
                self.draw_header_text()


    def draw_background(self):

        # Setup for background and border : TL, BL, TR, BR
        quad = (
            (self.left_offset                   , self.height_to_bottom + self.total_height),
            (self.left_offset                   , self.height_to_bottom),
            (self.left_offset + self.total_width, self.height_to_bottom + self.total_height),
            (self.left_offset + self.total_width, self.height_to_bottom))

        # Draw background
        if Data.draw_bg:
            render_quad(quad=quad,
                        color=self.Hops_UI_border_color,
                        bevel_corners=True)

        # Draw border
        if Data.draw_border:
            draw_border_lines(vertices=quad, width=self.prefs.ui.Hops_operator_border_size, color=self.Hops_UI_cell_background_color)


    def draw_divisor_line(self):

        left_point  = (self.left_offset + self.side_padding                   , self.height_to_bottom + self.padding_to_bottom_line)
        right_point = (self.left_offset - self.side_padding + self.total_width, self.height_to_bottom + self.padding_to_bottom_line)

        self.height_to_bottom += self.line_width + self.text_padding + self.padding_to_bottom_line

        draw_divider_lines(
            vertices=[left_point, right_point],
            width=self.line_width,
            color=self.Hops_UI_cell_background_color,
            bevel_corners=False,
            format_lines=False)


    def draw_body_text(self):

        # Text items for the body
        for index, item in enumerate(Data.draw_data[1:]):
            # Left or Right
            count = 0
            for string in item:

                if type(string) == float:
                    string = "{0:.3f}".format(string)
                string = str(string)

                # Left Side
                if count == 0:
                    pos = (self.left_offset + self.side_padding, self.height_to_bottom)
                    render_text(
                        text=string,
                        position=pos,
                        size=self.body_font_size,
                        color=self.Hops_UI_text_color)

                # Right Side
                elif count == 1:
                    text_dims = get_blf_text_dims(text=string, size=self.body_font_size)
                    pos = (self.left_offset + self.total_width - text_dims[0] - self.side_padding, self.height_to_bottom)
                    render_text(
                        text=string,
                        position=pos,
                        size=self.body_font_size,
                        color=self.Hops_UI_secondary_text_color)

                count += 1

            self.height_to_bottom += self.body_font_dims[1] + self.text_padding
        self.height_to_bottom -= self.text_padding * 2


    def draw_header_text(self):

        string = Data.draw_data[0][0]
        pos = (self.left_offset + self.side_padding, self.height_to_bottom)

        # Single line label only
        if len(Data.draw_data) == 1:
            pos = (self.left_offset + self.side_padding, self.height_to_bottom + self.text_padding * 2)

        render_text(
            text=string,
            position=pos,
            size=self.header_font_size,
            color=self.Hops_UI_secondary_text_color)


def draw_divider_lines(vertices=[], width=1, color=(0,0,0,1), bevel_corners=True, format_lines=False):
    '''Render quads passed in. \n
        Top Left, Bottom Left, Top Right, Bottom Right
    '''

    built_in_shader = 'UNIFORM_COLOR' if bpy.app.version[0] >=4 else '2D_UNIFORM_COLOR'
    shader = gpu.shader.from_builtin(built_in_shader)
    #Enable(GL_LINE_SMOOTH)
    gpu.state.blend_set('ALPHA')
    gpu.state.line_width_set(width)
    batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices})
    shader.bind()
    shader.uniform_float("color", color)
    batch.draw(shader)

    gpu.state.line_width_set(width)
    gpu.state.blend_set('NONE')

    del shader
    del batch


from bpy.app.handlers import persistent
@persistent
def remove_op_draw_shader(dummy):
    global HANDLE, SPACE, ACTIVE_UI_TYPE
    if HANDLE != None:
        HANDLE = SPACE.draw_handler_remove(HANDLE, "WINDOW")
        SPACE = None
        ACTIVE_UI_TYPE = None

    # Reset draw data
    Data.start_time = 0
    Data.time_difference = 0
    Data.hold_complete = False
    Data.active_count = 0
    Data.draw_data = []
    Data.draw_bg = False
    Data.draw_border = False
    Data.reset_all = False