OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / ui_framework / flow_ui / flow.py

426 lines · python ref:
import bpy
from ..utils.geo import get_blf_text_dims
from ..utils.checks import is_mouse_in_quad
from ..graphics.draw import render_quad, render_text, draw_border_lines, draw_2D_lines, render_image
from ..graphics.load import load_image_file
from ...utility.screen import dpi_factor
from ... utility import addon


####################################################
#   FLOW SYSTEM
####################################################

class Flow_Menu:
    '''Flow menu system.'''

    def __init__(self):
        # Event
        self.is_open = False
        self.exit_padding = 18 * dpi_factor(min=.25)

        # Data
        self.elements = []
        self.bounds = Bounds()

        # Draw Settings
        self.Hops_UI_cell_background_color = addon.preference().color.Hops_UI_cell_background_color
        self.Hops_UI_border_color = addon.preference().color.Hops_UI_border_color
        
        # Draw Data
        self.icon = load_image_file('logo_red')
        self.icon_size = 24 * dpi_factor(min=.25)

    # Setup flow
    def setup_flow_data(self, flow_data):
        '''Create all the elements.'''

        # Clear
        self.elements = []
        DATA.active_ref_key = None

        flow_data.reverse()

        # Create elements
        for index, item in enumerate(flow_data):
            element = Element(item)
            element.ref_key = index
            element.flow_form = item
            self.elements.append(element)

    # Modal func
    def run_updates(self, context, event, close_on_click=False, enable_tab_open=False):
        '''Modal func to handle window.'''

        if self.check_for_open(event, enable_tab_open):
            if self.is_open == False:
                self.open_menu(context, event)

        if self.is_open:
            self.__update(context, event, close_on_click)

    # Check if should open window
    def check_for_open(self, event, enable_tab_open):
        '''Check if the window should open.'''

        # Check for shift click
        check_events = {'SPACE'} if enable_tab_open == False else {'SPACE', 'TAB'}
        if event.type in check_events and event.value == 'PRESS':
            if event.shift == True:
                return True
            if event.alt == True:
                return True
            if enable_tab_open:
                return True

        # if event.type == 'LEFTMOUSE' and event.value == 'PRESS':
        #     if event.alt == True:
        #         if event.shift == False:
        #             if event.ctrl == False:
        #                 return True
        return False

    # Open window
    def open_menu(self, context, event):
        '''Update the window system.'''

        self.is_open = True
        DATA.active_ref_key = None
        mouse_pos = (event.mouse_region_x, event.mouse_region_y)

        # Setup window bounds
        # Get all the dims off each element
        all_dims = []
        for element in self.elements:
            dims = element.get_max_dims()
            all_dims.append(dims)

        # Get max dims
        max_width = 0
        max_height = 0
        for dims in all_dims:
            if dims[0] > max_width:
                max_width = dims[0]
            max_height += dims[1]

        # Assign flow window bounds
        self.bounds.bot_left  = (mouse_pos[0]            , mouse_pos[1] - max_height)
        self.bounds.bot_right = (mouse_pos[0] + max_width, mouse_pos[1] - max_height)
        self.bounds.top_left  = (mouse_pos[0]            , mouse_pos[1])
        self.bounds.top_right = (mouse_pos[0] + max_width, mouse_pos[1])

        # Setup element bounds
        y_offset = 0
        for element in self.elements:
            element.bounds.bot_left  = (self.bounds.bot_left[0] , self.bounds.bot_left[1] + y_offset)
            element.bounds.bot_right = (self.bounds.bot_right[0], self.bounds.bot_left[1] + y_offset)
            element.bounds.top_left  = (self.bounds.bot_left[0] , self.bounds.bot_left[1] + y_offset + element.max_dims[1])
            element.bounds.top_right = (self.bounds.bot_right[0], self.bounds.bot_left[1] + y_offset + element.max_dims[1])
            y_offset += element.max_dims[1]

        self.__update(context, event)

    # Update flow while open
    def __update(self, context, event, close_on_click=False):
        '''Update the flow system.'''

        mouse_pos = (event.mouse_region_x, event.mouse_region_y)
        clicked = True if event.type == 'LEFTMOUSE' and event.value == 'PRESS' else False

        # Setup
        for element in self.elements:
            element.update()

        # Event
        event_fired = False
        for element in self.elements:
            if element.event(event):
                event_fired = True
                break

        # Mouse left the window : Close
        quad = (
            self.bounds.top_left,
            self.bounds.bot_left,
            self.bounds.top_right,
            self.bounds.bot_right)

        if is_mouse_in_quad(quad=quad, mouse_pos=mouse_pos, tolerance=self.exit_padding) == False:
            self.is_open = False
            DATA.active_ref_key = None

        if close_on_click == True:
            if event_fired:
                self.is_open = False
                DATA.active_ref_key = None

    # Remove old data
    def shut_down(self):
        '''Remove images and clean up.'''

        if self.icon != None:
            try:
                bpy.data.images.remove(self.icon)
            except:
                pass

    # Draw flow while open
    def draw_2D(self):
        '''Draw all the elements.'''

        if self.is_open == False:
            return

        # Draw background
        render_quad(
            quad=(
                self.bounds.top_left,
                self.bounds.bot_left,
                self.bounds.top_right,
                self.bounds.bot_right),
            color=self.Hops_UI_cell_background_color,
            bevel_corners=True)

        draw_border_lines(
            vertices=[
                self.bounds.top_left,
                self.bounds.bot_left,
                self.bounds.top_right,
                self.bounds.bot_right],
            width=1,
            color=self.Hops_UI_border_color,
            bevel_corners=True,
            format_lines=False)

        # Draw elements
        for element in self.elements:
            element.draw()

        # Draw the icon
        if self.icon != None:
            verts = [
                (self.bounds.top_left[0] - self.icon_size, self.bounds.top_left[1] - self.icon_size),
                (self.bounds.top_left[0]                 , self.bounds.top_left[1] - self.icon_size),
                (self.bounds.top_left[0]                 , self.bounds.top_left[1]),
                (self.bounds.top_left[0] - self.icon_size, self.bounds.top_left[1])]
            render_image(self.icon, verts)

####################################################
#   DATA OBJECTS
####################################################

class DATA:
    active_ref_key = None


class Bounds:
    '''Generic dimension bounds.'''

    def __init__(self):
        self.bot_left  = (0,0)
        self.bot_right = (0,0)
        self.top_left  = (0,0)
        self.top_right = (0,0)


class Flow_Form:
    '''Controls flow menu elements.'''

    def __init__(self, text="", font_size=16, func=None, pos_args=None, neg_args=None, tip_box=""):
        self.text = text
        self.font_size = font_size
        self.func = func
        self.pos_args = pos_args
        self.neg_args = neg_args
        self.tip_box = tip_box

####################################################
#   ELEMENTS
####################################################

class Element:
    '''Text box element.'''

    def __init__(self, flow_form):
        self.flow_form = flow_form
        self.ref_key = 0
        self.font_pos = (0,0)
        self.font_color = addon.preference().color.Hops_UI_secondary_text_color
        self.font_padding = 12 * dpi_factor(min=.25)
        self.bounds = Bounds()
        self.max_dims = (0,0)
        # BG
        self.show_bg = False
        self.Hops_UI_mouse_over_color = addon.preference().color.Hops_UI_mouse_over_color
        self.Hops_UI_border_color = addon.preference().color.Hops_UI_border_color
        # Event
        self.mouse_pos = (0,0)
        # Tip Box
        self.tip_font_size = 12
        self.show_tip = False
        self.tip_bounds = Bounds()
        self.tip_list = []


    def get_max_dims(self):
        '''Get a tuple of the elements dims.'''

        text_dims = get_blf_text_dims(self.flow_form.text, self.flow_form.font_size)
        self.max_dims = (
            text_dims[0] + self.font_padding * 2,
            text_dims[1] + self.font_padding * 2)
        return self.max_dims


    def update(self):
        '''Update the elements dims.'''

        center_x = self.bounds.bot_right[0] - self.bounds.bot_left[0]
        center_x = (center_x * .5) + self.bounds.bot_left[0]

        center_y = (self.bounds.top_right[1] - self.bounds.bot_right[1])
        center_y = (center_y * .5) + self.bounds.bot_right[1]

        text_dims = get_blf_text_dims(self.flow_form.text, self.flow_form.font_size)

        self.font_pos = (
            center_x - (text_dims[0] * .5),
            center_y - (text_dims[1] * .5)) 


    def event(self, event):
        '''Run the event update on this element.'''

        # There is no call back
        if self.flow_form.func == None:
            if self.flow_form.tip_box == "":
                return

        # Mouse is over cell
        self.mouse_pos = (event.mouse_region_x, event.mouse_region_y)
        quad = (
            self.bounds.top_left,
            self.bounds.bot_left,
            self.bounds.top_right,
            self.bounds.bot_right)

        mouse_is_inside = False
        if is_mouse_in_quad(quad=quad, mouse_pos=self.mouse_pos):
            mouse_is_inside = True

            # Decide what to draw
            if self.flow_form.func != None:
                self.show_bg = True
            if self.flow_form.tip_box != "":
                self.show_tip = True
                self.calculate_tip_box()
        else:
            self.show_bg = False
            self.show_tip = False

        # Call backs
        if self.flow_form.func != None:
            if mouse_is_inside:

                # Click event
                if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
                    if self.flow_form.pos_args != None:
                        self.flow_form.func(*self.flow_form.pos_args)
                        return True
                    else:
                        self.flow_form.func()
                        return True

        return False

    # Called from inside event on this element
    def calculate_tip_box(self):
        '''Setup tip box for rendering.'''

        self.tip_list = self.flow_form.tip_box.split(';')

        height = 0
        width = 0

        for tip in self.tip_list:
            text_dims = get_blf_text_dims(tip, self.tip_font_size)
            height += text_dims[1] + self.font_padding
            if text_dims[0] > width:
                width = text_dims[0]
        height += self.font_padding
        width += self.font_padding * 2

        # This box is to the right of the active box + font padding : the top is flush with the active box
        self.tip_bounds.top_left =  (self.font_padding + self.bounds.top_right[0]        , self.bounds.top_right[1])
        self.tip_bounds.top_right = (self.font_padding + self.bounds.top_right[0] + width, self.bounds.top_right[1])
        self.tip_bounds.bot_left =  (self.font_padding + self.bounds.top_right[0]        , self.bounds.top_right[1] - height)
        self.tip_bounds.bot_right = (self.font_padding + self.bounds.top_right[0] + width, self.bounds.top_right[1] - height)


    def draw(self):
        '''Draw the element.'''

        if self.show_bg == True:
            self.draw_bg()

        if self.show_tip == True:
            self.draw_tip_box()

        # Draw label
        render_text(self.flow_form.text, self.font_pos, self.flow_form.font_size, self.font_color)


    def draw_bg(self):
        '''If show bg is true draw highlight.'''

        # Draw background
        render_quad(
            quad=(
                self.bounds.top_left,
                self.bounds.bot_left,
                self.bounds.top_right,
                self.bounds.bot_right),
            color=self.Hops_UI_mouse_over_color,
            bevel_corners=True)

        draw_border_lines(
            vertices=[
                self.bounds.top_left,
                self.bounds.bot_left,
                self.bounds.top_right,
                self.bounds.bot_right],
            width=1,
            color=self.Hops_UI_border_color,
            bevel_corners=True,
            format_lines=False)


    def draw_tip_box(self):
        '''If flow form has hover box text.'''

        render_quad(
            quad=(
                self.tip_bounds.top_left,
                self.tip_bounds.bot_left,
                self.tip_bounds.top_right,
                self.tip_bounds.bot_right),
            color=self.Hops_UI_mouse_over_color,
            bevel_corners=True)

        draw_border_lines(
            vertices=[
                self.tip_bounds.top_left,
                self.tip_bounds.bot_left,
                self.tip_bounds.top_right,
                self.tip_bounds.bot_right],
            width=1,
            color=self.Hops_UI_border_color,
            bevel_corners=True,
            format_lines=False)

        # Draw all the text
        pos_x = self.tip_bounds.bot_left[0] + self.font_padding
        pos_y = self.tip_bounds.bot_left[1] + self.font_padding
        for tip in reversed(self.tip_list):
            render_text(tip, (pos_x, pos_y), self.tip_font_size, self.font_color)
            pos_y += get_blf_text_dims(tip, self.tip_font_size)[1] + self.font_padding