OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / operators / misc / screen_saver.py

367 lines · python ref:
import bpy, bmesh, mathutils, math, time, random
from gpu_extras.batch import batch_for_shader
from mathutils import Vector
from .screen_saver_tips import tips
from ... utility import method_handler
from ... utility import addon
from ... utility.base_modal_controls import Base_Modal_Controls
from ... ui_framework.master import Master
from ... ui_framework.graphics.draw import render_text, render_quad, draw_2D_lines
from ... ui_framework.utils.geo import get_blf_text_dims
from ... utils.blender_ui import get_dpi_factor

##############################################################################
# Draw Data
##############################################################################

HANDLE_2D = None

class Data:
    # States
    screen_saver_running = False
    restart_screen_saver = False

    @staticmethod
    def reset_states():
        Data.screen_saver_running = False
        Data.restart_screen_saver = False


##############################################################################
# Launch Operator
##############################################################################

class HOPS_OT_Draw_Screen_Saver_Launcher(bpy.types.Operator):
    bl_idname = "hops.draw_screen_saver_launcher"
    bl_label = "Screen Saver Draw Launcher"
    bl_options = {"INTERNAL"}

    def execute(self, context):

        # Trigger modal to reset its timers and start over again
        if Data.screen_saver_running == True:
            Data.restart_screen_saver = True
        # Call a fresh operator if none is currently running
        else:
            bpy.ops.hops.draw_screen_saver('INVOKE_DEFAULT')

        return {'FINISHED'}


##############################################################################
# Drawing Modal
##############################################################################

class HOPS_OT_Draw_Screen_Saver(bpy.types.Operator):
    bl_idname = "hops.draw_screen_saver"
    bl_label = "Drawing for screen saver"
    bl_options = {"INTERNAL"}

    def invoke(self, context, event):

        self.master = Master(context=context)
        self.master.only_use_fast_ui = True
        self.base_controls = Base_Modal_Controls(context, event)

        # Registers
        self.shader = Shader(context)
        self.timer = context.window_manager.event_timer_add(0.075, window=context.window)
        context.window_manager.modal_handler_add(self)

        return {'RUNNING_MODAL'}


    def modal(self, context, event):

        # Base Systems
        self.master.receive_event(event=event)
        self.base_controls.update(context, event)

        Data.screen_saver_running = True

        # Resest every thing if new drawing is initialized
        if Data.restart_screen_saver == True:
            Data.restart_screen_saver = False
            self.shader.reset()

        # Reset everything
        if self.shader.fade_complete:
            self.shader.reset()

        # Exit and Reset
        if event.type in {'SPACE', 'LEFTMOUSE', 'ESC', 'RIGHTMOUSE'}:
            self.__finished(context)
            self.master.run_fade()
            return {'FINISHED'}

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

        # Toggle the viewport animation
        if event.type == "A" and event.value == "PRESS":
            bpy.ops.screen.animation_play('INVOKE_DEFAULT')

        self.draw_master(context)

        if self.base_controls.pass_through:
            return {'PASS_THROUGH'}

        return {'PASS_THROUGH'}


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

        # Global
        Data.reset_states()

        # Unregister
        if self.timer != None:
            context.window_manager.event_timer_remove(self.timer)
        if self.shader != None:
            self.shader.destroy()
        if context.area != None:
            context.area.tag_redraw()


    def draw_master(self, context):

        self.master.setup()
        if self.master.should_build_fast_ui():

            # Help
            help_items = {"GLOBAL" : [], "STANDARD" : []}
            help_items["STANDARD"] = [
                ("A", "Toggle play animation")]

            self.master.receive_fast_ui(help_list=help_items)
        self.master.finished()


class Shader():

    def __init__(self, context):

        self.hold_duration = addon.preference().ui.Hops_screen_saver_fade
        self.fade_duration = 2

        self.screen_width = context.area.width
        self.screen_height = context.area.height
        self.scale = get_dpi_factor()

        self.header_size = 26     # Header font size
        self.header_dims = (0,0)  # Header font size X / Y
        self.body_size = 16       # Body font size
        self.body_line_height = 0 # Tallest line height from body
        self.body_dims = (0,0)    # Bounding box for body

        self.tip_index = 0      # Tips index chosen random

        self.text_area_dims = (0,0)    # Full text dims
        self.padding = 16 * self.scale # Font padding

        self.reset()
        self.__setup_handle(context)


    def reset(self):
        '''Restart the shader.'''

        # States
        self.fade_complete = False
        self.start_time = time.time()

        # Font color / alpha
        color = addon.preference().color.Hops_UI_text_color
        self.font_alpha = color[3]
        self.og_font_alpha = color[3]
        self.font_color = (color[0], color[1], color[2], self.font_alpha)

        # BG color / alpha
        color = addon.preference().color.Hops_border_color
        self.bg_alpha = color[3]
        self.og_bg_alpha = color[3]
        self.bg_color = (color[0], color[1], color[2], self.bg_alpha)

        # Trim color
        color = addon.preference().color.Hops_UI_cell_background_color
        self.trim_color = (color[0], color[1], color[2], self.bg_alpha)

        self.__setup_next_tip()


    def __setup_next_tip(self):
        
        # Get the current tip
        self.tip_index = random.randint(0, len(tips) - 1)
        tip = tips[self.tip_index]

        # Get header dims
        header_text = tip[0]
        self.header_dims = get_blf_text_dims(text=header_text, size=self.header_size)

        # Get body dims
        x, y, tallest_line = 0, 0, 0
        for item in tip[1:]:
            dims = get_blf_text_dims(text=item, size=self.body_size)

            if dims[0] > x:
                x = dims[0]

            if dims[1] > tallest_line:
                tallest_line = dims[1]

            y += dims[1] + self.padding

        self.body_line_height = tallest_line
        self.body_dims = (x, y)

        # Over all
        over_all_x = x if x > self.header_dims[0] else self.header_dims[0] # Widest line
        y += self.header_dims[1] + self.padding
        self.text_area_dims = (over_all_x, y)


    def destroy(self):
        '''Remove the shader.'''

        global HANDLE_2D
        if HANDLE_2D != None:
            bpy.types.SpaceView3D.draw_handler_remove(HANDLE_2D, "WINDOW")
            HANDLE_2D = None
            print("Screen Saver Removed")


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

        global HANDLE_2D
        if HANDLE_2D == None:
            HANDLE_2D = bpy.types.SpaceView3D.draw_handler_add(self.__safe_draw_2d, (context, ), "WINDOW", "POST_PIXEL")


    def __safe_draw_2d(self, context):
        method_handler(self.__draw_2d,
            arguments = (context,),
            identifier = 'Screen Saver 2D Shader',
            exit_method = self.destroy)


    def __draw_2d(self, context):
        '''Draw the UVs.'''

        self.__handle_fade()

        # Trigger exit
        if self.font_alpha <= 0:
            self.fade_complete = True

        # Draw Tips
        self.__draw_tips_box()
        self.__draw_under_line()
        self.__draw_tips()

    
    def __draw_tips_box(self):

        x = self.screen_width
        y = self.screen_height * .75
        o = self.header_dims[1] + self.padding
        h = y - self.text_area_dims[1]

        # Trim
        top_left  = (0, self.padding * .25 + y + o)
        bot_left  = (0, y + o)
        top_right = (x, self.padding * .25 + y + o)
        bot_right = (x, y + o)

        render_quad(
            quad=(top_left, bot_left, top_right, bot_right),
            color=(self.trim_color[0], self.trim_color[1], self.trim_color[2], self.bg_alpha),
            bevel_corners=False)

        # BG
        top_left  = (0, y + o)
        bot_left  = (0, h)
        top_right = (x, y + o)
        bot_right = (x, h)

        render_quad(
            quad=(top_left, bot_left, top_right, bot_right),
            color=(self.bg_color[0], self.bg_color[1], self.bg_color[2], self.bg_alpha),
            bevel_corners=False)

        draw_2D_lines(
            vertices=[top_left, top_right, top_right, bot_right, bot_right, bot_left, bot_left, top_left],
            width=3,
            color=(self.bg_color[0], self.bg_color[1], self.bg_color[2], self.bg_alpha))


    def __draw_under_line(self):

        x = self.screen_width * .25
        y = (self.screen_height * .75) - self.padding * .5
        o = self.header_dims[0] * 1.75
        draw_2D_lines(
            vertices=[(x, y), (x + o, y)],
            width=2,
            color=(self.bg_color[0], self.bg_color[1], self.bg_color[2], self.bg_alpha))


    def __draw_tips(self):

        # Tips
        tip = tips[self.tip_index]

        # Header
        x = self.screen_width * .25
        y = self.screen_height * .75

        render_text(
            text=tip[0],
            position=(x, y),
            size=self.header_size,
            color=(self.font_color[0], self.font_color[1], self.font_color[2], self.font_alpha))

        # Body
        y -= self.header_dims[1] + self.padding
        for item in tip[1:]:
            render_text(
                text=item,
                position=(x, y),
                size=self.body_size,
                color=(self.font_color[0], self.font_color[1], self.font_color[2], self.font_alpha))

            y -= self.body_line_height + self.padding


    def __handle_fade(self):
        '''Fade the alpha based on current time and Global DURATION variable'''

        if time.time() - self.start_time > self.hold_duration:
            diff = time.time() - (self.start_time + self.hold_duration)
            ratio = diff / self.fade_duration

            # Font
            self.font_alpha = self.og_font_alpha - (self.og_font_alpha * ratio)

            # BG
            self.bg_alpha = self.og_bg_alpha - (self.og_bg_alpha * ratio)


##############################################################################
# Remove On App Reload
##############################################################################

from bpy.app.handlers import persistent
@persistent
def remove_screen_saver_shader(dummy):
    global HANDLE_2D
    if HANDLE_2D != None:
        bpy.types.SpaceView3D.draw_handler_remove(HANDLE_2D, "WINDOW")
        HANDLE_2D = None

    Data.reset_states()