OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / ui_framework / form_ui / elements / input.py

485 lines · python ref:
import bpy, time, math, numpy
from math import sin
from mathutils import Vector
from ....utility.screen import dpi_factor
from ... graphics.load import load_image_file
from ... graphics.draw import render_text, draw_border_lines, render_quad, draw_2D_lines
from ... utils.geo import get_blf_text_dims
from ... utils.checks import is_mouse_in_quad
from . import Dims, Tips


class Taps:
    def __init__(self, font_color, bg_color, border_color, tag=""):
        self.dims = Dims()
        self.font_color = font_color
        self.bg_color = bg_color
        self.border_color = border_color
        self.tag = tag
        self.verts = []


    def draw(self):
        render_quad(self.dims.quad(), color=self.bg_color)
        draw_border_lines(self.dims.quad(), color=self.border_color)
        draw_2D_lines(self.verts, width=2, color=self.font_color)


'''
decimal_draw_precision
    - pass an INT in this param to clamp the drawing percision (doesnt effect the actual setter)
handle_radians
    - when true, this will display the values to the screen in degrees (does not alter the increment)
attr_index
    - if the item is a list item use this to specify the slot to get / set
'''


class Input:
    def __init__(self,
        obj=None, attr="", font_size=12, width=50, height=0,
        tips=None, tip_size=12, limit=6, increment=.5, use_mod_keys=True,
        on_active_callback=None, ctrl_scroll_callback=None,
        decimal_draw_precision=None, handle_radians=False,
        attr_index=None):

        self.dims = Dims()
        self.font_color = (0,0,0,1)
        self.bg_color = (0,0,0,1)
        self.border_color = (0,0,0,1)
        self.obj = obj
        self.attr = attr
        self.font_size = font_size
        self.width = width * dpi_factor(min=0.5)
        self.height = height * dpi_factor(min=0.5)
        self.limit = limit
        self.increment = increment
        self.use_mod_keys = use_mod_keys
        self.text = ""
        self.tips = Tips(tips, tip_size) if tips else None
        self.mouse_over = False
        self.on_active_callback = on_active_callback
        self.ctrl_scroll_callback = ctrl_scroll_callback
        self.decimal_draw_precision = decimal_draw_precision
        self.handle_radians = handle_radians
        self.attr_index = attr_index
        # Drag
        self.drag_was_used = False
        self.test_for_drag = True
        self.click_loc = (0,0)
        self.track_dist = 0
        self.drag_dist = 0
        # Taps
        self.taps = []
        self.using_taps = False
        self.shift_pressed_on_entry = False
        # Double Click
        self.click_time = 0
        self.double_click_duration = .25
        # Notify
        self.timer = None
        self.locked = False
        self.alpha = 1
        # Input
        self.entry_string = ""
        self.tabbed_finish = False


    def build(self, db, x_offset, h_offset):
        self.font_color = db.color.text
        self.bg_color = db.color.cell_background
        self.border_color = db.color.border

        if self.obj == None or self.attr == "": return

        self.text = self.__get_value(use_convert=True, as_string=True)
        dims = get_blf_text_dims(self.text, self.font_size)
        pad = self.dims.padding

        self.dims.max_width = self.width
        if self.height: self.dims.max_height = self.height
        else: self.dims.max_height = dims[1] + pad * 2

        self.dims.bot_left  = (x_offset, h_offset)
        self.dims.top_left  = (x_offset, h_offset + self.dims.max_height)
        self.dims.top_right = (x_offset + self.dims.max_width, h_offset + self.dims.max_height)
        self.dims.bot_right = (x_offset + self.dims.max_width, h_offset)

        self.dims.x_pos = x_offset + pad
        self.dims.y_pos = h_offset + pad

        # Taps
        self.taps = []
        bg_color = (db.color.mouse_over[0], db.color.mouse_over[1], db.color.mouse_over[2], 1)
        width = abs(self.dims.top_left[1] - self.dims.bot_left[1])
        bot_x = self.dims.bot_left[0]
        bot_y = self.dims.bot_left[1]

        #--- LEFT ---#
        tap = Taps(font_color=db.color.text, bg_color=bg_color, border_color=db.color.mods_highlight, tag="LEFT")
        dims = tap.dims
        dims.bot_left  = (bot_x - width, bot_y)
        dims.top_left  = (bot_x - width, bot_y + width)
        dims.top_right = (bot_x, bot_y + width)
        dims.bot_right = (bot_x, bot_y)

        pad = width * .25
        tap.verts = [
            (bot_x - width + pad, bot_y + width * .5), # Tip
            (bot_x - pad, bot_y + pad),
            (bot_x - pad, bot_y + width - pad),
            (bot_x - width + pad, bot_y + width * .5)] # Tip

        self.taps.append(tap)

        #--- RIGHT ---#
        bot_x = self.dims.bot_right[0]

        tap = Taps(font_color=db.color.text, bg_color=bg_color, border_color=db.color.mods_highlight, tag="RIGHT")
        dims = tap.dims
        dims.bot_left  = (bot_x, bot_y)
        dims.top_left  = (bot_x, bot_y + width)
        dims.top_right = (bot_x + width, bot_y + width)
        dims.bot_right = (bot_x + width, bot_y)

        pad = width * .25
        tap.verts = [
            (bot_x + pad, bot_y + pad), # Bottom
            (bot_x + width - pad, bot_y + width * .5), # Tip
            (bot_x + pad, bot_y + width - pad),
            (bot_x + pad, bot_y + pad)]

        self.taps.append(tap)

        if self.tips:
            self.tips.build(db, self.dims.top_left[0], self.dims.top_left[1])


    def update(self, context, event, db):
        if self.tips: self.tips.update()
        self.locked = False
        self.mouse_over = is_mouse_in_quad(self.dims.quad(), db.mouse_pos, tolerance=-1)
        self.__set_colors(db, self.mouse_over)
        if self.mouse_over:
            self.__mouse_over_actions(context, event, db)

        if self.obj == None or self.attr == "": return
        self.text = self.__get_value(use_convert=True, as_string=True)


    def __set_colors(self, db, mouse_over):

        self.border_color = db.color.border

        # Mouse over
        if mouse_over:
            self.bg_color = db.color.mouse_over
        # Standard color
        else:
            self.bg_color = db.color.cell_background


    def __mouse_over_actions(self, context, event, db):
        if db.locked_element: return

        # Clicked down
        if db.clicked == True:
            self.__on_active_callback()
            # Drag testing
            self.drag_was_used = False
            self.test_for_drag = True
            self.click_loc = (event.mouse_region_x, event.mouse_region_y)
            self.track_dist = 0
            self.drag_dist = 0
            # Taps
            self.shift_pressed_on_entry = event.shift
            # Double Click
            self.click_time = time.time()
            # Notify
            self.notified_of_entry = False
            # Locked State
            db.locked_element = self
            self.entry_string = ""
            self.tabbed_finish = False

        # Scroll : Up
        elif db.increment > 0:
            self.__on_active_callback()
            self.__ctrl_scroll(event)

            val = self.__get_value()
            if event.shift:
                self.__set_value(val + self.increment * .1)
            elif event.alt:
                self.__set_value(val * 2)
            else:
                self.__set_value(val + self.increment)

            self.__ctrl_scroll(event, before=False)

        # Scroll : Down
        elif db.increment < 0:
            self.__on_active_callback()
            self.__ctrl_scroll(event)

            val = self.__get_value()
            if event.shift:
                self.__set_value(val - self.increment * .1)
            elif event.alt:
                self.__set_value(val / 2)
            else:
                self.__set_value(val - self.increment)

            self.__ctrl_scroll(event, before=False)


    def __on_active_callback(self):
        if self.on_active_callback:
            self.on_active_callback()


    def __ctrl_scroll(self, event, before=True):
        if event.ctrl and self.ctrl_scroll_callback:
            if before:
                self.ctrl_scroll_callback(opt='before')
            else:
                self.ctrl_scroll_callback(opt='after')


    def locked_update(self, context, event, db):

        self.locked = True

        # Test for mouse drag event first
        if self.test_for_drag:
            if event.value != 'RELEASE':
                self.drag_dist = (Vector(self.click_loc) - Vector((event.mouse_region_x, event.mouse_region_y))).magnitude
                if self.drag_dist > 5:
                    self.drag_was_used = True
                    self.__drag(context, event, db)
                    return
            else:
                # Clear out drag event after use
                self.test_for_drag = False
                if self.drag_was_used:
                    db.locked_element = None
                    self.entry_string = ""
                    return

                # Set for taps
                if self.shift_pressed_on_entry:
                    self.using_taps = True

                self.timer = context.window_manager.event_timer_add(0.025, window=context.window)

        # Double click
        if not self.using_taps:
            if db.clicked:
                if time.time() - self.click_time < self.double_click_duration:
                    self.using_taps = True
                    return

        # Set highlight color during focus mode
        self.border_color = db.color.mods_highlight

        if self.using_taps:
            self.__taps(context, event, db)
            return

        valid = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '-'}

        if event.ascii in valid and event.value == 'PRESS':
            # Decimal
            decimal_blocked = False
            if '.' in self.entry_string and event.ascii == '.':
                decimal_blocked = True

            # Append text
            if decimal_blocked == False:
                if len(self.entry_string) <= self.limit:
                    self.entry_string += str(event.ascii)

            # Negative
            if self.entry_string.count('-') > 1:
                self.entry_string = self.entry_string.replace('-', '')
            elif self.entry_string.count('-') == 1:
                self.entry_string = self.entry_string.replace('-', '')
                self.entry_string = '-' + self.entry_string

        # Finish
        if event.type in {'RET', 'NUMPAD_ENTER', 'SPACE', 'LEFTMOUSE', 'TAB'} and event.value == 'PRESS':
            if event.type == 'TAB':
                self.tabbed_finish = True

            if self.entry_string != "":
                value = None
                try:
                    value = float(self.entry_string) if self.entry_string else 0
                except: pass

                if value != None:
                    if self.handle_radians:
                        value = math.radians(value)

                    self.__set_value(value)

            # Unlock / Set
            self.text = self.__get_value(use_convert=True, as_string=True)
            db.locked_element = None
            self.entry_string = ""
            self.__remove_timer(context)

        # Edit
        elif event.type == 'BACK_SPACE' and event.value == 'PRESS':
            self.entry_string = self.entry_string[:-1]


    def __drag(self, context, event, db):

        if abs(self.drag_dist - self.track_dist) > 10 * dpi_factor(min=0.5):
            self.track_dist = self.drag_dist
            value = float(self.__get_value())

            if event.mouse_x > event.mouse_prev_x:
                if event.shift and self.use_mod_keys:
                    value += self.increment * .1
                else:
                    value += self.increment
            else:
                if event.shift and self.use_mod_keys:
                    value -= self.increment * .1
                else:
                    value -= self.increment

            self.__set_value(value)
            self.text = self.__get_value(use_convert=True, as_string=True)


    def __taps(self, context, event, db):

        mouse_over_any = False
        for tap in self.taps:
            mouse_over = is_mouse_in_quad(tap.dims.quad(), db.mouse_pos)

            if mouse_over_any == False and mouse_over:
                mouse_over_any = True

            if mouse_over and db.clicked:
                if tap.tag == "LEFT":
                    val = self.__get_value()

                    if event.shift and self.use_mod_keys:
                        val -= self.increment * .1
                    else:
                        val -= self.increment

                    self.__set_value(val)
                    self.text = self.__get_value(use_convert=True, as_string=True)

                elif tap.tag == "RIGHT":
                    val = self.__get_value()

                    if event.shift and self.use_mod_keys:
                        val += self.increment * .1
                    else:
                        val += self.increment

                    self.__set_value(val)

                    self.text = self.__get_value(use_convert=True, as_string=True)

        # Click off : Reset
        if mouse_over_any == False and db.clicked:
            self.using_taps = False
            db.locked_element = None
            self.__remove_timer(context)


    def __get_value(self, use_convert=False, as_string=False):
        val = getattr(self.obj, self.attr)

        if self.attr_index != None:
            val = val[self.attr_index]

        if self.handle_radians and use_convert:
            if type(val) in {float, int}:
                val = round(math.degrees(val))

        if as_string:
            if type(val) == float:
                return numpy.format_float_positional(val, trim='-')
            return str(val)

        return val


    def __set_value(self, val):
        if self.attr_index != None:
            item = getattr(self.obj, self.attr)
            item[self.attr_index] = val
            val = item

        t = type(getattr(self.obj, self.attr))
        if t is int:
            val = int(val)
        setattr(self.obj, self.attr, val)


    def draw(self, db):
        render_quad(self.dims.quad(), color=self.bg_color)

        color = self.__faded_color()
        draw_border_lines(self.dims.quad(), color=color)

        if self.using_taps:
            self.__draw_taps()
            return

        text = self.entry_string if self.entry_string else self.text

        text = self.__decimal_clamped_text(text)

        render_text(text, (self.dims.x_pos, self.dims.y_pos), self.font_size, self.font_color)


    def draw_tips(self, db):
        if self.tips and self.mouse_over:
            self.tips.draw()


    def __faded_color(self):
        if self.locked and not self.test_for_drag and not self.using_taps:
            self.alpha = sin(time.time() * 10)
            return (self.border_color[0], self.border_color[1], self.border_color[2], self.alpha)

        return self.border_color


    def __draw_taps(self):

        text = self.__decimal_clamped_text(self.text)

        render_text(text, (self.dims.x_pos, self.dims.y_pos), self.font_size, self.font_color)

        for tap in self.taps:
            tap.draw()


    def __decimal_clamped_text(self, text):
        if self.decimal_draw_precision == None: return text
        if '.' not in text: return text

        split = text.split('.')
        if self.decimal_draw_precision == 0:
            return split[0]
        else:
            return f'{split[0]}.{split[1][:self.decimal_draw_precision]}'


    def shut_down(self, context):
        self.__remove_timer(context)


    def __remove_timer(self, context):
        if self.timer:
            context.window_manager.event_timer_remove(self.timer)