OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / operators / modals / font_scroll.py

292 lines · python ref:
import bpy
import enum
from ... utility import addon, method_handler

from os import walk, path
import traceback
import random

from ... ui_framework.master import Master
from ... ui_framework.utils.mods_list import get_mods_list
from ... utility.base_modal_controls import Base_Modal_Controls

# Cursor Warp imports
from ... utils.toggle_view3d_panels import collapse_3D_view_panels
from ... utils.modal_frame_drawing import draw_modal_frame
from ... utils.cursor_warp import mouse_warp

DESC = """Scroll through fonts in a Font folder"""

supported_fonts = {'.ttf', '.otf', '.woff', '.woff2'}

class HOPS_OT_FontScroll(bpy.types.Operator):
    bl_idname = "hops.font_scroll"
    bl_label = "Font Scroll"
    bl_options = {'REGISTER', 'UNDO'}
    bl_description = DESC

    @classmethod
    def poll(cls, context):
        active = context.active_object
        if context.area.ui_type == 'GeometryNodeTree' and context.active_object:
            if not active.modifiers.active: return
            if active.modifiers.active.type != 'NODES': return
            if not active.modifiers.active.node_group: return
            if not active.modifiers.active.node_group.nodes.active: return

            return active.modifiers.active.node_group.nodes.active.type == 'STRING_TO_CURVES'

        return active and active.type == 'FONT'

    def invoke (self, context, event):
        self.font_folder_path: str = addon.preference().property.font_folder

        if not path.exists(self.font_folder_path):
            self.report({'INFO'}, 'Font directory does not exist or its path is invalid')
            return {'CANCELLED'}

        self.active_font_index = 0
        self.font_index_max = 0
        self.active_loaded_font_name: str = ''
        self.fonts_dir: list[tuple[str, str]] = [] # fontname dirpath
        self.initial_fonts = {f.filepath : f.name for f in bpy.data.fonts}

        if context.area.ui_type == 'GeometryNodeTree':
            self.selected_items = [context.active_object.modifiers.active.node_group.nodes.active]
        else:
            self.selected_items = [o.data for o in context.selected_objects if o.type == 'FONT']


        self.back = [itm.font.name for itm in self.selected_items]

        for root, dirs, filenames in walk(self.font_folder_path):
            names = [s for s in filenames if path.splitext(s)[1] in supported_fonts]
            if names:
                self.font_index_max += len(names) - 1

                for name in names:
                    self.fonts_dir.append((name, root))

        if not self.fonts_dir:
            self.report({'INFO'}, 'Font directory is empty or has no supported fonts')
            return {'CANCELLED'}

        # Base Systems
        self.master = Master(context)
        self.master.only_use_fast_ui = True
        self.base_controls = Base_Modal_Controls(context, event, popover_keys=['TAB', 'SPACE'])
        self.original_tool_shelf, self.original_n_panel = collapse_3D_view_panels()
        self.draw_handle = bpy.types.SpaceView3D.draw_handler_add(self.safe_draw_shader, (context,), 'WINDOW', 'POST_PIXEL')

        self.__class__.operator = self
        context.window_manager.modal_handler_add(self)

        return {'RUNNING_MODAL'}

    def modal(self, context, event):
        try:
            return self._modal(context, event)
        except Exception as e:
            traceback.print_exc()
            self.cancel_exit()
            self.report({'ERROR'}, f'{e}')

            return {'CANCELLED'}

    def _modal(self, context, event):

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

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

        elif self.base_controls.scroll:
            if not self.active_loaded_font_name:
                self.active_font_index = 0
            else:
                self.scroll(self.base_controls.scroll)

            font = self.load_active_font()
            if not font:
                fontpath = self.active_fontpath()
                self.report({'ERROR'}, rf'{fontpath} is not a supportted font. Remove the file and try again.')
                self.cancel_exit()

                return {'CANCELLED'}


            # unloading active can invalidate font
            name = font.name
            if self.active_loaded_font_name != name:
                for itm in self.selected_items:
                    itm.font = font

                self.unload_active_font()
                self.active_loaded_font_name = name

        elif event.type == 'R' and event.value == 'PRESS':
            new_id = random.randint(0, self.font_index_max)

            self.set_font_index(new_id)

            font = self.load_active_font()
            if not font:
                fontpath = self.active_fontpath()
                self.report({'ERROR'}, rf'{fontpath} is not a supportted font. Remove the file and try again.')
                self.cancel_exit()

                return {'CANCELLED'}


            # unloading active can invalidate font
            name = font.name
            if self.active_loaded_font_name != name:
                for itm in self.selected_items:
                    itm.font = font

                self.unload_active_font()
                self.active_loaded_font_name = name

        elif event.type == 'S' and event.value == 'RELEASE':
            f_dit = self.fonts_dir[self.active_font_index]

            random.shuffle(self.fonts_dir)

            self.active_font_index = self.fonts_dir.index(f_dit)

        elif self.base_controls.confirm:
            self.kill_ui()
            return {'FINISHED'}

        elif self.base_controls.cancel:
            self.cancel_exit()

            return {'CANCELLED'}

        self.draw_ui(context)
        context.area.tag_redraw()

        return {'RUNNING_MODAL'}

    def scroll(self, direction:int) -> str:
        '''Upadte tracked directory and folder indices'''
        self.active_font_index = (self.active_font_index + direction) % len(self.fonts_dir)


    def load_active_font(self):
        fpath = self.active_fontpath()
        name =  self.initial_fonts.get(fpath)

        if name: return bpy.data.fonts[name]

        try:
            font = bpy.data.fonts.load(self.active_fontpath(), check_existing=True)
            font.user_clear()
            return font
        except Exception as e:
            print(e)
            return None

    def unload_active_font(self):
        if not self.active_loaded_font_name: return

        font = self.active_font_get()
        if font.filepath not in self.initial_fonts:
            font.user_clear()
            bpy.data.fonts.remove(font)
            self.active_loaded_font_name = ''

    def active_font_get(self):
        return bpy.data.fonts[self.active_loaded_font_name]

    def active_fontpath(self) -> str:
        font_name, dirpath = self.fonts_dir[self.active_font_index]
        return path.join(dirpath, font_name)

    def set_font_index(self, index:int):
        self.active_font_index = index % (self.font_index_max + 1)

    def draw_ui(self, context):

        self.master.setup()

        # -- Fast UI -- #
        if not self.master.should_build_fast_ui(): return

        # Main
        win_list = []
        if addon.preference().ui.Hops_modal_fast_ui_loc_options != 1:
            if self.active_loaded_font_name:
                win_list.append(self.active_loaded_font_name)

        else:
            if self.active_loaded_font_name:
                font_dir = self.fonts_dir[self.active_font_index]
                dir_path = path.relpath(font_dir[1], self.font_folder_path)

                win_list.append(dir_path)
                win_list.append(self.active_loaded_font_name)
                win_list.append(f'{self.active_font_index} / {self.font_index_max}')

        # Help
        help_items = {"GLOBAL" : [], "STANDARD" : []}

        help_items["GLOBAL"] = [
            ("M", "Toggle mods list"),
            ("H", "Toggle help"),
            ("~", "Toggle UI Display Type"),
            ("O", "Toggle viewport rendering")]

        help_append = help_items["STANDARD"].append


        help_append(["LMB", "Apply"])
        help_append(["RMB", "Cancel"])
        help_append(["Scroll", "Cycle Fonts"])
        help_append(["R", "Random Font"])
        help_append(["S", "Shuffle Font Order"])

        #copy list because ui mutates it in redacted manner
        active = self.fonts_dir[self.active_font_index][0]
        self.master.receive_fast_ui(win_list=win_list, help_list=help_items, image="Tthick", mods_list=[[fd[0], ''] for fd in self.fonts_dir], active_mod_name=active)
        self.master.finished()

    ####################################################
    #   CURSOR WARP
    ####################################################

    def safe_draw_shader(self, context):
        method_handler(self.draw_shader,
            arguments = (context,),
            identifier = 'UI Framework',
            exit_method = self.remove_shader)


    def remove_shader(self):
        '''Remove shader handle.'''

        if self.draw_handle:
            self.draw_handle = bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, "WINDOW")


    def draw_shader(self, context):
        '''Draw shader handle.'''

        draw_modal_frame(context)

    def kill_ui(self):
        collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
        self.remove_shader()
        self.master.run_fade()

    def cancel_exit(self):
        self.kill_ui()

        for name, itm in zip(self.back, self.selected_items):
            itm.font = bpy.data.fonts[name]

        self.unload_active_font()