diff --git a/.gitignore b/.gitignore index 220a137..e7b21a5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,3 @@ venv # vim *.swo *.swp -/abc3d_updater/* diff --git a/README.md b/README.md index d41cb13..7187011 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,6 @@ source venv/bin/activate pip install bpy ``` -to install mathutils, this was necessary for me: -``` -sudo xbps-install -Sy python3.11-devel -CFLAGS=$(python3.11-config --cflags) LDFLAGS=$(python3.11-config --ldflags) pip install mathutils -``` - # install addon: ```bash cd diff --git a/__init__.py b/__init__.py index c68b1f7..36e2f46 100644 --- a/__init__.py +++ b/__init__.py @@ -4,21 +4,13 @@ A 3D font helper """ -import os -from bpy.app.handlers import persistent -from bpy.types import Panel -import functools -import io -import bpy -import importlib - bl_info = { "name": "ABC3D", "author": "Jakob Schlötter, Studio Pointer*", - "version": (0, 0, 2), + "version": (0, 0, 1), "blender": (4, 1, 0), "location": "VIEW3D", - "description": "Convenience addon for 3D fonts", + "description": "Does ABC3D stuff", "category": "Typography", } @@ -26,26 +18,39 @@ bl_info = { # when registering # handy for development # first import dependencies for the method -if "Font" in locals(): +import importlib + +# then import dependencies for our addon +if "bpy" in locals(): importlib.reload(Font) importlib.reload(utils) importlib.reload(butils) importlib.reload(bimport) - importlib.reload(addon_updater_ops) else: from .common import Font from .common import utils from . import butils from . import bimport - from . import addon_updater_ops +import bpy +import math +import mathutils +import io +import functools +from bpy.types import Panel +from bpy.app.handlers import persistent +from random import uniform + +import time +import datetime + +import os +import re def getPreferences(context): preferences = context.preferences return preferences.addons[__name__].preferences - -@addon_updater_ops.make_annotations class ABC3D_addonPreferences(bpy.types.AddonPreferences): """ABC3D Addon Preferences @@ -53,80 +58,40 @@ class ABC3D_addonPreferences(bpy.types.AddonPreferences): bl_idname = __name__ - # Addon updater preferences. - auto_check_update = bpy.props.BoolProperty( - name="Auto-check for Update", - description="If enabled, auto-check for updates using an interval", - default=False) - - updater_interval_months = bpy.props.IntProperty( - name='Months', - description="Number of months between checking for updates", - default=0, - min=0) - - updater_interval_days = bpy.props.IntProperty( - name='Days', - description="Number of days between checking for updates", - default=7, - min=0, - max=31) - - updater_interval_hours = bpy.props.IntProperty( - name='Hours', - description="Number of hours between checking for updates", - default=0, - min=0, - max=23) - - updater_interval_minutes = bpy.props.IntProperty( - name='Minutes', - description="Number of minutes between checking for updates", - default=0, - min=0, - max=59) - def get_default_assets_dir(): return bpy.utils.user_resource( - 'DATAFILES', - path=f"{__name__}", - create=True) + 'DATAFILES', + path=f"{__name__}", + create=True) def on_change_assets_dir(self, context): if not os.path.isdir(self.assets_dir): - butils.ShowMessageBox( - title=f"{__name__} Warning", - icon="ERROR", - message=("Chosen directory does not exist.", - "Please, reset to default, create it or chose another one.")) + butils.ShowMessageBox( + title=f"{__name__} Warning", + icon="ERROR", + message=("Chosen directory does not exist.", + "Please, reset to default, create it or chose another one.")) elif not os.access(self.assets_dir, os.W_OK): - butils.ShowMessageBox( - title=f"{__name__} Warning", - icon="ERROR", - message=("Chosen directory is not writable.", - "Please reset to default or chose another one.")) + butils.ShowMessageBox( + title=f"{__name__} Warning", + icon="ERROR", + message=("Chosen directory is not writable.", + "Please reset to default or chose another one.")) print(f"{__name__}: change assets_dir to {self.assets_dir}") assets_dir: bpy.props.StringProperty( - name="Assets Folder", - subtype='DIR_PATH', - default=get_default_assets_dir(), - update=on_change_assets_dir, - ) + name="Assets Folder", + subtype='DIR_PATH', + default=get_default_assets_dir(), + update=on_change_assets_dir, + ) def draw(self, context): layout = self.layout layout.label(text="Directory for storage of fonts and other assets:") layout.prop(self, "assets_dir") - # Works best if a column, or even just self.layout. - mainrow = layout.row() - col = mainrow.column() - - # Updater draw function, could also pass in col as third arg. - addon_updater_ops.update_settings_ui(self, context) - class ABC3D_available_font(bpy.types.PropertyGroup): font_name: bpy.props.StringProperty(name="") @@ -137,9 +102,9 @@ class ABC3D_glyph_properties(bpy.types.PropertyGroup): glyph_id: bpy.props.StringProperty(maxlen=1) glyph_object: bpy.props.PointerProperty(type=bpy.types.Object) letter_spacing: bpy.props.FloatProperty( - name="Letter Spacing", - description="Letter Spacing", - ) + name="Letter Spacing", + description="Letter Spacing", + ) class ABC3D_text_properties(bpy.types.PropertyGroup): @@ -154,17 +119,17 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): d = context.scene.abc3d_data items = self.font_items_callback(context) if len(d.available_fonts) > 0: - if len(d.available_fonts) > d.active_text_index: - f = d.available_fonts[d.active_text_index] - return 0 # f"{f.font_name} {f.face_name}" - else: - f = d.available_fonts[0] - return 0 # f"{f.font_name} {f.face_name}" + if len(d.available_fonts) > d.active_text_index: + f = d.available_fonts[d.active_text_index] + return 0 #f"{f.font_name} {f.face_name}" + else: + f = d.available_fonts[0] + return 0 #f"{f.font_name} {f.face_name}" if type(self.font_name) != type(None) and type(self.face_name) != type(None): - return 0 # f"{self.font_name} {self.face_name}" + return 0 #f"{self.font_name} {self.face_name}" else: - return 0 # "" + return 0 #"" def glyphs_update_callback(self, context): butils.prepare_text(self.font_name, @@ -183,89 +148,83 @@ class ABC3D_text_properties(bpy.types.PropertyGroup): text_id: bpy.props.IntProperty() font: bpy.props.EnumProperty( - items=font_items_callback, - update=font_update_callback, - ) + items=font_items_callback, + update=font_update_callback, + ) font_name: bpy.props.StringProperty( - update=glyphs_update_callback - ) + update=glyphs_update_callback + ) face_name: bpy.props.StringProperty( - update=glyphs_update_callback - ) + update=glyphs_update_callback + ) text_object: bpy.props.PointerProperty(type=bpy.types.Object) text: bpy.props.StringProperty( - update=glyphs_update_callback - ) + update=glyphs_update_callback + ) letter_spacing: bpy.props.FloatProperty( - update=update_callback, - name="Letter Spacing", - description="Letter Spacing", - options={'ANIMATABLE'}, - step=0.01, - ) + update=update_callback, + name="Letter Spacing", + description="Letter Spacing", + options={'ANIMATABLE'}, + step=0.01, + ) orientation: bpy.props.FloatVectorProperty( - update=update_callback, - name="Orientation", - default=(1.5707963267948966, 0.0, 0.0), # 90 degrees in radians - subtype='EULER', - ) + update=update_callback, + name="Orientation", + default=(1.5707963267948966, 0.0, 0.0), # 90 degrees in radians + subtype='EULER', + ) translation: bpy.props.FloatVectorProperty( - update=update_callback, - name="Translation", - default=(0.0, 0.0, 0.0), - subtype='TRANSLATION', - ) + update=update_callback, + name="Translation", + default=(0.0, 0.0, 0.0), + subtype='TRANSLATION', + ) font_size: bpy.props.FloatProperty( - update=update_callback, - name="Font Size", - default=1.0, - subtype='NONE', - ) + update=update_callback, + name="Font Size", + default=1.0, + subtype='NONE', + ) offset: bpy.props.FloatProperty( - update=update_callback, - name="Offset", - default=0.0, - subtype='NONE', - ) + update=update_callback, + name="Offset", + default=0.0, + subtype='NONE', + ) compensate_curvature: bpy.props.BoolProperty( - update=update_callback, - name="Compensate Curvature", - description="Fixes curvature spacing issues for simple curves, don't use on curve with tiny loops.", - default=True, + update=update_callback, + name="Compensate Curvature", + description="Fixes curvature spacing issues for simple curves, don't use on curve with tiny loops.", + default=True, ) ignore_orientation: bpy.props.BoolProperty( - update=update_callback, - name="Ignore Curve Orientation", - default=False, + update=update_callback, + name="Ignore Curve Orientation", + default=False, ) distribution_type: bpy.props.StringProperty() glyphs: bpy.props.CollectionProperty(type=ABC3D_glyph_properties) -# TODO: simply, merge, cut cut cut - - +#TODO: simply, merge, cut cut cut class ABC3D_data(bpy.types.PropertyGroup): - available_fonts: bpy.props.CollectionProperty( - type=ABC3D_available_font, name="Available fonts") - + available_fonts: bpy.props.CollectionProperty(type=ABC3D_available_font, name="Available fonts") def active_font_index_update(self, context): if len(self.available_fonts) <= self.active_font_index: self.active_font_index = len(self.available_fonts) - 1 active_font_index: bpy.props.IntProperty( - default=-1, - update=active_font_index_update, - ) - available_texts: bpy.props.CollectionProperty( - type=ABC3D_text_properties, name="Available texts") - + default=-1, + update=active_font_index_update, + ) + available_texts: bpy.props.CollectionProperty(type=ABC3D_text_properties, name="Available texts") def active_text_index_update(self, context): if self.active_text_index != -1: o = self.available_texts[self.active_text_index].text_object # active_text_index changed. so let's update the selection # check if it is already selected # or perhaps one of the glyphs - if not o.select_get() and not len([c for c in o.children if c.select_get()]) > 0: + if not o.select_get() and not len([ c for c in o.children if c.select_get() ]) > 0: bpy.ops.object.select_all(action="DESELECT") o.select_set(True) bpy.context.view_layer.objects.active = o @@ -275,43 +234,39 @@ class ABC3D_data(bpy.types.PropertyGroup): active_text_index: bpy.props.IntProperty(update=active_text_index_update) # def font_path_update_callback(self, context): - # butils.ShowMessageBox("Friendly Reminder", message="do not forget to click on 'Install new font'") - # bpy.ops.abc3d.install_font() + # butils.ShowMessageBox("Friendly Reminder", message="do not forget to click on 'Install new font'") + # bpy.ops.abc3d.install_font() # stupid hack for Mac OS font_path: bpy.props.StringProperty( - name="Font path", - description="Install a *.glb or *.gltf fontfile from disk", - default="", - maxlen=1024, - # update=font_path_update_callback, - subtype="FILE_PATH") + name="Font path", + description="Install a *.glb or *.gltf fontfile from disk", + default="", + maxlen=1024, + # update=font_path_update_callback, + subtype="FILE_PATH") export_dir: bpy.props.StringProperty( - name="Export Directory", - description=f"The directory in which we will export fonts.\nIf it is blank, we will export to the addon assets path.\nThis is where the fonts are installed.", - subtype="DIR_PATH") + name="Export Directory", + description=f"The directory in which we will export fonts.\nIf it is blank, we will export to the addon assets path.\nThis is where the fonts are installed.", + subtype="DIR_PATH") class ABC3D_UL_fonts(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - # avoids renaming the item by accident - layout.label(text=f"{index}: {item.font_name} {item.face_name}") + layout.label(text=f"{index}: {item.font_name} {item.face_name}") # avoids renaming the item by accident def invoke(self, context, event): pass - class ABC3D_UL_texts(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): split = layout.split(factor=0.3) split.label(text="Id: %d" % (item.text_id)) - # avoids renaming the item by accident - split.label(text=f"{item.text}") + split.label(text=f"{item.text}") # avoids renaming the item by accident def invoke(self, context, event): pass - class ABC3D_PT_Panel(bpy.types.Panel): bl_label = f"{__name__} panel" bl_category = "ABC3D" @@ -326,10 +281,7 @@ class ABC3D_PT_Panel(bpy.types.Panel): icon = 'ERROR' layout.row().label(text='no fonts loaded yet') - layout.operator(f"{__name__}.load_installed_fonts", - text="load installed fonts", icon=icon) - layout.operator(f"{__name__}.open_asset_directory", - text="open asset directory", icon='FILEBROWSER') + layout.operator(f"{__name__}.load_installed_fonts", text="load installed fonts", icon=icon) class ABC3D_PT_LoadFontPanel(bpy.types.Panel): @@ -368,16 +320,13 @@ class ABC3D_PT_FontList(bpy.types.Panel): abc3d_data = scene.abc3d_data layout.label(text="Available Fonts") - layout.template_list("ABC3D_UL_fonts", "", abc3d_data, - "available_fonts", abc3d_data, "active_font_index") + layout.template_list("ABC3D_UL_fonts", "", abc3d_data, "available_fonts", abc3d_data, "active_font_index") if abc3d_data.active_font_index >= 0: available_font = abc3d_data.available_fonts[abc3d_data.active_font_index] font_name = available_font.font_name face_name = available_font.face_name - available_glyphs = sorted( - Font.fonts[font_name].faces[face_name].glyphs_in_fontfile) - loaded_glyphs = sorted( - Font.fonts[font_name].faces[face_name].loaded_glyphs) + available_glyphs = sorted(Font.fonts[font_name].faces[face_name].glyphs_in_fontfile) + loaded_glyphs = sorted(Font.fonts[font_name].faces[face_name].loaded_glyphs) box = layout.box() box.row().label(text=f"Font Name: {font_name}") box.row().label(text=f"Face Name: {face_name}") @@ -385,27 +334,21 @@ class ABC3D_PT_FontList(bpy.types.Panel): n_rows = int(len(available_glyphs) / n) box.row().label(text=f"Glyphs:") subbox = box.box() - for i in range(0, n_rows + 1): - text = ''.join([f"{u}" for ui, u in enumerate( - available_glyphs) if ui < (i+1) * n and ui >= i * n]) + for i in range(0, n_rows + 1): + text = ''.join([f"{u}" for ui, u in enumerate(available_glyphs) if ui < (i+1) * n and ui >= i * n]) scale_y = 0.5 - row = subbox.row() - row.scale_y = scale_y - row.alignment = 'CENTER' + row = subbox.row(); row.scale_y = scale_y; row.alignment = 'CENTER' row.label(text=text) n_rows = int(len(loaded_glyphs) / n) box.row().label(text=f"Loaded/Used Glyphs:") subbox = box.box() - for i in range(0, n_rows + 1): - text = ''.join([f"{u}" for ui, u in enumerate( - loaded_glyphs) if ui < (i+1) * n and ui >= i * n]) + for i in range(0, n_rows + 1): + text = ''.join([f"{u}" for ui, u in enumerate(loaded_glyphs) if ui < (i+1) * n and ui >= i * n]) scale_y = 0.5 - row = subbox.row() - row.scale_y = scale_y + row = subbox.row(); row.scale_y = scale_y row.label(text=text) row = layout.row() - oper = row.operator(f"{__name__}.load_font", - text='Load all glyphs in memory') + oper = row.operator(f"{__name__}.load_font", text='Load all glyphs in memory') oper.font_name = font_name oper.face_name = face_name @@ -441,7 +384,6 @@ class ABC3D_PT_TextPlacement(bpy.types.Panel): layout.label(text="Cannot place Text.") layout.label(text="Select a curve as active object.") - class ABC3D_PT_TextManagement(bpy.types.Panel): bl_label = "Text Management" bl_parent_id = "ABC3D_PT_Panel" @@ -456,7 +398,6 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): scene = context.scene abc3d_data = scene.abc3d_data # TODO: update available_texts - def update(): if bpy.context.screen.is_animation_playing: return @@ -475,17 +416,16 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): # these might be there in t.glyphs, but linked to removed objects # or they might be lost if type(next((g for g in t.glyphs if type(g.glyph_object) == type(None)), None)) == type(None): - g = next((g for g in t.glyphs if type( - g.glyph_object) == type(None)), None) + g = next((g for g in t.glyphs if type(g.glyph_object) == type(None)), None) # for g in t.glyphs: # if type(g) == type(None): - # print("IS NONE") + # print("IS NONE") # if type(g.glyph_object) == type(None): - # print("go IS NONE") + # print("go IS NONE") # else: - # if g.glyph_object == c: - # # print(g.glyph_object.name) - # pass + # if g.glyph_object == c: + # # print(g.glyph_object.name) + # pass if remove_me: remove_list.append(i) @@ -493,18 +433,17 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): for i in remove_list: if type(abc3d_data.available_texts[i].text_object) != type(None): mom = abc3d_data.available_texts[i].text_object - def delif(o, p): if p in o: del o[p] - delif(mom, f"{utils.prefix()}_linked_textobject") - delif(mom, f"{utils.prefix()}_font_name") - delif(mom, f"{utils.prefix()}_face_name") - delif(mom, f"{utils.prefix()}_font_size") - delif(mom, f"{utils.prefix()}_letter_spacing") - delif(mom, f"{utils.prefix()}_orientation") - delif(mom, f"{utils.prefix()}_translation") - delif(mom, f"{utils.prefix()}_offset") + delif(mom,f"{utils.prefix()}_linked_textobject") + delif(mom,f"{utils.prefix()}_font_name") + delif(mom,f"{utils.prefix()}_face_name") + delif(mom,f"{utils.prefix()}_font_size") + delif(mom,f"{utils.prefix()}_letter_spacing") + delif(mom,f"{utils.prefix()}_orientation") + delif(mom,f"{utils.prefix()}_translation") + delif(mom,f"{utils.prefix()}_offset") abc3d_data.available_texts.remove(i) for i, t in enumerate(abc3d_data.available_texts): @@ -529,11 +468,8 @@ class ABC3D_PT_TextManagement(bpy.types.Panel): abc3d_data = scene.abc3d_data layout.label(text="Text Objects") - layout.template_list("ABC3D_UL_texts", "", abc3d_data, - "available_texts", abc3d_data, "active_text_index") - layout.row().operator( - f"{__name__}.remove_text", text="Remove Textobject") - + layout.template_list("ABC3D_UL_texts", "", abc3d_data, "available_texts", abc3d_data, "active_text_index") + layout.row().operator(f"{__name__}.remove_text", text="Remove Textobject") class ABC3D_PT_FontCreation(bpy.types.Panel): bl_label = "Font Creation" @@ -550,29 +486,22 @@ class ABC3D_PT_FontCreation(bpy.types.Panel): abc3d_data = scene.abc3d_data - layout.row().operator( - f"{__name__}.create_font_from_objects", text='Create/Extend Font') + layout.row().operator(f"{__name__}.create_font_from_objects", text='Create/Extend Font') box = layout.box() box.row().label(text="Exporting a fontfile") box.row().label(text="1. Select export directory:") box.prop(abc3d_data, 'export_dir') box.row().label(text="2. More options and export:") - box.row().operator(f"{__name__}.save_font_to_file", - text='Export Font To File') - layout.row().operator( - f"{__name__}.toggle_abc3d_collection", text='Toggle Collection') + box.row().operator(f"{__name__}.save_font_to_file", text='Export Font To File') + layout.row().operator(f"{__name__}.toggle_abc3d_collection", text='Toggle Collection') box = layout.box() box.label(text="metrics") - box.row().operator( - f"{__name__}.add_default_metrics", text='Add Default Metrics') + box.row().operator(f"{__name__}.add_default_metrics", text='Add Default Metrics') box.row().operator(f"{__name__}.remove_metrics", text='Remove Metrics') box.row().operator(f"{__name__}.align_metrics", text='Align Metrics') - box.row().operator( - f"{__name__}.align_metrics_to_active_object", text='Align Metrics to Active Object') - layout.row().operator( - f"{__name__}.temporaryhelper", text='Debug Function Do Not Use') - + box.row().operator(f"{__name__}.align_metrics_to_active_object", text='Align Metrics to Active Object') + layout.row().operator(f"{__name__}.temporaryhelper", text='Debug Function Do Not Use') class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): bl_label = "Text Properties" @@ -582,8 +511,7 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): bl_region_type = "UI" def get_active_text_properties(self): - # and bpy.context.object.select_get(): - if type(bpy.context.active_object) != type(None): + if type(bpy.context.active_object) != type(None):# and bpy.context.object.select_get(): for t in bpy.context.scene.abc3d_data.available_texts: if bpy.context.active_object == t.text_object: return t @@ -593,17 +521,17 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): # def font_items_callback(self, context): # items = [] - # fonts = Font.get_loaded_fonts_and_faces() + # fonts = Font.get_loaded_fonts_and_faces() # for f in fonts: - # items.append((f"{f[0]} {f[1]}", f"{f[0]} {f[1]}", "")) + # items.append((f"{f[0]} {f[1]}", f"{f[0]} {f[1]}", "")) # return items # def font_default_callback(self, context): # t = self.get_active_text_properties(self) # if type(t) != type(None): - # return f"{t.font_name} {t.face_name}" + # return f"{t.font_name} {t.face_name}" # else: - # return None + # return None # def font_update_callback(self, context): # font_name, face_name = self.font.split(" ") @@ -613,13 +541,13 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): # butils.set_text_on_curve(t) # font: bpy.props.EnumProperty( - # items=font_items_callback, - # default=font_default_callback, - # update=font_update_callback, - # ) + # items=font_items_callback, + # default=font_default_callback, + # update=font_update_callback, + # ) @classmethod - def poll(self, context): + def poll(self,context): return type(self.get_active_text_properties(self)) != type(None) def draw(self, context): @@ -635,7 +563,7 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): # as then polling does not work # however, we are paranoid return - + layout.label(text=f"Mom: {props.text_object.name}") layout.row().prop(props, "font") layout.row().prop(props, "text") @@ -647,7 +575,6 @@ class ABC3D_PT_TextPropertiesPanel(bpy.types.Panel): layout.column().prop(props, "translation") layout.column().prop(props, "orientation") - class ABC3D_OT_InstallFont(bpy.types.Operator): """Install or load Fontfile from path above. (Format must be *.glb or *.gltf)""" @@ -662,18 +589,18 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): print(f"{self.font_path} does not exist") font_path: bpy.props.StringProperty( - name="Font path", - description="Install a *.glb or *.gltf fontfile from disk", - default="", - maxlen=1024, - # update=font_path_update_callback, - subtype="FILE_PATH") + name="Font path", + description="Install a *.glb or *.gltf fontfile from disk", + default="", + maxlen=1024, + # update=font_path_update_callback, + subtype="FILE_PATH") install_in_assets: bpy.props.BoolProperty( - name="install in assets", - description="install the font in the assets directory of the addon", - default=True, - ) + name="install in assets", + description="install the font in the assets directory of the addon", + default=True, + ) load_into_memory: bpy.props.BoolProperty(name="load font data into memory", description="if false, it will load font data on demand", @@ -687,8 +614,7 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): layout.row().prop(self, "install_in_assets") if not self.install_in_assets and not self.load_into_memory: layout.label(text="If the fontfile is not installed,") - layout.label( - text="and the font is not loaded in memory completely,") + layout.label(text="and the font is not loaded in memory completely,") layout.label(text="the fontfile should not be moved.") layout.row().prop(self, "load_into_memory") if self.load_into_memory: @@ -707,8 +633,8 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): message=[ f"We believe the font path ({self.font_path}) does not exist.", f"Did you select your fontfile in the field above the 'Install new font'-button?", - ], - ), first_interval=0.1) + ], + ), first_interval=0.1) return context.window_manager.invoke_props_dialog(self) def execute(self, context): @@ -723,11 +649,11 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): f"Could not install font.", f"We believe the font path ({self.font_path}) does not exist.", f"If this is an error, please let us know.", - ], - ) + ], + ) return {'CANCELLED'} - if self.install_in_assets: + if self.install_in_assets: preferences = getPreferences(context) filename = os.path.basename(self.font_path) target = os.path.join(preferences.assets_dir, "fonts", filename) @@ -735,42 +661,19 @@ class ABC3D_OT_InstallFont(bpy.types.Operator): os.makedirs(os.path.dirname(target), exist_ok=True) shutil.copyfile(self.font_path, target) # def register_load(target, load=False): - # print(f"registering installed fonts") + # print(f"registering installed fonts") # bpy.app.timers.register(lambda: register_load(target, self.load_into_memory), first_interval=5) butils.register_font_from_filepath(target) if self.load_into_memory: butils.load_font_from_filepath(target) butils.update_available_fonts() - else: + else: butils.register_font_from_filepath(self.font_path) if self.load_into_memory: butils.load_font_from_filepath(self.font_path) return {'FINISHED'} - -class ABC3D_OT_OpenAssetDirectory(bpy.types.Operator): - """Open Asset Directory""" - bl_idname = f"{__name__}.open_asset_directory" - bl_label = "Opens asset directory." - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - preferences = getPreferences(context) - directory = os.path.realpath(preferences.assets_dir) - if os.path.exists(directory): - utils.open_file_browser(directory) - return {'FINISHED'} - else: - butils.ShowMessageBox( - title=f"{__name__} Warning", - icon="ERROR", - message=("Asset directory does not exist.", - f"Command failed trying to access '{directory}'.", - "Please, make sure it exists or chose another directory.")) - return {'CANCELLED'} - - class ABC3D_OT_LoadInstalledFonts(bpy.types.Operator): """Load installed fontfiles from datapath.""" bl_idname = f"{__name__}.load_installed_fonts" @@ -811,7 +714,6 @@ class ABC3D_OT_LoadInstalledFonts(bpy.types.Operator): return {'FINISHED'} - class ABC3D_OT_LoadFont(bpy.types.Operator): """Load all glyphs from a specific font in memory.\nThis can take a while and slow down Blender.""" bl_idname = f"{__name__}.load_font" @@ -827,7 +729,6 @@ class ABC3D_OT_LoadFont(bpy.types.Operator): butils.load_font_from_filepath(f) return {'FINISHED'} - class ABC3D_OT_AddDefaultMetrics(bpy.types.Operator): """Add default metrics to selected objects""" bl_idname = f"{__name__}.add_default_metrics" @@ -839,7 +740,6 @@ class ABC3D_OT_AddDefaultMetrics(bpy.types.Operator): butils.add_default_metrics_to_objects(objects) return {'FINISHED'} - class ABC3D_OT_RemoveMetrics(bpy.types.Operator): """Remove metrics from selected objects""" bl_idname = f"{__name__}.remove_metrics" @@ -851,7 +751,6 @@ class ABC3D_OT_RemoveMetrics(bpy.types.Operator): butils.remove_metrics_from_objects(objects) return {'FINISHED'} - class ABC3D_OT_AlignMetricsToActiveObject(bpy.types.Operator): """Align metrics of selected objects to metrics of active object""" bl_idname = f"{__name__}.align_metrics_to_active_object" @@ -863,7 +762,6 @@ class ABC3D_OT_AlignMetricsToActiveObject(bpy.types.Operator): butils.align_metrics_of_objects_to_active_object(objects) return {'FINISHED'} - class ABC3D_OT_AlignMetrics(bpy.types.Operator): """Align metrics of selected objects to each other""" bl_idname = f"{__name__}.align_metrics" @@ -875,7 +773,6 @@ class ABC3D_OT_AlignMetrics(bpy.types.Operator): butils.align_metrics_of_objects(objects) return {'FINISHED'} - class ABC3D_OT_TemporaryHelper(bpy.types.Operator): """Temporary Helper ABC3D\nThis could do anything.\nIt's just there to make random functions available for testing.""" bl_idname = f"{__name__}.temporaryhelper" @@ -889,21 +786,20 @@ class ABC3D_OT_TemporaryHelper(bpy.types.Operator): # butils.load_font_from_filepath("/home/jrkb/.config/blender/4.1/datafiles/abc3d/fonts/NM_Origin.glb") butils.update_available_fonts() - + # objects = bpy.context.selected_objects # butils.add_default_metrics_to_objects(objects) # reference_bound_box = None # for o in objects: - # bb = o.bound_box - # reference_bound_box = butils.get_max_bound_box(bb, reference_bound_box) + # bb = o.bound_box + # reference_bound_box = butils.get_max_bound_box(bb, reference_bound_box) # for o in objects: - # metrics = butils.get_metrics_bound_box(o.bound_box, reference_bound_box) - # butils.add_metrics_obj_from_bound_box(o, metrics) + # metrics = butils.get_metrics_bound_box(o.bound_box, reference_bound_box) + # butils.add_metrics_obj_from_bound_box(o, metrics) # bpy.app.timers.register(lambda: butils.remove_metrics_from_objects(objects), first_interval=5) return {'FINISHED'} - class ABC3D_OT_RemoveText(bpy.types.Operator): """Remove Text 3D""" bl_idname = f"{__name__}.remove_text" @@ -911,9 +807,9 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): bl_options = {'REGISTER', 'UNDO'} remove_objects: bpy.props.BoolProperty( - name="Remove Objects", - description="Remove both ABC3D text functionality and the objects/meshes", - default=True) + name="Remove Objects", + description="Remove both ABC3D text functionality and the objects/meshes", + default=True) def invoke(self, context, event): wm = context.window_manager @@ -923,26 +819,25 @@ class ABC3D_OT_RemoveText(bpy.types.Operator): abc3d_data = context.scene.abc3d_data if abc3d_data.active_text_index < 0: butils.ShowMessageBox( - title="No text selected", - message=("Please select a text."), - icon='GHOST_ENABLED') + title="No text selected", + message=("Please select a text."), + icon='GHOST_ENABLED') return {'CANCELLED'} i = abc3d_data.active_text_index if type(abc3d_data.available_texts[i].text_object) != type(None): mom = abc3d_data.available_texts[i].text_object - def delif(o, p): if p in o: del o[p] - delif(mom, f"{utils.prefix()}_linked_textobject") - delif(mom, f"{utils.prefix()}_font_name") - delif(mom, f"{utils.prefix()}_face_name") - delif(mom, f"{utils.prefix()}_font_size") - delif(mom, f"{utils.prefix()}_letter_spacing") - delif(mom, f"{utils.prefix()}_orientation") - delif(mom, f"{utils.prefix()}_translation") - delif(mom, f"{utils.prefix()}_offset") + delif(mom,f"{utils.prefix()}_linked_textobject") + delif(mom,f"{utils.prefix()}_font_name") + delif(mom,f"{utils.prefix()}_face_name") + delif(mom,f"{utils.prefix()}_font_size") + delif(mom,f"{utils.prefix()}_letter_spacing") + delif(mom,f"{utils.prefix()}_orientation") + delif(mom,f"{utils.prefix()}_translation") + delif(mom,f"{utils.prefix()}_offset") if self.remove_objects: remove_list = [] for g in abc3d_data.available_texts[i].glyphs: @@ -963,7 +858,7 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): def font_items_callback(self, context): items = [] - fonts = Font.get_loaded_fonts_and_faces() + fonts = Font.get_loaded_fonts_and_faces() for f in fonts: items.append((f"{f[0]} {f[1]}", f"{f[0]} {f[1]}", "")) return items @@ -974,50 +869,50 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): self.face_name = face_name font_name: bpy.props.StringProperty( - options={'HIDDEN'} - ) + options={'HIDDEN'} + ) face_name: bpy.props.StringProperty( - options={'HIDDEN'} - ) + options={'HIDDEN'} + ) font: bpy.props.EnumProperty(items=font_items_callback, update=font_update_callback ) text: bpy.props.StringProperty( - name="Text", - description="The text.", - default="ABC3D", - maxlen=1024, - ) + name="Text", + description="The text.", + default="ABC3D", + maxlen=1024, + ) # target_object: bpy.props.PointerProperty( - # name="The Target Object", - # description="The target, which will be populated by character children of text.", - # type=bpy.types.Object, - # ) + # name="The Target Object", + # description="The target, which will be populated by character children of text.", + # type=bpy.types.Object, + # ) letter_spacing: bpy.props.FloatProperty( - name="Letter Spacing", - description="Letter Spacing", - default=0.0, - ) + name="Letter Spacing", + description="Letter Spacing", + default=0.0, + ) font_size: bpy.props.FloatProperty( - name="Font Size", - default=1.0, - subtype='NONE', - ) + name="Font Size", + default=1.0, + subtype='NONE', + ) offset: bpy.props.FloatProperty( - name="Offset", - default=0.0, - subtype='NONE', - ) + name="Offset", + default=0.0, + subtype='NONE', + ) translation: bpy.props.FloatVectorProperty( - name="Translation", - default=(0.0, 0.0, 0.0), - subtype='TRANSLATION', - ) + name="Translation", + default=(0.0, 0.0, 0.0), + subtype='TRANSLATION', + ) orientation: bpy.props.FloatVectorProperty( - name="Orientation", - default=(1.5707963267948966, 0.0, 0.0), # 90 degrees in radians - subtype='EULER', - ) + name="Orientation", + default=(1.5707963267948966, 0.0, 0.0), # 90 degrees in radians + subtype='EULER', + ) def invoke(self, context, event): wm = context.window_manager @@ -1045,7 +940,7 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): text_id = text_id + 1 t = abc3d_data.available_texts.add() # If you wish to set a value and not fire an update, set the id property. - # A property defined via bpy.props for example ob.prop is stored as ob["prop"] once set to non default. + # A property defined via bpy.props for example ob.prop is stored as ob["prop"] once set to non default. t['text_id'] = text_id # t['font'] = self.font # enums want to be set as attribute t['font_name'] = self.font_name @@ -1058,27 +953,26 @@ class ABC3D_OT_PlaceText(bpy.types.Operator): t['translation'] = self.translation t['orientation'] = self.orientation t['distribution_type'] = distribution_type - t.font = self.font # enums want to be set as attribute - # this also calls the update function - # so we don't need to prepare/set again + t.font = self.font # enums want to be set as attribute + # this also calls the update function + # so we don't need to prepare/set again # no need for these: # butils.prepare_text(t.font_name, - # t.face_name, - # t.text) + # t.face_name, + # t.text) # or this: # butils.set_text_on_curve(t) # else: butils.ShowMessageBox( - title="No object selected", - message=( - "Please select an object.", - "It will be used to put the type on.", - "Thank you :)"), - icon='GHOST_ENABLED') + title="No object selected", + message=( + "Please select an object.", + "It will be used to put the type on.", + "Thank you :)"), + icon='GHOST_ENABLED') return {'FINISHED'} - class ABC3D_OT_ToggleABC3DCollection(bpy.types.Operator): """Toggle ABC3D Collection""" bl_idname = f"{__name__}.toggle_abc3d_collection" @@ -1090,8 +984,7 @@ class ABC3D_OT_ToggleABC3DCollection(bpy.types.Operator): fontcollection = bpy.data.collections.get("ABC3D") if fontcollection is None: - self.report( - {'INFO'}, f"{bl_info['name']}: There is no collection. Did you use or create any glyphs yet?") + self.report({'INFO'}, f"{bl_info['name']}: There is no collection. Did you use or create any glyphs yet?") elif scene.collection.children.find(fontcollection.name) < 0: scene.collection.children.link(fontcollection) self.report({'INFO'}, f"{bl_info['name']}: show collection") @@ -1113,32 +1006,27 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): preferences = getPreferences(context) abc3d_data = context.scene.abc3d_data if abc3d_data.export_dir == "": - abc3d_data.export_dir = os.path.join( - preferences.assets_dir, "fonts") + abc3d_data.export_dir = os.path.join(preferences.assets_dir, "fonts") return wm.invoke_props_dialog(self) def draw(self, context): abc3d_data = context.scene.abc3d_data layout = self.layout layout.label(text="Available Fonts") - layout.template_list("ABC3D_UL_fonts", "", abc3d_data, - "available_fonts", abc3d_data, "active_font_index") + layout.template_list("ABC3D_UL_fonts", "", abc3d_data, "available_fonts", abc3d_data, "active_font_index") available_font = abc3d_data.available_fonts[abc3d_data.active_font_index] font_name = available_font.font_name face_name = available_font.face_name - loaded_glyphs = sorted( - Font.fonts[font_name].faces[face_name].loaded_glyphs) + loaded_glyphs = sorted(Font.fonts[font_name].faces[face_name].loaded_glyphs) n = 16 n_rows = int(len(loaded_glyphs) / n) box = layout.box() box.row().label(text=f"Glyphs to be exported:") subbox = box.box() - for i in range(0, n_rows + 1): - text = ''.join([f"{u}" for ui, u in enumerate( - loaded_glyphs) if ui < (i+1) * n and ui >= i * n]) + for i in range(0, n_rows + 1): + text = ''.join([f"{u}" for ui, u in enumerate(loaded_glyphs) if ui < (i+1) * n and ui >= i * n]) scale_y = 0.5 - row = subbox.row() - row.scale_y = scale_y + row = subbox.row(); row.scale_y = scale_y row.label(text=text) layout.prop(abc3d_data, 'export_dir') @@ -1155,18 +1043,15 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): return {'CANCELLED'} if abc3d_data.active_font_index < 0: - self.report( - {'INFO'}, f"{bl_info['name']}: There is no active font") + self.report({'INFO'}, f"{bl_info['name']}: There is no active font") return {'CANCELLED'} if len(abc3d_data.available_fonts) <= abc3d_data.active_font_index: - self.report( - {'INFO'}, f"{bl_info['name']}: Active font is not available") + self.report({'INFO'}, f"{bl_info['name']}: Active font is not available") return {'CANCELLED'} # save state to restore later - was_fontcollection_linked = scene.collection.children.find( - fontcollection.name) >= 0 + was_fontcollection_linked = scene.collection.children.find(fontcollection.name) >= 0 was_selection = [] for obj in bpy.context.selected_objects: was_selection.append(obj) @@ -1178,8 +1063,7 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): selected_font = abc3d_data.available_fonts[abc3d_data.active_font_index] # print(selected_font.font_name) - self.report( - {'INFO'}, f"{bl_info['name']}: {selected_font.font_name} {selected_font.face_name}") + self.report({'INFO'}, f"{bl_info['name']}: {selected_font.font_name} {selected_font.face_name}") preferences = getPreferences(context) print(f"assets folder: {preferences.assets_dir}") @@ -1218,14 +1102,12 @@ class ABC3D_OT_SaveFontToFile(bpy.types.Operator): bpy.ops.export_scene.gltf( filepath=filepath, check_existing=False, - # GLB or GLTF_SEPARATE (also change filepath) - export_format='GLB', + export_format='GLB', # GLB or GLTF_SEPARATE (also change filepath) export_extras=True, use_selection=True, use_active_scene=True, ) - bpy.app.timers.register( - lambda: bpy.ops.scene.delete(), first_interval=1) + bpy.app.timers.register(lambda: bpy.ops.scene.delete(), first_interval=1) # bpy.ops.scene.delete() # restore() @@ -1251,20 +1133,20 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): bl_options = {'REGISTER', 'UNDO'} font_name: bpy.props.StringProperty( - default="NM_Origin", - ) + default="NM_Origin", + ) face_name: bpy.props.StringProperty( - default="Tender", - ) + default="Tender", + ) import_infix: bpy.props.StringProperty( - default="_NM_Origin_Tender", - ) + default="_NM_Origin_Tender", + ) autodetect_names: bpy.props.BoolProperty( - default=True, - ) + default=True, + ) fix_common_misspellings: bpy.props.BoolProperty( - default=True, - ) + default=True, + ) def invoke(self, context, event): wm = context.window_manager @@ -1280,42 +1162,29 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): row.prop(self, 'autodetect_names') if self.autodetect_names: scale_y = 0.5 - row = layout.row() - row.scale_y = scale_y - row.label( - text="Watch out, follow convention in naming your meshes:") - row = layout.row() - row.scale_y = scale_y + row = layout.row(); row.scale_y = scale_y + row.label(text="Watch out, follow convention in naming your meshes:") + row = layout.row(); row.scale_y = scale_y row.label(text="'__'") - row = layout.row() - row.scale_y = scale_y + row = layout.row(); row.scale_y = scale_y row.label(text=" - glyph id: unicode glyph name or raw glyph") - row = layout.row() - row.scale_y = scale_y + row = layout.row(); row.scale_y = scale_y row.label(text=" - font name: font name with underscore") - row = layout.row() - row.scale_y = scale_y + row = layout.row(); row.scale_y = scale_y row.label(text=" - face name: face name") - row = layout.row() - row.scale_y = scale_y + row = layout.row(); row.scale_y = scale_y row.label(text="working examples:") - row = layout.row() - row.scale_y = scale_y + row = layout.row(); row.scale_y = scale_y row.label(text="- 'A_NM_Origin_Tender'") - row = layout.row() - row.scale_y = scale_y + row = layout.row(); row.scale_y = scale_y row.label(text="- 'B_NM_Origin_Tender'") - row = layout.row() - row.scale_y = scale_y + row = layout.row(); row.scale_y = scale_y row.label(text="- 'arrowright_NM_Origin_Tender'") - row = layout.row() - row.scale_y = scale_y + row = layout.row(); row.scale_y = scale_y row.label(text="- '→_NM_Origin_Tender' (equal to above)") - row = layout.row() - row.scale_y = scale_y + row = layout.row(); row.scale_y = scale_y row.label(text="- 'quotesingle_NM_Origin_Tender.001'") - row = layout.row() - row.scale_y = scale_y + row = layout.row(); row.scale_y = scale_y row.label(text="- 'colon_NM_Origin_Tender_2'") box = layout.box() box.enabled = not self.autodetect_names @@ -1328,10 +1197,8 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): character = "" if Font.known_misspellings[k] in Font.name_to_glyph_d: character = f" ({Font.name_to_glyph_d[Font.known_misspellings[k]]})" - row = layout.row() - row.scale_y = 0.5 - row.label( - text=f"{k} → {Font.known_misspellings[k]}{character}") + row = layout.row(); row.scale_y = 0.5 + row.label(text=f"{k} → {Font.known_misspellings[k]}{character}") def execute(self, context): print(f"executing {self.bl_idname}") @@ -1366,8 +1233,7 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): if self.autodetect_names: ifxsplit = o.name.split('_') if len(ifxsplit) < 4: - print( - f"whoops name could not be autodetected {o.name}") + print(f"whoops name could not be autodetected {o.name}") continue font_name = f"{ifxsplit[1]}_{ifxsplit[2]}" face_name = ifxsplit[3] @@ -1381,25 +1247,25 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): name = o.name.split('_')[0] glyph_id = Font.name_to_glyph(name) - if type(glyph_id) != type(None): + if type(glyph_id )!= type(None): o["glyph"] = glyph_id o["font_name"] = font_name o["face_name"] = face_name # butils.apply_all_transforms(o) butils.move_in_fontcollection( - o, - fontcollection) + o, + fontcollection) Font.add_glyph( - font_name, - face_name, - glyph_id, - o) + font_name, + face_name, + glyph_id, + o) - # TODO: is there a better way to iterate over a CollectionProperty? + #TODO: is there a better way to iterate over a CollectionProperty? found = False for f in abc3d_data.available_fonts.values(): if (f.font_name == font_name - and f.face_name == face_name): + and f.face_name == face_name): found = True break if not found: @@ -1408,14 +1274,11 @@ class ABC3D_OT_CreateFontFromObjects(bpy.types.Operator): f.face_name = face_name else: - print( - f"import warning: did not understand glyph {name}") - self.report( - {'INFO'}, f"did not understand glyph {name}") + print(f"import warning: did not understand glyph {name}") + self.report({'INFO'}, f"did not understand glyph {name}") return {'FINISHED'} - class ABC3D_PT_RightPropertiesPanel(bpy.types.Panel): """Creates a Panel in the Object properties window""" bl_label = f"{bl_info['name']}" @@ -1425,12 +1288,10 @@ class ABC3D_PT_RightPropertiesPanel(bpy.types.Panel): bl_context = "object" @classmethod - def poll(self, context): + def poll(self,context): # only show the panel, if it's a textobject or a glyph - is_text = type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == - context.active_object), None)) != type(None) - is_glyph = type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == - context.active_object.parent), None)) != type(None) + is_text = type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object), None)) != type(None) + is_glyph = type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object.parent), None)) != type(None) return is_text or is_glyph def draw(self, context): @@ -1442,7 +1303,6 @@ class ABC3D_PT_RightPropertiesPanel(bpy.types.Panel): def is_it_text(): return type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object), None)) != type(None) - def is_it_glyph(): return type(next((t for t in context.scene.abc3d_data.available_texts if t.text_object == context.active_object.parent), None)) != type(None) @@ -1474,63 +1334,59 @@ class ABC3D_PT_RightPropertiesPanel(bpy.types.Panel): if is_glyph: layout.row().label(text="Glyph Properties:") - class ABC3D_OT_Reporter(bpy.types.Operator): bl_idname = f"{__name__}.reporter" bl_label = "Report" label = bpy.props.StringProperty( - name="label", - default="INFO", - ) + name="label", + default="INFO", + ) message = bpy.props.StringProperty( - name="message", - default="I have nothing to say really", - ) + name="message", + default="I have nothing to say really", + ) def execute(self, context): - # this is where I send the message + #this is where I send the message self.report({'INFO'}, 'whatever') - for i in range(0, 10): - butils.ShowMessageBox('whatever', 'INFO', 'INFO') + for i in range(0,10): + butils.ShowMessageBox('whatever','INFO','INFO') return {'FINISHED'} - classes = ( - bimport.ImportGLTF2, - bimport.GetFontFacesInFile, - ABC3D_addonPreferences, - ABC3D_available_font, - ABC3D_glyph_properties, - ABC3D_text_properties, - ABC3D_data, - ABC3D_UL_fonts, - ABC3D_UL_texts, - ABC3D_PT_Panel, - ABC3D_PT_LoadFontPanel, - ABC3D_PT_FontList, - ABC3D_PT_TextPlacement, - ABC3D_PT_TextManagement, - ABC3D_PT_FontCreation, - ABC3D_PT_TextPropertiesPanel, - ABC3D_OT_OpenAssetDirectory, - ABC3D_OT_LoadInstalledFonts, - ABC3D_OT_LoadFont, - ABC3D_OT_AddDefaultMetrics, - ABC3D_OT_RemoveMetrics, - ABC3D_OT_AlignMetricsToActiveObject, - ABC3D_OT_AlignMetrics, - ABC3D_OT_TemporaryHelper, - ABC3D_OT_RemoveText, - ABC3D_OT_PlaceText, - ABC3D_OT_InstallFont, - ABC3D_OT_ToggleABC3DCollection, - ABC3D_OT_SaveFontToFile, - ABC3D_OT_CreateFontFromObjects, - ABC3D_PT_RightPropertiesPanel, - ABC3D_OT_Reporter, -) - + bimport.ImportGLTF2, + bimport.GetFontFacesInFile, + ABC3D_addonPreferences, + ABC3D_available_font, + ABC3D_glyph_properties, + ABC3D_text_properties, + ABC3D_data, + ABC3D_UL_fonts, + ABC3D_UL_texts, + ABC3D_PT_Panel, + ABC3D_PT_LoadFontPanel, + ABC3D_PT_FontList, + ABC3D_PT_TextPlacement, + ABC3D_PT_TextManagement, + ABC3D_PT_FontCreation, + ABC3D_PT_TextPropertiesPanel, + ABC3D_OT_LoadInstalledFonts, + ABC3D_OT_LoadFont, + ABC3D_OT_AddDefaultMetrics, + ABC3D_OT_RemoveMetrics, + ABC3D_OT_AlignMetricsToActiveObject, + ABC3D_OT_AlignMetrics, + ABC3D_OT_TemporaryHelper, + ABC3D_OT_RemoveText, + ABC3D_OT_PlaceText, + ABC3D_OT_InstallFont, + ABC3D_OT_ToggleABC3DCollection, + ABC3D_OT_SaveFontToFile, + ABC3D_OT_CreateFontFromObjects, + ABC3D_PT_RightPropertiesPanel, + ABC3D_OT_Reporter, + ) def compare_text_object_with_object(t, o, strict=False): for k in o.keys(): @@ -1538,7 +1394,7 @@ def compare_text_object_with_object(t, o, strict=False): if o[k] != "textobject": return False elif k.startswith(f"{utils.prefix()}_"): - p = k.replace(f"{utils.prefix()}_", "") + p = k.replace(f"{utils.prefix()}_","") if p in t.keys(): if t[p] != o[k]: return False @@ -1547,10 +1403,9 @@ def compare_text_object_with_object(t, o, strict=False): if strict: return False # for p in t.keys(): - # if + # if return True - def detect_text(): scene = bpy.context.scene abc3d_data = scene.abc3d_data @@ -1560,11 +1415,9 @@ def detect_text(): if len(abc3d_data.available_texts) > linked_textobject \ and abc3d_data.available_texts[linked_textobject].text_object == o: t = abc3d_data.available_texts[linked_textobject] - a = test_availability( - o["font_name"], o["face_name"], o["text"]) + a = test_availability(o["font_name"], o["face_name"], o["text"]) butils.transfer_blender_object_to_text_properties(o, t) - def load_used_glyphs(): print("LOAD USED GLYPHS") scene = bpy.context.scene @@ -1598,39 +1451,18 @@ def load_handler(self, dummy): butils.run_in_main_thread(bpy.ops.abc3d.load_installed_fonts) butils.run_in_main_thread(load_used_glyphs) - def load_handler_unload(): if bpy.app.timers.is_registered(butils.execute_queued_functions): bpy.app.timers.unregister(butils.execute_queued_functions) - @persistent def on_frame_changed(self, dummy): for t in bpy.context.scene.abc3d_data.available_texts: # TODO PERFORMANCE: only on demand butils.set_text_on_curve(t) - -@persistent -def on_depsgraph_update(scene, depsgraph): - for u in depsgraph.updates: - if f"{utils.prefix()}_linked_textobject" in u.id.keys() \ - and f"{utils.prefix()}_type" in u.id.keys() \ - and u.id[f"{utils.prefix()}_type"] == 'textobject': - linked_textobject = u.id[f"{utils.prefix()}_linked_textobject"] - if u.is_updated_geometry and len(scene.abc3d_data.available_texts) > linked_textobject and not "prevent_recursion" in u.id: - u.id["prevent_recursion"] = True - butils.set_text_on_curve( - scene.abc3d_data.available_texts[linked_textobject]) - elif "prevent_recursion" in u.id.keys(): - del u.id["prevent_recursion"] - - def register(): - addon_updater_ops.register(bl_info) - for cls in classes: - addon_updater_ops.make_annotations(cls) # Avoid blender 2.8 warnings. bpy.utils.register_class(cls) bpy.types.Scene.abc3d_data = bpy.props.PointerProperty(type=ABC3D_data) # bpy.types.Object.__del__ = lambda self: print(f"Bye {self.name}") @@ -1645,23 +1477,16 @@ def register(): if on_frame_changed not in bpy.app.handlers.frame_change_post: bpy.app.handlers.frame_change_post.append(on_frame_changed) - if on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post: - bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update) - butils.run_in_main_thread(butils.clear_available_fonts) # butils.run_in_main_thread(butils.load_installed_fonts) butils.run_in_main_thread(butils.update_available_fonts) - butils.run_in_main_thread(butils.update_types) # bpy.ops.abc3d.load_installed_fonts() Font.name_to_glyph_d = Font.generate_name_to_glyph_d() - def unregister(): - addon_updater_ops.unregister() - - for cls in reversed(classes): + for cls in classes: bpy.utils.unregister_class(cls) # remove autostart when loading blend file @@ -1676,6 +1501,6 @@ def unregister(): del bpy.types.Scene.abc3d_data print(f"UNREGISTER {bl_info['name']}") - if __name__ == '__main__': register() + diff --git a/_vimrc_local.vim b/_vimrc_local.vim index 1733d50..51e03a8 100644 --- a/_vimrc_local.vim +++ b/_vimrc_local.vim @@ -6,15 +6,12 @@ let g:jedi#environment_path = "venv" """"""""""""""""""""""""""""""""" ALE -"let g:ale_python_pylint_executable = '/home/jrkb/git/pointer/neomatter/font3d/abc3d/venv/bin/pylint' -"let g:ale_python_executable='/home/jrkb/git/pointer/neomatter/font3d/abc3d/venv/bin/python' +"let g:ale_python_pylint_executable = '/home/jrkb/git/pointer/neomatter/font3d/font3d_blender_addon/venv/bin/pylint' +"let g:ale_python_executable='/home/jrkb/git/pointer/neomatter/font3d/font3d_blender_addon/venv/bin/python' "let g:ale_python_pylint_use_global=1 "let g:ale_use_global_executables=1 "let g:ale_python_auto_pipenv=1 "let g:ale_python_auto_virtualenv=1 "let g:ale_virtualenv_dir_names = ['venv'] -"let g:ale_linters = { 'javascript': ['eslint', 'tsserver'], 'python': ['jedils', 'pylint', 'flake8'], 'cpp': ['cc', 'clangcheck', 'clangd', 'clangtidy', 'clazy', 'cppcheck', 'cpplint', 'cquery', 'cspell', 'flawfinder'], 'php': ['php_cs_fixer'] } -"let g:ale_fixers = { '*': ['remove_trailing_lines', 'trim_whitespace'], 'python': ['autopep8'], 'cpp': ['uncrustify'], 'javascript': js_fixers, 'css': ['prettier'], 'json': ['prettier'], 'php': ['php_cs_fixer'] } - let g:ale_pattern_options = {'\.py$': {'ale_enabled': 0}} diff --git a/addon_updater.py b/addon_updater.py deleted file mode 100644 index f1ac048..0000000 --- a/addon_updater.py +++ /dev/null @@ -1,1787 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -""" -See documentation for usage -https://github.com/CGCookie/blender-addon-updater -""" - -__version__ = "1.1.1" - -import errno -import traceback -import platform -import ssl -import urllib.request -import urllib -import os -import json -import zipfile -import shutil -import threading -import fnmatch -from datetime import datetime, timedelta - -# Blender imports, used in limited cases. -import bpy -import addon_utils - -# ----------------------------------------------------------------------------- -# The main class -# ----------------------------------------------------------------------------- - - -class SingletonUpdater: - """Addon updater service class. - - This is the singleton class to instance once and then reference where - needed throughout the addon. It implements all the interfaces for running - updates. - """ - def __init__(self): - - self._engine = ForgejoEngine() - self._user = None - self._repo = None - self._website = None - self._current_version = None - self._subfolder_path = None - self._tags = list() - self._tag_latest = None - self._tag_names = list() - self._latest_release = None - self._use_releases = False - self._include_branches = False - self._include_branch_list = ['master'] - self._include_branch_auto_check = False - self._manual_only = False - self._version_min_update = None - self._version_max_update = None - - # By default, backup current addon on update/target install. - self._backup_current = True - self._backup_ignore_patterns = None - - # Set patterns the files to overwrite during an update. - self._overwrite_patterns = ["*.py", "*.pyc"] - self._remove_pre_update_patterns = list() - - # By default, don't auto disable+re-enable the addon after an update, - # as this is less stable/often won't fully reload all modules anyways. - self._auto_reload_post_update = False - - # Settings for the frequency of automated background checks. - self._check_interval_enabled = False - self._check_interval_months = 0 - self._check_interval_days = 7 - self._check_interval_hours = 0 - self._check_interval_minutes = 0 - - # runtime variables, initial conditions - self._verbose = False - self._use_print_traces = True - self._fake_install = False - self._async_checking = False # only true when async daemon started - self._update_ready = None - self._update_link = None - self._update_version = None - self._source_zip = None - self._check_thread = None - self._select_link = None - self.skip_tag = None - - # Get data from the running blender module (addon). - self._addon = __package__.lower() - self._addon_package = __package__ # Must not change. - self._updater_path = os.path.join( - os.path.dirname(__file__), self._addon + "_updater") - self._addon_root = os.path.dirname(__file__) - self._json = dict() - self._error = None - self._error_msg = None - self._prefiltered_tag_count = 0 - - # UI properties, not used within this module but still useful to have. - - # to verify a valid import, in place of placeholder import - self.show_popups = True # UI uses to show popups or not. - self.invalid_updater = False - - # pre-assign basic select-link function - def select_link_function(self, tag): - return tag["zipball_url"] - - self._select_link = select_link_function - - def print_trace(self): - """Print handled exception details when use_print_traces is set""" - if self._use_print_traces: - traceback.print_exc() - - def print_verbose(self, msg): - """Print out a verbose logging message if verbose is true.""" - if not self._verbose: - return - print("{} addon: ".format(self.addon) + msg) - - # ------------------------------------------------------------------------- - # Getters and setters - # ------------------------------------------------------------------------- - @property - def addon(self): - return self._addon - - @addon.setter - def addon(self, value): - self._addon = str(value) - - @property - def api_url(self): - return self._engine.api_url - - @api_url.setter - def api_url(self, value): - if not self.check_is_url(value): - raise ValueError("Not a valid URL: " + value) - self._engine.api_url = value - - @property - def async_checking(self): - return self._async_checking - - @property - def auto_reload_post_update(self): - return self._auto_reload_post_update - - @auto_reload_post_update.setter - def auto_reload_post_update(self, value): - try: - self._auto_reload_post_update = bool(value) - except: - raise ValueError("auto_reload_post_update must be a boolean value") - - @property - def backup_current(self): - return self._backup_current - - @backup_current.setter - def backup_current(self, value): - if value is None: - self._backup_current = False - else: - self._backup_current = value - - @property - def backup_ignore_patterns(self): - return self._backup_ignore_patterns - - @backup_ignore_patterns.setter - def backup_ignore_patterns(self, value): - if value is None: - self._backup_ignore_patterns = None - elif not isinstance(value, list): - raise ValueError("Backup pattern must be in list format") - else: - self._backup_ignore_patterns = value - - @property - def check_interval(self): - return (self._check_interval_enabled, - self._check_interval_months, - self._check_interval_days, - self._check_interval_hours, - self._check_interval_minutes) - - @property - def current_version(self): - return self._current_version - - @current_version.setter - def current_version(self, tuple_values): - if tuple_values is None: - self._current_version = None - return - elif type(tuple_values) is not tuple: - try: - tuple(tuple_values) - except: - raise ValueError( - "current_version must be a tuple of integers") - for i in tuple_values: - if type(i) is not int: - raise ValueError( - "current_version must be a tuple of integers") - self._current_version = tuple(tuple_values) - - @property - def engine(self): - return self._engine.name - - @engine.setter - def engine(self, value): - engine = value.lower() - if engine == "github": - self._engine = GithubEngine() - elif engine == "gitlab": - self._engine = GitlabEngine() - elif engine == "bitbucket": - self._engine = BitbucketEngine() - elif engine == "forgejo": - self._engine = ForgejoEngine() - else: - raise ValueError("Invalid engine selection") - - @property - def error(self): - return self._error - - @property - def error_msg(self): - return self._error_msg - - @property - def fake_install(self): - return self._fake_install - - @fake_install.setter - def fake_install(self, value): - if not isinstance(value, bool): - raise ValueError("fake_install must be a boolean value") - self._fake_install = bool(value) - - # not currently used - @property - def include_branch_auto_check(self): - return self._include_branch_auto_check - - @include_branch_auto_check.setter - def include_branch_auto_check(self, value): - try: - self._include_branch_auto_check = bool(value) - except: - raise ValueError("include_branch_autocheck must be a boolean") - - @property - def include_branch_list(self): - return self._include_branch_list - - @include_branch_list.setter - def include_branch_list(self, value): - try: - if value is None: - self._include_branch_list = ['master'] - elif not isinstance(value, list) or len(value) == 0: - raise ValueError( - "include_branch_list should be a list of valid branches") - else: - self._include_branch_list = value - except: - raise ValueError( - "include_branch_list should be a list of valid branches") - - @property - def include_branches(self): - return self._include_branches - - @include_branches.setter - def include_branches(self, value): - try: - self._include_branches = bool(value) - except: - raise ValueError("include_branches must be a boolean value") - - @property - def json(self): - if len(self._json) == 0: - self.set_updater_json() - return self._json - - @property - def latest_release(self): - if self._latest_release is None: - return None - return self._latest_release - - @property - def manual_only(self): - return self._manual_only - - @manual_only.setter - def manual_only(self, value): - try: - self._manual_only = bool(value) - except: - raise ValueError("manual_only must be a boolean value") - - @property - def overwrite_patterns(self): - return self._overwrite_patterns - - @overwrite_patterns.setter - def overwrite_patterns(self, value): - if value is None: - self._overwrite_patterns = ["*.py", "*.pyc"] - elif not isinstance(value, list): - raise ValueError("overwrite_patterns needs to be in a list format") - else: - self._overwrite_patterns = value - - @property - def private_token(self): - return self._engine.token - - @private_token.setter - def private_token(self, value): - if value is None: - self._engine.token = None - else: - self._engine.token = str(value) - - @property - def remove_pre_update_patterns(self): - return self._remove_pre_update_patterns - - @remove_pre_update_patterns.setter - def remove_pre_update_patterns(self, value): - if value is None: - self._remove_pre_update_patterns = list() - elif not isinstance(value, list): - raise ValueError( - "remove_pre_update_patterns needs to be in a list format") - else: - self._remove_pre_update_patterns = value - - @property - def repo(self): - return self._repo - - @repo.setter - def repo(self, value): - try: - self._repo = str(value) - except: - raise ValueError("repo must be a string value") - - @property - def select_link(self): - return self._select_link - - @select_link.setter - def select_link(self, value): - # ensure it is a function assignment, with signature: - # input self, tag; returns link name - if not hasattr(value, "__call__"): - raise ValueError("select_link must be a function") - self._select_link = value - - @property - def stage_path(self): - return self._updater_path - - @stage_path.setter - def stage_path(self, value): - if value is None: - self.print_verbose("Aborting assigning stage_path, it's null") - return - elif value is not None and not os.path.exists(value): - try: - os.makedirs(value) - except: - self.print_verbose("Error trying to staging path") - self.print_trace() - return - self._updater_path = value - - @property - def subfolder_path(self): - return self._subfolder_path - - @subfolder_path.setter - def subfolder_path(self, value): - self._subfolder_path = value - - @property - def tags(self): - if len(self._tags) == 0: - return list() - tag_names = list() - for tag in self._tags: - tag_names.append(tag["name"]) - return tag_names - - @property - def tag_latest(self): - if self._tag_latest is None: - return None - return self._tag_latest["name"] - - @property - def update_link(self): - return self._update_link - - @property - def update_ready(self): - return self._update_ready - - @property - def update_version(self): - return self._update_version - - @property - def use_releases(self): - return self._use_releases - - @use_releases.setter - def use_releases(self, value): - try: - self._use_releases = bool(value) - except: - raise ValueError("use_releases must be a boolean value") - - @property - def user(self): - return self._user - - @user.setter - def user(self, value): - try: - self._user = str(value) - except: - raise ValueError("User must be a string value") - - @property - def verbose(self): - return self._verbose - - @verbose.setter - def verbose(self, value): - try: - self._verbose = bool(value) - self.print_verbose("Verbose is enabled") - except: - raise ValueError("Verbose must be a boolean value") - - @property - def use_print_traces(self): - return self._use_print_traces - - @use_print_traces.setter - def use_print_traces(self, value): - try: - self._use_print_traces = bool(value) - except: - raise ValueError("use_print_traces must be a boolean value") - - @property - def version_max_update(self): - return self._version_max_update - - @version_max_update.setter - def version_max_update(self, value): - if value is None: - self._version_max_update = None - return - if not isinstance(value, tuple): - raise ValueError("Version maximum must be a tuple") - for subvalue in value: - if type(subvalue) is not int: - raise ValueError("Version elements must be integers") - self._version_max_update = value - - @property - def version_min_update(self): - return self._version_min_update - - @version_min_update.setter - def version_min_update(self, value): - if value is None: - self._version_min_update = None - return - if not isinstance(value, tuple): - raise ValueError("Version minimum must be a tuple") - for subvalue in value: - if type(subvalue) != int: - raise ValueError("Version elements must be integers") - self._version_min_update = value - - @property - def website(self): - return self._website - - @website.setter - def website(self, value): - if not self.check_is_url(value): - raise ValueError("Not a valid URL: " + value) - self._website = value - - # ------------------------------------------------------------------------- - # Parameter validation related functions - # ------------------------------------------------------------------------- - @staticmethod - def check_is_url(url): - if not ("http://" in url or "https://" in url): - return False - if "." not in url: - return False - return True - - def _get_tag_names(self): - tag_names = list() - self.get_tags() - for tag in self._tags: - tag_names.append(tag["name"]) - return tag_names - - def set_check_interval(self, enabled=False, - months=0, days=14, hours=0, minutes=0): - """Set the time interval between automated checks, and if enabled. - - Has enabled = False as default to not check against frequency, - if enabled, default is 2 weeks. - """ - - if type(enabled) is not bool: - raise ValueError("Enable must be a boolean value") - if type(months) is not int: - raise ValueError("Months must be an integer value") - if type(days) is not int: - raise ValueError("Days must be an integer value") - if type(hours) is not int: - raise ValueError("Hours must be an integer value") - if type(minutes) is not int: - raise ValueError("Minutes must be an integer value") - - if not enabled: - self._check_interval_enabled = False - else: - self._check_interval_enabled = True - - self._check_interval_months = months - self._check_interval_days = days - self._check_interval_hours = hours - self._check_interval_minutes = minutes - - def __repr__(self): - return "".format(a=__file__) - - def __str__(self): - return "Updater, with user: {a}, repository: {b}, url: {c}".format( - a=self._user, b=self._repo, c=self.form_repo_url()) - - # ------------------------------------------------------------------------- - # API-related functions - # ------------------------------------------------------------------------- - def form_repo_url(self): - return self._engine.form_repo_url(self) - - def form_tags_url(self): - return self._engine.form_tags_url(self) - - def form_branch_url(self, branch): - return self._engine.form_branch_url(branch, self) - - def get_tags(self): - request = self.form_tags_url() - self.print_verbose("Getting tags from server") - - # get all tags, internet call - all_tags = self._engine.parse_tags(self.get_api(request), self) - if all_tags is not None: - self._prefiltered_tag_count = len(all_tags) - else: - self._prefiltered_tag_count = 0 - all_tags = list() - - # pre-process to skip tags - if self.skip_tag is not None: - self._tags = [tg for tg in all_tags if not self.skip_tag(self, tg)] - else: - self._tags = all_tags - - # get additional branches too, if needed, and place in front - # Does NO checking here whether branch is valid - if self._include_branches: - temp_branches = self._include_branch_list.copy() - temp_branches.reverse() - for branch in temp_branches: - request = self.form_branch_url(branch) - include = { - "name": branch.title(), - "zipball_url": request - } - self._tags = [include] + self._tags # append to front - - if self._tags is None: - # some error occurred - self._tag_latest = None - self._tags = list() - - elif self._prefiltered_tag_count == 0 and not self._include_branches: - self._tag_latest = None - if self._error is None: # if not None, could have had no internet - self._error = "No releases found" - self._error_msg = "No releases or tags found in repository" - self.print_verbose("No releases or tags found in repository") - - elif self._prefiltered_tag_count == 0 and self._include_branches: - if not self._error: - self._tag_latest = self._tags[0] - branch = self._include_branch_list[0] - self.print_verbose("{} branch found, no releases: {}".format( - branch, self._tags[0])) - - elif ((len(self._tags) - len(self._include_branch_list) == 0 - and self._include_branches) - or (len(self._tags) == 0 and not self._include_branches) - and self._prefiltered_tag_count > 0): - self._tag_latest = None - self._error = "No releases available" - self._error_msg = "No versions found within compatible version range" - self.print_verbose(self._error_msg) - - else: - if not self._include_branches: - self._tag_latest = self._tags[0] - self.print_verbose( - "Most recent tag found:" + str(self._tags[0]['name'])) - else: - # Don't return branch if in list. - n = len(self._include_branch_list) - self._tag_latest = self._tags[n] # guaranteed at least len()=n+1 - self.print_verbose( - "Most recent tag found:" + str(self._tags[n]['name'])) - - def get_raw(self, url): - """All API calls to base url.""" - request = urllib.request.Request(url) - try: - context = ssl._create_unverified_context() - except: - # Some blender packaged python versions don't have this, largely - # useful for local network setups otherwise minimal impact. - context = None - - # Setup private request headers if appropriate. - if self._engine.token is not None: - if self._engine.name == "gitlab": - request.add_header('PRIVATE-TOKEN', self._engine.token) - else: - self.print_verbose("Tokens not setup for engine yet") - - # Always set user agent. - request.add_header( - 'User-Agent', "Python/" + str(platform.python_version())) - - # Run the request. - try: - if context: - result = urllib.request.urlopen(request, context=context) - else: - result = urllib.request.urlopen(request) - except urllib.error.HTTPError as e: - if str(e.code) == "403": - self._error = "HTTP error (access denied)" - self._error_msg = str(e.code) + " - server error response" - print(self._error, self._error_msg) - else: - self._error = "HTTP error" - self._error_msg = str(e.code) - print(self._error, self._error_msg) - self.print_trace() - self._update_ready = None - except urllib.error.URLError as e: - reason = str(e.reason) - if "TLSV1_ALERT" in reason or "SSL" in reason.upper(): - self._error = "Connection rejected, download manually" - self._error_msg = reason - print(self._error, self._error_msg) - else: - self._error = "URL error, check internet connection" - self._error_msg = reason - print(self._error, self._error_msg) - self.print_trace() - self._update_ready = None - return None - else: - result_string = result.read() - result.close() - return result_string.decode() - - def get_api(self, url): - """Result of all api calls, decoded into json format.""" - get = None - get = self.get_raw(url) - if get is not None: - try: - return json.JSONDecoder().decode(get) - except Exception as e: - self._error = "API response has invalid JSON format" - self._error_msg = str(e.reason) - self._update_ready = None - print(self._error, self._error_msg) - self.print_trace() - return None - else: - return None - - def stage_repository(self, url): - """Create a working directory and download the new files""" - - local = os.path.join(self._updater_path, "update_staging") - error = None - - # Make/clear the staging folder, to ensure the folder is always clean. - self.print_verbose( - "Preparing staging folder for download:\n" + str(local)) - if os.path.isdir(local): - try: - shutil.rmtree(local) - os.makedirs(local) - except: - error = "failed to remove existing staging directory" - self.print_trace() - else: - try: - os.makedirs(local) - except: - error = "failed to create staging directory" - self.print_trace() - - if error is not None: - self.print_verbose("Error: Aborting update, " + error) - self._error = "Update aborted, staging path error" - self._error_msg = "Error: {}".format(error) - return False - - if self._backup_current: - self.create_backup() - - self.print_verbose("Now retrieving the new source zip") - self._source_zip = os.path.join(local, "source.zip") - self.print_verbose("Starting download update zip") - try: - request = urllib.request.Request(url) - context = ssl._create_unverified_context() - - # Setup private token if appropriate. - if self._engine.token is not None: - if self._engine.name == "gitlab": - request.add_header('PRIVATE-TOKEN', self._engine.token) - else: - self.print_verbose( - "Tokens not setup for selected engine yet") - - # Always set user agent - request.add_header( - 'User-Agent', "Python/" + str(platform.python_version())) - - self.url_retrieve(urllib.request.urlopen(request, context=context), - self._source_zip) - # Add additional checks on file size being non-zero. - self.print_verbose("Successfully downloaded update zip") - return True - except Exception as e: - self._error = "Error retrieving download, bad link?" - self._error_msg = "Error: {}".format(e) - print("Error retrieving download, bad link?") - print("Error: {}".format(e)) - self.print_trace() - return False - - def create_backup(self): - """Save a backup of the current installed addon prior to an update.""" - self.print_verbose("Backing up current addon folder") - local = os.path.join(self._updater_path, "backup") - tempdest = os.path.join( - self._addon_root, os.pardir, self._addon + "_updater_backup_temp") - - self.print_verbose("Backup destination path: " + str(local)) - - if os.path.isdir(local): - try: - shutil.rmtree(local) - except: - self.print_verbose( - "Failed to removed previous backup folder, continuing") - self.print_trace() - - # Remove the temp folder. - # Shouldn't exist but could if previously interrupted. - if os.path.isdir(tempdest): - try: - shutil.rmtree(tempdest) - except: - self.print_verbose( - "Failed to remove existing temp folder, continuing") - self.print_trace() - - # Make a full addon copy, temporarily placed outside the addon folder. - if self._backup_ignore_patterns is not None: - try: - shutil.copytree(self._addon_root, tempdest, - ignore=shutil.ignore_patterns( - *self._backup_ignore_patterns)) - except: - print("Failed to create backup, still attempting update.") - self.print_trace() - return - else: - try: - shutil.copytree(self._addon_root, tempdest) - except: - print("Failed to create backup, still attempting update.") - self.print_trace() - return - shutil.move(tempdest, local) - - # Save the date for future reference. - now = datetime.now() - self._json["backup_date"] = "{m}-{d}-{yr}".format( - m=now.strftime("%B"), d=now.day, yr=now.year) - self.save_updater_json() - - def restore_backup(self): - """Restore the last backed up addon version, user initiated only""" - self.print_verbose("Restoring backup, backing up current addon folder") - backuploc = os.path.join(self._updater_path, "backup") - tempdest = os.path.join( - self._addon_root, os.pardir, self._addon + "_updater_backup_temp") - tempdest = os.path.abspath(tempdest) - - # Move instead contents back in place, instead of copy. - shutil.move(backuploc, tempdest) - shutil.rmtree(self._addon_root) - os.rename(tempdest, self._addon_root) - - self._json["backup_date"] = "" - self._json["just_restored"] = True - self._json["just_updated"] = True - self.save_updater_json() - - self.reload_addon() - - def unpack_staged_zip(self, clean=False): - """Unzip the downloaded file, and validate contents""" - if not os.path.isfile(self._source_zip): - self.print_verbose("Error, update zip not found") - self._error = "Install failed" - self._error_msg = "Downloaded zip not found" - return -1 - - # Clear the existing source folder in case previous files remain. - outdir = os.path.join(self._updater_path, "source") - try: - shutil.rmtree(outdir) - self.print_verbose("Source folder cleared") - except: - self.print_trace() - - # Create parent directories if needed, would not be relevant unless - # installing addon into another location or via an addon manager. - try: - os.mkdir(outdir) - except Exception as err: - print("Error occurred while making extract dir:") - print(str(err)) - self.print_trace() - self._error = "Install failed" - self._error_msg = "Failed to make extract directory" - return -1 - - if not os.path.isdir(outdir): - print("Failed to create source directory") - self._error = "Install failed" - self._error_msg = "Failed to create extract directory" - return -1 - - self.print_verbose( - "Begin extracting source from zip:" + str(self._source_zip)) - with zipfile.ZipFile(self._source_zip, "r") as zfile: - - if not zfile: - self._error = "Install failed" - self._error_msg = "Resulting file is not a zip, cannot extract" - self.print_verbose(self._error_msg) - return -1 - - # Now extract directly from the first subfolder (not root) - # this avoids adding the first subfolder to the path length, - # which can be too long if the download has the SHA in the name. - zsep = '/' # Not using os.sep, always the / value even on windows. - for name in zfile.namelist(): - if zsep not in name: - continue - top_folder = name[:name.index(zsep) + 1] - if name == top_folder + zsep: - continue # skip top level folder - sub_path = name[name.index(zsep) + 1:] - if name.endswith(zsep): - try: - os.mkdir(os.path.join(outdir, sub_path)) - self.print_verbose( - "Extract - mkdir: " + os.path.join(outdir, sub_path)) - except OSError as exc: - if exc.errno != errno.EEXIST: - self._error = "Install failed" - self._error_msg = "Could not create folder from zip" - self.print_trace() - return -1 - else: - with open(os.path.join(outdir, sub_path), "wb") as outfile: - data = zfile.read(name) - outfile.write(data) - self.print_verbose( - "Extract - create: " + os.path.join(outdir, sub_path)) - - self.print_verbose("Extracted source") - - unpath = os.path.join(self._updater_path, "source") - if not os.path.isdir(unpath): - self._error = "Install failed" - self._error_msg = "Extracted path does not exist" - print("Extracted path does not exist: ", unpath) - return -1 - - if self._subfolder_path: - self._subfolder_path.replace('/', os.path.sep) - self._subfolder_path.replace('\\', os.path.sep) - - # Either directly in root of zip/one subfolder, or use specified path. - if not os.path.isfile(os.path.join(unpath, "__init__.py")): - dirlist = os.listdir(unpath) - if len(dirlist) > 0: - if self._subfolder_path == "" or self._subfolder_path is None: - unpath = os.path.join(unpath, dirlist[0]) - else: - unpath = os.path.join(unpath, self._subfolder_path) - - # Smarter check for additional sub folders for a single folder - # containing the __init__.py file. - if not os.path.isfile(os.path.join(unpath, "__init__.py")): - print("Not a valid addon found") - print("Paths:") - print(dirlist) - self._error = "Install failed" - self._error_msg = "No __init__ file found in new source" - return -1 - - # Merge code with the addon directory, using blender default behavior, - # plus any modifiers indicated by user (e.g. force remove/keep). - self.deep_merge_directory(self._addon_root, unpath, clean) - - # Now save the json state. - # Change to True to trigger the handler on other side if allowing - # reloading within same blender session. - self._json["just_updated"] = True - self.save_updater_json() - self.reload_addon() - self._update_ready = False - return 0 - - def deep_merge_directory(self, base, merger, clean=False): - """Merge folder 'merger' into 'base' without deleting existing""" - if not os.path.exists(base): - self.print_verbose("Base path does not exist:" + str(base)) - return -1 - elif not os.path.exists(merger): - self.print_verbose("Merger path does not exist") - return -1 - - # Path to be aware of and not overwrite/remove/etc. - staging_path = os.path.join(self._updater_path, "update_staging") - - # If clean install is enabled, clear existing files ahead of time - # note: will not delete the update.json, update folder, staging, or - # staging but will delete all other folders/files in addon directory. - error = None - if clean: - try: - # Implement clearing of all folders/files, except the updater - # folder and updater json. - # Careful, this deletes entire subdirectories recursively... - # Make sure that base is not a high level shared folder, but - # is dedicated just to the addon itself. - self.print_verbose( - "clean=True, clearing addon folder to fresh install state") - - # Remove root files and folders (except update folder). - files = [f for f in os.listdir(base) - if os.path.isfile(os.path.join(base, f))] - folders = [f for f in os.listdir(base) - if os.path.isdir(os.path.join(base, f))] - - for f in files: - os.remove(os.path.join(base, f)) - self.print_verbose( - "Clean removing file {}".format(os.path.join(base, f))) - for f in folders: - if os.path.join(base, f) is self._updater_path: - continue - shutil.rmtree(os.path.join(base, f)) - self.print_verbose( - "Clean removing folder and contents {}".format( - os.path.join(base, f))) - - except Exception as err: - error = "failed to create clean existing addon folder" - print(error, str(err)) - self.print_trace() - - # Walk through the base addon folder for rules on pre-removing - # but avoid removing/altering backup and updater file. - for path, dirs, files in os.walk(base): - # Prune ie skip updater folder. - dirs[:] = [d for d in dirs - if os.path.join(path, d) not in [self._updater_path]] - for file in files: - for pattern in self.remove_pre_update_patterns: - if fnmatch.filter([file], pattern): - try: - fl = os.path.join(path, file) - os.remove(fl) - self.print_verbose("Pre-removed file " + file) - except OSError: - print("Failed to pre-remove " + file) - self.print_trace() - - # Walk through the temp addon sub folder for replacements - # this implements the overwrite rules, which apply after - # the above pre-removal rules. This also performs the - # actual file copying/replacements. - for path, dirs, files in os.walk(merger): - # Verify structure works to prune updater sub folder overwriting. - dirs[:] = [d for d in dirs - if os.path.join(path, d) not in [self._updater_path]] - rel_path = os.path.relpath(path, merger) - dest_path = os.path.join(base, rel_path) - if not os.path.exists(dest_path): - os.makedirs(dest_path) - for file in files: - # Bring in additional logic around copying/replacing. - # Blender default: overwrite .py's, don't overwrite the rest. - dest_file = os.path.join(dest_path, file) - srcFile = os.path.join(path, file) - - # Decide to replace if file already exists, and copy new over. - if os.path.isfile(dest_file): - # Otherwise, check each file for overwrite pattern match. - replaced = False - for pattern in self._overwrite_patterns: - if fnmatch.filter([file], pattern): - replaced = True - break - if replaced: - os.remove(dest_file) - os.rename(srcFile, dest_file) - self.print_verbose( - "Overwrote file " + os.path.basename(dest_file)) - else: - self.print_verbose( - "Pattern not matched to {}, not overwritten".format( - os.path.basename(dest_file))) - else: - # File did not previously exist, simply move it over. - os.rename(srcFile, dest_file) - self.print_verbose( - "New file " + os.path.basename(dest_file)) - - # now remove the temp staging folder and downloaded zip - try: - shutil.rmtree(staging_path) - except: - error = ("Error: Failed to remove existing staging directory, " - "consider manually removing ") + staging_path - self.print_verbose(error) - self.print_trace() - - def reload_addon(self): - # if post_update false, skip this function - # else, unload/reload addon & trigger popup - if not self._auto_reload_post_update: - print("Restart blender to reload addon and complete update") - return - - self.print_verbose("Reloading addon...") - addon_utils.modules(refresh=True) - bpy.utils.refresh_script_paths() - - # not allowed in restricted context, such as register module - # toggle to refresh - if "addon_disable" in dir(bpy.ops.wm): # 2.7 - bpy.ops.wm.addon_disable(module=self._addon_package) - bpy.ops.wm.addon_refresh() - bpy.ops.wm.addon_enable(module=self._addon_package) - print("2.7 reload complete") - else: # 2.8 - bpy.ops.preferences.addon_disable(module=self._addon_package) - bpy.ops.preferences.addon_refresh() - bpy.ops.preferences.addon_enable(module=self._addon_package) - print("2.8 reload complete") - - # ------------------------------------------------------------------------- - # Other non-api functions and setups - # ------------------------------------------------------------------------- - def clear_state(self): - self._update_ready = None - self._update_link = None - self._update_version = None - self._source_zip = None - self._error = None - self._error_msg = None - - def url_retrieve(self, url_file, filepath): - """Custom urlretrieve implementation""" - chunk = 1024 * 8 - f = open(filepath, "wb") - while 1: - data = url_file.read(chunk) - if not data: - # print("done.") - break - f.write(data) - # print("Read %s bytes" % len(data)) - f.close() - - def version_tuple_from_text(self, text): - """Convert text into a tuple of numbers (int). - - Should go through string and remove all non-integers, and for any - given break split into a different section. - """ - if text is None: - return () - - segments = list() - tmp = '' - for char in str(text): - if not char.isdigit(): - if len(tmp) > 0: - segments.append(int(tmp)) - tmp = '' - else: - tmp += char - if len(tmp) > 0: - segments.append(int(tmp)) - - if len(segments) == 0: - self.print_verbose("No version strings found text: " + str(text)) - if not self._include_branches: - return () - else: - return (text) - return tuple(segments) - - def check_for_update_async(self, callback=None): - """Called for running check in a background thread""" - is_ready = ( - self._json is not None - and "update_ready" in self._json - and self._json["version_text"] != dict() - and self._json["update_ready"]) - - if is_ready: - self._update_ready = True - self._update_link = self._json["version_text"]["link"] - self._update_version = str(self._json["version_text"]["version"]) - # Cached update. - callback(True) - return - - # do the check - if not self._check_interval_enabled: - return - elif self._async_checking: - self.print_verbose("Skipping async check, already started") - # already running the bg thread - elif self._update_ready is None: - print("{} updater: Running background check for update".format( - self.addon)) - self.start_async_check_update(False, callback) - - def check_for_update_now(self, callback=None): - self._error = None - self._error_msg = None - self.print_verbose( - "Check update pressed, first getting current status") - if self._async_checking: - self.print_verbose("Skipping async check, already started") - return # already running the bg thread - elif self._update_ready is None: - self.start_async_check_update(True, callback) - else: - self._update_ready = None - self.start_async_check_update(True, callback) - - def check_for_update(self, now=False): - """Check for update not in a syncrhonous manner. - - This function is not async, will always return in sequential fashion - but should have a parent which calls it in another thread. - """ - self.print_verbose("Checking for update function") - - # clear the errors if any - self._error = None - self._error_msg = None - - # avoid running again in, just return past result if found - # but if force now check, then still do it - if self._update_ready is not None and not now: - return (self._update_ready, - self._update_version, - self._update_link) - - if self._current_version is None: - raise ValueError("current_version not yet defined") - - if self._repo is None: - raise ValueError("repo not yet defined") - - if self._user is None: - raise ValueError("username not yet defined") - - self.set_updater_json() # self._json - - if not now and not self.past_interval_timestamp(): - self.print_verbose( - "Aborting check for updated, check interval not reached") - return (False, None, None) - - # check if using tags or releases - # note that if called the first time, this will pull tags from online - if self._fake_install: - self.print_verbose( - "fake_install = True, setting fake version as ready") - self._update_ready = True - self._update_version = "(999,999,999)" - self._update_link = "http://127.0.0.1" - - return (self._update_ready, - self._update_version, - self._update_link) - - # Primary internet call, sets self._tags and self._tag_latest. - self.get_tags() - - self._json["last_check"] = str(datetime.now()) - self.save_updater_json() - - # Can be () or ('master') in addition to branches, and version tag. - new_version = self.version_tuple_from_text(self.tag_latest) - - if len(self._tags) == 0: - self._update_ready = False - self._update_version = None - self._update_link = None - return (False, None, None) - - if not self._include_branches: - link = self.select_link(self, self._tags[0]) - else: - n = len(self._include_branch_list) - if len(self._tags) == n: - # effectively means no tags found on repo - # so provide the first one as default - link = self.select_link(self, self._tags[0]) - else: - link = self.select_link(self, self._tags[n]) - - if new_version == (): - self._update_ready = False - self._update_version = None - self._update_link = None - return (False, None, None) - elif str(new_version).lower() in self._include_branch_list: - # Handle situation where master/whichever branch is included - # however, this code effectively is not triggered now - # as new_version will only be tag names, not branch names. - if not self._include_branch_auto_check: - # Don't offer update as ready, but set the link for the - # default branch for installing. - self._update_ready = False - self._update_version = new_version - self._update_link = link - self.save_updater_json() - return (True, new_version, link) - else: - # Bypass releases and look at timestamp of last update from a - # branch compared to now, see if commit values match or not. - raise ValueError("include_branch_autocheck: NOT YET DEVELOPED") - - else: - # Situation where branches not included. - if new_version > self._current_version: - - self._update_ready = True - self._update_version = new_version - self._update_link = link - self.save_updater_json() - return (True, new_version, link) - - # If no update, set ready to False from None to show it was checked. - self._update_ready = False - self._update_version = None - self._update_link = None - return (False, None, None) - - def set_tag(self, name): - """Assign the tag name and url to update to""" - tg = None - for tag in self._tags: - if name == tag["name"]: - tg = tag - break - if tg: - new_version = self.version_tuple_from_text(self.tag_latest) - self._update_version = new_version - self._update_link = self.select_link(self, tg) - elif self._include_branches and name in self._include_branch_list: - # scenario if reverting to a specific branch name instead of tag - tg = name - link = self.form_branch_url(tg) - self._update_version = name # this will break things - self._update_link = link - if not tg: - raise ValueError("Version tag not found: " + name) - - def run_update(self, force=False, revert_tag=None, clean=False, callback=None): - """Runs an install, update, or reversion of an addon from online source - - Arguments: - force: Install assigned link, even if self.update_ready is False - revert_tag: Version to install, if none uses detected update link - clean: not used, but in future could use to totally refresh addon - callback: used to run function on update completion - """ - self._json["update_ready"] = False - self._json["ignore"] = False # clear ignore flag - self._json["version_text"] = dict() - - if revert_tag is not None: - self.set_tag(revert_tag) - self._update_ready = True - - # clear the errors if any - self._error = None - self._error_msg = None - - self.print_verbose("Running update") - - if self._fake_install: - # Change to True, to trigger the reload/"update installed" handler. - self.print_verbose("fake_install=True") - self.print_verbose( - "Just reloading and running any handler triggers") - self._json["just_updated"] = True - self.save_updater_json() - if self._backup_current is True: - self.create_backup() - self.reload_addon() - self._update_ready = False - res = True # fake "success" zip download flag - - elif not force: - if not self._update_ready: - self.print_verbose("Update stopped, new version not ready") - if callback: - callback( - self._addon_package, - "Update stopped, new version not ready") - return "Update stopped, new version not ready" - elif self._update_link is None: - # this shouldn't happen if update is ready - self.print_verbose("Update stopped, update link unavailable") - if callback: - callback(self._addon_package, - "Update stopped, update link unavailable") - return "Update stopped, update link unavailable" - - if revert_tag is None: - self.print_verbose("Staging update") - else: - self.print_verbose("Staging install") - - res = self.stage_repository(self._update_link) - if not res: - print("Error in staging repository: " + str(res)) - if callback is not None: - callback(self._addon_package, self._error_msg) - return self._error_msg - res = self.unpack_staged_zip(clean) - if res < 0: - if callback: - callback(self._addon_package, self._error_msg) - return res - - else: - if self._update_link is None: - self.print_verbose("Update stopped, could not get link") - return "Update stopped, could not get link" - self.print_verbose("Forcing update") - - res = self.stage_repository(self._update_link) - if not res: - print("Error in staging repository: " + str(res)) - if callback: - callback(self._addon_package, self._error_msg) - return self._error_msg - res = self.unpack_staged_zip(clean) - if res < 0: - return res - # would need to compare against other versions held in tags - - # run the front-end's callback if provided - if callback: - callback(self._addon_package) - - # return something meaningful, 0 means it worked - return 0 - - def past_interval_timestamp(self): - if not self._check_interval_enabled: - return True # ie this exact feature is disabled - - if "last_check" not in self._json or self._json["last_check"] == "": - return True - - now = datetime.now() - last_check = datetime.strptime( - self._json["last_check"], "%Y-%m-%d %H:%M:%S.%f") - offset = timedelta( - days=self._check_interval_days + 30 * self._check_interval_months, - hours=self._check_interval_hours, - minutes=self._check_interval_minutes) - - delta = (now - offset) - last_check - if delta.total_seconds() > 0: - self.print_verbose("Time to check for updates!") - return True - - self.print_verbose("Determined it's not yet time to check for updates") - return False - - def get_json_path(self): - """Returns the full path to the JSON state file used by this updater. - - Will also rename old file paths to addon-specific path if found. - """ - json_path = os.path.join( - self._updater_path, - "{}_updater_status.json".format(self._addon_package)) - old_json_path = os.path.join(self._updater_path, "updater_status.json") - - # Rename old file if it exists. - try: - os.rename(old_json_path, json_path) - except FileNotFoundError: - pass - except Exception as err: - print("Other OS error occurred while trying to rename old JSON") - print(err) - self.print_trace() - return json_path - - def set_updater_json(self): - """Load or initialize JSON dictionary data for updater state""" - if self._updater_path is None: - raise ValueError("updater_path is not defined") - elif not os.path.isdir(self._updater_path): - os.makedirs(self._updater_path) - - jpath = self.get_json_path() - if os.path.isfile(jpath): - with open(jpath) as data_file: - self._json = json.load(data_file) - self.print_verbose("Read in JSON settings from file") - else: - self._json = { - "last_check": "", - "backup_date": "", - "update_ready": False, - "ignore": False, - "just_restored": False, - "just_updated": False, - "version_text": dict() - } - self.save_updater_json() - - def save_updater_json(self): - """Trigger save of current json structure into file within addon""" - if self._update_ready: - if isinstance(self._update_version, tuple): - self._json["update_ready"] = True - self._json["version_text"]["link"] = self._update_link - self._json["version_text"]["version"] = self._update_version - else: - self._json["update_ready"] = False - self._json["version_text"] = dict() - else: - self._json["update_ready"] = False - self._json["version_text"] = dict() - - jpath = self.get_json_path() - if not os.path.isdir(os.path.dirname(jpath)): - print("State error: Directory does not exist, cannot save json: ", - os.path.basename(jpath)) - return - try: - with open(jpath, 'w') as outf: - data_out = json.dumps(self._json, indent=4) - outf.write(data_out) - except: - print("Failed to open/save data to json: ", jpath) - self.print_trace() - self.print_verbose("Wrote out updater JSON settings with content:") - self.print_verbose(str(self._json)) - - def json_reset_postupdate(self): - self._json["just_updated"] = False - self._json["update_ready"] = False - self._json["version_text"] = dict() - self.save_updater_json() - - def json_reset_restore(self): - self._json["just_restored"] = False - self._json["update_ready"] = False - self._json["version_text"] = dict() - self.save_updater_json() - self._update_ready = None # Reset so you could check update again. - - def ignore_update(self): - self._json["ignore"] = True - self.save_updater_json() - - # ------------------------------------------------------------------------- - # ASYNC related methods - # ------------------------------------------------------------------------- - def start_async_check_update(self, now=False, callback=None): - """Start a background thread which will check for updates""" - if self._async_checking: - return - self.print_verbose("Starting background checking thread") - check_thread = threading.Thread(target=self.async_check_update, - args=(now, callback,)) - check_thread.daemon = True - self._check_thread = check_thread - check_thread.start() - - def async_check_update(self, now, callback=None): - """Perform update check, run as target of background thread""" - self._async_checking = True - self.print_verbose("Checking for update now in background") - - try: - self.check_for_update(now=now) - except Exception as exception: - print("Checking for update error:") - print(exception) - self.print_trace() - if not self._error: - self._update_ready = False - self._update_version = None - self._update_link = None - self._error = "Error occurred" - self._error_msg = "Encountered an error while checking for updates" - - self._async_checking = False - self._check_thread = None - - if callback: - self.print_verbose("Finished check update, doing callback") - callback(self._update_ready) - self.print_verbose("BG thread: Finished check update, no callback") - - def stop_async_check_update(self): - """Method to give impression of stopping check for update. - - Currently does nothing but allows user to retry/stop blocking UI from - hitting a refresh button. This does not actually stop the thread, as it - will complete after the connection timeout regardless. If the thread - does complete with a successful response, this will be still displayed - on next UI refresh (ie no update, or update available). - """ - if self._check_thread is not None: - self.print_verbose("Thread will end in normal course.") - # however, "There is no direct kill method on a thread object." - # better to let it run its course - # self._check_thread.stop() - self._async_checking = False - self._error = None - self._error_msg = None - - -# ----------------------------------------------------------------------------- -# Updater Engines -# ----------------------------------------------------------------------------- - -class BitbucketEngine: - """Integration to Bitbucket API for git-formatted repositories""" - - def __init__(self): - self.api_url = 'https://api.bitbucket.org' - self.token = None - self.name = "bitbucket" - - def form_repo_url(self, updater): - return "{}/2.0/repositories/{}/{}".format( - self.api_url, updater.user, updater.repo) - - def form_tags_url(self, updater): - return self.form_repo_url(updater) + "/refs/tags?sort=-name" - - def form_branch_url(self, branch, updater): - return self.get_zip_url(branch, updater) - - def get_zip_url(self, name, updater): - return "https://bitbucket.org/{user}/{repo}/get/{name}.zip".format( - user=updater.user, - repo=updater.repo, - name=name) - - def parse_tags(self, response, updater): - if response is None: - return list() - return [ - { - "name": tag["name"], - "zipball_url": self.get_zip_url(tag["name"], updater) - } for tag in response["values"]] - - -class GithubEngine: - """Integration to Github API""" - - def __init__(self): - self.api_url = 'https://api.github.com' - self.token = None - self.name = "github" - - def form_repo_url(self, updater): - return "{}/repos/{}/{}".format( - self.api_url, updater.user, updater.repo) - - def form_tags_url(self, updater): - if updater.use_releases: - return "{}/releases".format(self.form_repo_url(updater)) - else: - return "{}/tags".format(self.form_repo_url(updater)) - - def form_branch_list_url(self, updater): - return "{}/branches".format(self.form_repo_url(updater)) - - def form_branch_url(self, branch, updater): - return "{}/zipball/{}".format(self.form_repo_url(updater), branch) - - def parse_tags(self, response, updater): - if response is None: - return list() - return response - - -class GitlabEngine: - """Integration to GitLab API""" - - def __init__(self): - self.api_url = 'https://gitlab.com' - self.token = None - self.name = "gitlab" - - def form_repo_url(self, updater): - return "{}/api/v4/projects/{}".format(self.api_url, updater.repo) - - def form_tags_url(self, updater): - return "{}/repository/tags".format(self.form_repo_url(updater)) - - def form_branch_list_url(self, updater): - # does not validate branch name. - return "{}/repository/branches".format( - self.form_repo_url(updater)) - - def form_branch_url(self, branch, updater): - # Could clash with tag names and if it does, it will download TAG zip - # instead of branch zip to get direct path, would need. - return "{}/repository/archive.zip?sha={}".format( - self.form_repo_url(updater), branch) - - def get_zip_url(self, sha, updater): - return "{base}/repository/archive.zip?sha={sha}".format( - base=self.form_repo_url(updater), - sha=sha) - - # def get_commit_zip(self, id, updater): - # return self.form_repo_url(updater)+"/repository/archive.zip?sha:"+id - - def parse_tags(self, response, updater): - if response is None: - return list() - return [ - { - "name": tag["name"], - "zipball_url": self.get_zip_url(tag["commit"]["id"], updater) - } for tag in response] - -class ForgejoEngine: - """Integration to Forgejo/Gitea API""" - - def __init__(self): - self.api_url = 'https://git.pointer.click' - self.token = None - self.name = "forgejo" - - def form_repo_url(self, updater): - return "{}/api/v1/repos/{}/{}".format(self.api_url, updater.user, updater.repo) - - def form_tags_url(self, updater): - return "{}/tags".format(self.form_repo_url(updater)) - - def form_branch_list_url(self, updater): - # does not validate branch name. - return "{}/branches".format( - self.form_repo_url(updater)) - - def form_branch_url(self, branch, updater): - # Could clash with tag names and if it does, it will download TAG zip - # instead of branch zip to get direct path, would need. - return "{}/archive/{}.zip".format( - self.form_repo_url(updater), branch) - - def get_zip_url(self, sha, updater): - return "{base}/archive/{sha}.zip".format( - base=self.form_repo_url(updater), - sha=sha) - - # def get_commit_zip(self, id, updater): - # return self.form_repo_url(updater)+"/repository/archive.zip?sha:"+id - - def parse_tags(self, response, updater): - if response is None: - return list() - return [ - { - "name": tag["name"], - "zipball_url": self.get_zip_url(tag["commit"]["id"], updater) - } for tag in response] - - -# ----------------------------------------------------------------------------- -# The module-shared class instance, -# should be what's imported to other files -# ----------------------------------------------------------------------------- - -Updater = SingletonUpdater() diff --git a/addon_updater_ops.py b/addon_updater_ops.py deleted file mode 100644 index 6cc021d..0000000 --- a/addon_updater_ops.py +++ /dev/null @@ -1,1539 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -"""Blender UI integrations for the addon updater. - -Implements draw calls, popups, and operators that use the addon_updater. -""" - -import os -import traceback - -import bpy -from bpy.app.handlers import persistent - -# Safely import the updater. -# Prevents popups for users with invalid python installs e.g. missing libraries -# and will replace with a fake class instead if it fails (so UI draws work). -try: - from .addon_updater import Updater as updater -except Exception as e: - print("ERROR INITIALIZING UPDATER") - print(str(e)) - traceback.print_exc() - - class SingletonUpdaterNone(object): - """Fake, bare minimum fields and functions for the updater object.""" - - def __init__(self): - self.invalid_updater = True # Used to distinguish bad install. - - self.addon = None - self.verbose = False - self.use_print_traces = True - self.error = None - self.error_msg = None - self.async_checking = None - - def clear_state(self): - self.addon = None - self.verbose = False - self.invalid_updater = True - self.error = None - self.error_msg = None - self.async_checking = None - - def run_update(self, force, callback, clean): - pass - - def check_for_update(self, now): - pass - - updater = SingletonUpdaterNone() - updater.error = "Error initializing updater module" - updater.error_msg = str(e) - -# Must declare this before classes are loaded, otherwise the bl_idname's will -# not match and have errors. Must be all lowercase and no spaces! Should also -# be unique among any other addons that could exist (using this updater code), -# to avoid clashes in operator registration. -updater.addon = "addon_updater_demo" - - -# ----------------------------------------------------------------------------- -# Blender version utils -# ----------------------------------------------------------------------------- -def make_annotations(cls): - """Add annotation attribute to fields to avoid Blender 2.8+ warnings""" - if not hasattr(bpy.app, "version") or bpy.app.version < (2, 80): - return cls - if bpy.app.version < (2, 93, 0): - bl_props = {k: v for k, v in cls.__dict__.items() - if isinstance(v, tuple)} - else: - bl_props = {k: v for k, v in cls.__dict__.items() - if isinstance(v, bpy.props._PropertyDeferred)} - if bl_props: - if '__annotations__' not in cls.__dict__: - setattr(cls, '__annotations__', {}) - annotations = cls.__dict__['__annotations__'] - for k, v in bl_props.items(): - annotations[k] = v - delattr(cls, k) - return cls - - -def layout_split(layout, factor=0.0, align=False): - """Intermediate method for pre and post blender 2.8 split UI function""" - if not hasattr(bpy.app, "version") or bpy.app.version < (2, 80): - return layout.split(percentage=factor, align=align) - return layout.split(factor=factor, align=align) - - -def get_user_preferences(context=None): - """Intermediate method for pre and post blender 2.8 grabbing preferences""" - if not context: - context = bpy.context - prefs = None - if hasattr(context, "user_preferences"): - prefs = context.user_preferences.addons.get(__package__, None) - elif hasattr(context, "preferences"): - prefs = context.preferences.addons.get(__package__, None) - if prefs: - return prefs.preferences - # To make the addon stable and non-exception prone, return None - # raise Exception("Could not fetch user preferences") - return None - - -# ----------------------------------------------------------------------------- -# Updater operators -# ----------------------------------------------------------------------------- - - -# Simple popup to prompt use to check for update & offer install if available. -class AddonUpdaterInstallPopup(bpy.types.Operator): - """Check and install update if available""" - bl_label = "Update {x} addon".format(x=updater.addon) - bl_idname = updater.addon + ".updater_install_popup" - bl_description = "Popup to check and display current updates available" - bl_options = {'REGISTER', 'INTERNAL'} - - # if true, run clean install - ie remove all files before adding new - # equivalent to deleting the addon and reinstalling, except the - # updater folder/backup folder remains - clean_install = bpy.props.BoolProperty( - name="Clean install", - description=("If enabled, completely clear the addon's folder before " - "installing new update, creating a fresh install"), - default=False, - options={'HIDDEN'} - ) - - ignore_enum = bpy.props.EnumProperty( - name="Process update", - description="Decide to install, ignore, or defer new addon update", - items=[ - ("install", "Update Now", "Install update now"), - ("ignore", "Ignore", "Ignore this update to prevent future popups"), - ("defer", "Defer", "Defer choice till next blender session") - ], - options={'HIDDEN'} - ) - - def check(self, context): - return True - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self) - - def draw(self, context): - layout = self.layout - if updater.invalid_updater: - layout.label(text="Updater module error") - return - elif updater.update_ready: - col = layout.column() - col.scale_y = 0.7 - col.label(text="Update {} ready!".format(updater.update_version), - icon="LOOP_FORWARDS") - col.label(text="Choose 'Update Now' & press OK to install, ", - icon="BLANK1") - col.label(text="or click outside window to defer", icon="BLANK1") - row = col.row() - row.prop(self, "ignore_enum", expand=True) - col.split() - elif not updater.update_ready: - col = layout.column() - col.scale_y = 0.7 - col.label(text="No updates available") - col.label(text="Press okay to dismiss dialog") - # add option to force install - else: - # Case: updater.update_ready = None - # we have not yet checked for the update. - layout.label(text="Check for update now?") - - # Potentially in future, UI to 'check to select/revert to old version'. - - def execute(self, context): - # In case of error importing updater. - if updater.invalid_updater: - return {'CANCELLED'} - - if updater.manual_only: - bpy.ops.wm.url_open(url=updater.website) - elif updater.update_ready: - - # Action based on enum selection. - if self.ignore_enum == 'defer': - return {'FINISHED'} - elif self.ignore_enum == 'ignore': - updater.ignore_update() - return {'FINISHED'} - - res = updater.run_update(force=False, - callback=post_update_callback, - clean=self.clean_install) - - # Should return 0, if not something happened. - if updater.verbose: - if res == 0: - print("Updater returned successful") - else: - print("Updater returned {}, error occurred".format(res)) - elif updater.update_ready is None: - _ = updater.check_for_update(now=True) - - # Re-launch this dialog. - atr = AddonUpdaterInstallPopup.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') - else: - updater.print_verbose("Doing nothing, not ready for update") - return {'FINISHED'} - - -# User preference check-now operator -class AddonUpdaterCheckNow(bpy.types.Operator): - bl_label = "Check now for " + updater.addon + " update" - bl_idname = updater.addon + ".updater_check_now" - bl_description = "Check now for an update to the {} addon".format( - updater.addon) - bl_options = {'REGISTER', 'INTERNAL'} - - def execute(self, context): - if updater.invalid_updater: - return {'CANCELLED'} - - if updater.async_checking and updater.error is None: - # Check already happened. - # Used here to just avoid constant applying settings below. - # Ignoring if error, to prevent being stuck on the error screen. - return {'CANCELLED'} - - # apply the UI settings - settings = get_user_preferences(context) - if not settings: - updater.print_verbose( - "Could not get {} preferences, update check skipped".format( - __package__)) - return {'CANCELLED'} - - updater.set_check_interval( - enabled=settings.auto_check_update, - months=settings.updater_interval_months, - days=settings.updater_interval_days, - hours=settings.updater_interval_hours, - minutes=settings.updater_interval_minutes) - - # Input is an optional callback function. This function should take a - # bool input. If true: update ready, if false: no update ready. - updater.check_for_update_now(ui_refresh) - - return {'FINISHED'} - - -class AddonUpdaterUpdateNow(bpy.types.Operator): - bl_label = "Update " + updater.addon + " addon now" - bl_idname = updater.addon + ".updater_update_now" - bl_description = "Update to the latest version of the {x} addon".format( - x=updater.addon) - bl_options = {'REGISTER', 'INTERNAL'} - - # If true, run clean install - ie remove all files before adding new - # equivalent to deleting the addon and reinstalling, except the updater - # folder/backup folder remains. - clean_install = bpy.props.BoolProperty( - name="Clean install", - description=("If enabled, completely clear the addon's folder before " - "installing new update, creating a fresh install"), - default=False, - options={'HIDDEN'} - ) - - def execute(self, context): - - # in case of error importing updater - if updater.invalid_updater: - return {'CANCELLED'} - - if updater.manual_only: - bpy.ops.wm.url_open(url=updater.website) - if updater.update_ready: - # if it fails, offer to open the website instead - try: - res = updater.run_update(force=False, - callback=post_update_callback, - clean=self.clean_install) - - # Should return 0, if not something happened. - if updater.verbose: - if res == 0: - print("Updater returned successful") - else: - print("Updater error response: {}".format(res)) - except Exception as expt: - updater._error = "Error trying to run update" - updater._error_msg = str(expt) - updater.print_trace() - atr = AddonUpdaterInstallManually.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') - elif updater.update_ready is None: - (update_ready, version, link) = updater.check_for_update(now=True) - # Re-launch this dialog. - atr = AddonUpdaterInstallPopup.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') - - elif not updater.update_ready: - self.report({'INFO'}, "Nothing to update") - return {'CANCELLED'} - else: - self.report( - {'ERROR'}, "Encountered a problem while trying to update") - return {'CANCELLED'} - - return {'FINISHED'} - - -class AddonUpdaterUpdateTarget(bpy.types.Operator): - bl_label = updater.addon + " version target" - bl_idname = updater.addon + ".updater_update_target" - bl_description = "Install a targeted version of the {x} addon".format( - x=updater.addon) - bl_options = {'REGISTER', 'INTERNAL'} - - def target_version(self, context): - # In case of error importing updater. - if updater.invalid_updater: - ret = [] - - ret = [] - i = 0 - for tag in updater.tags: - ret.append((tag, tag, "Select to install " + tag)) - i += 1 - return ret - - target = bpy.props.EnumProperty( - name="Target version to install", - description="Select the version to install", - items=target_version - ) - - # If true, run clean install - ie remove all files before adding new - # equivalent to deleting the addon and reinstalling, except the - # updater folder/backup folder remains. - clean_install = bpy.props.BoolProperty( - name="Clean install", - description=("If enabled, completely clear the addon's folder before " - "installing new update, creating a fresh install"), - default=False, - options={'HIDDEN'} - ) - - @classmethod - def poll(cls, context): - if updater.invalid_updater: - return False - return updater.update_ready is not None and len(updater.tags) > 0 - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self) - - def draw(self, context): - layout = self.layout - if updater.invalid_updater: - layout.label(text="Updater error") - return - split = layout_split(layout, factor=0.5) - sub_col = split.column() - sub_col.label(text="Select install version") - sub_col = split.column() - sub_col.prop(self, "target", text="") - - def execute(self, context): - # In case of error importing updater. - if updater.invalid_updater: - return {'CANCELLED'} - - res = updater.run_update( - force=False, - revert_tag=self.target, - callback=post_update_callback, - clean=self.clean_install) - - # Should return 0, if not something happened. - if res == 0: - updater.print_verbose("Updater returned successful") - else: - updater.print_verbose( - "Updater returned {}, , error occurred".format(res)) - return {'CANCELLED'} - - return {'FINISHED'} - - -class AddonUpdaterInstallManually(bpy.types.Operator): - """As a fallback, direct the user to download the addon manually""" - bl_label = "Install update manually" - bl_idname = updater.addon + ".updater_install_manually" - bl_description = "Proceed to manually install update" - bl_options = {'REGISTER', 'INTERNAL'} - - error = bpy.props.StringProperty( - name="Error Occurred", - default="", - options={'HIDDEN'} - ) - - def invoke(self, context, event): - return context.window_manager.invoke_popup(self) - - def draw(self, context): - layout = self.layout - - if updater.invalid_updater: - layout.label(text="Updater error") - return - - # Display error if a prior autoamted install failed. - if self.error != "": - col = layout.column() - col.scale_y = 0.7 - col.label(text="There was an issue trying to auto-install", - icon="ERROR") - col.label(text="Press the download button below and install", - icon="BLANK1") - col.label(text="the zip file like a normal addon.", icon="BLANK1") - else: - col = layout.column() - col.scale_y = 0.7 - col.label(text="Install the addon manually") - col.label(text="Press the download button below and install") - col.label(text="the zip file like a normal addon.") - - # If check hasn't happened, i.e. accidentally called this menu, - # allow to check here. - - row = layout.row() - - if updater.update_link is not None: - row.operator( - "wm.url_open", - text="Direct download").url = updater.update_link - else: - row.operator( - "wm.url_open", - text="(failed to retrieve direct download)") - row.enabled = False - - if updater.website is not None: - row = layout.row() - ops = row.operator("wm.url_open", text="Open website") - ops.url = updater.website - else: - row = layout.row() - row.label(text="See source website to download the update") - - def execute(self, context): - return {'FINISHED'} - - -class AddonUpdaterUpdatedSuccessful(bpy.types.Operator): - """Addon in place, popup telling user it completed or what went wrong""" - bl_label = "Installation Report" - bl_idname = updater.addon + ".updater_update_successful" - bl_description = "Update installation response" - bl_options = {'REGISTER', 'INTERNAL', 'UNDO'} - - error = bpy.props.StringProperty( - name="Error Occurred", - default="", - options={'HIDDEN'} - ) - - def invoke(self, context, event): - return context.window_manager.invoke_props_popup(self, event) - - def draw(self, context): - layout = self.layout - - if updater.invalid_updater: - layout.label(text="Updater error") - return - - saved = updater.json - if self.error != "": - col = layout.column() - col.scale_y = 0.7 - col.label(text="Error occurred, did not install", icon="ERROR") - if updater.error_msg: - msg = updater.error_msg - else: - msg = self.error - col.label(text=str(msg), icon="BLANK1") - rw = col.row() - rw.scale_y = 2 - rw.operator( - "wm.url_open", - text="Click for manual download.", - icon="BLANK1").url = updater.website - elif not updater.auto_reload_post_update: - # Tell user to restart blender after an update/restore! - if "just_restored" in saved and saved["just_restored"]: - col = layout.column() - col.label(text="Addon restored", icon="RECOVER_LAST") - alert_row = col.row() - alert_row.alert = True - alert_row.operator( - "wm.quit_blender", - text="Restart blender to reload", - icon="BLANK1") - updater.json_reset_restore() - else: - col = layout.column() - col.label( - text="Addon successfully installed", icon="FILE_TICK") - alert_row = col.row() - alert_row.alert = True - alert_row.operator( - "wm.quit_blender", - text="Restart blender to reload", - icon="BLANK1") - - else: - # reload addon, but still recommend they restart blender - if "just_restored" in saved and saved["just_restored"]: - col = layout.column() - col.scale_y = 0.7 - col.label(text="Addon restored", icon="RECOVER_LAST") - col.label( - text="Consider restarting blender to fully reload.", - icon="BLANK1") - updater.json_reset_restore() - else: - col = layout.column() - col.scale_y = 0.7 - col.label( - text="Addon successfully installed", icon="FILE_TICK") - col.label( - text="Consider restarting blender to fully reload.", - icon="BLANK1") - - def execute(self, context): - return {'FINISHED'} - - -class AddonUpdaterRestoreBackup(bpy.types.Operator): - """Restore addon from backup""" - bl_label = "Restore backup" - bl_idname = updater.addon + ".updater_restore_backup" - bl_description = "Restore addon from backup" - bl_options = {'REGISTER', 'INTERNAL'} - - @classmethod - def poll(cls, context): - try: - return os.path.isdir(os.path.join(updater.stage_path, "backup")) - except: - return False - - def execute(self, context): - # in case of error importing updater - if updater.invalid_updater: - return {'CANCELLED'} - updater.restore_backup() - return {'FINISHED'} - - -class AddonUpdaterIgnore(bpy.types.Operator): - """Ignore update to prevent future popups""" - bl_label = "Ignore update" - bl_idname = updater.addon + ".updater_ignore" - bl_description = "Ignore update to prevent future popups" - bl_options = {'REGISTER', 'INTERNAL'} - - @classmethod - def poll(cls, context): - if updater.invalid_updater: - return False - elif updater.update_ready: - return True - else: - return False - - def execute(self, context): - # in case of error importing updater - if updater.invalid_updater: - return {'CANCELLED'} - updater.ignore_update() - self.report({"INFO"}, "Open addon preferences for updater options") - return {'FINISHED'} - - -class AddonUpdaterEndBackground(bpy.types.Operator): - """Stop checking for update in the background""" - bl_label = "End background check" - bl_idname = updater.addon + ".end_background_check" - bl_description = "Stop checking for update in the background" - bl_options = {'REGISTER', 'INTERNAL'} - - def execute(self, context): - # in case of error importing updater - if updater.invalid_updater: - return {'CANCELLED'} - updater.stop_async_check_update() - return {'FINISHED'} - - -# ----------------------------------------------------------------------------- -# Handler related, to create popups -# ----------------------------------------------------------------------------- - - -# global vars used to prevent duplicate popup handlers -ran_auto_check_install_popup = False -ran_update_success_popup = False - -# global var for preventing successive calls -ran_background_check = False - - -@persistent -def updater_run_success_popup_handler(scene): - global ran_update_success_popup - ran_update_success_popup = True - - # in case of error importing updater - if updater.invalid_updater: - return - - try: - if "scene_update_post" in dir(bpy.app.handlers): - bpy.app.handlers.scene_update_post.remove( - updater_run_success_popup_handler) - else: - bpy.app.handlers.depsgraph_update_post.remove( - updater_run_success_popup_handler) - except: - pass - - atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') - - -@persistent -def updater_run_install_popup_handler(scene): - global ran_auto_check_install_popup - ran_auto_check_install_popup = True - updater.print_verbose("Running the install popup handler.") - - # in case of error importing updater - if updater.invalid_updater: - return - - try: - if "scene_update_post" in dir(bpy.app.handlers): - bpy.app.handlers.scene_update_post.remove( - updater_run_install_popup_handler) - else: - bpy.app.handlers.depsgraph_update_post.remove( - updater_run_install_popup_handler) - except: - pass - - if "ignore" in updater.json and updater.json["ignore"]: - return # Don't do popup if ignore pressed. - elif "version_text" in updater.json and updater.json["version_text"].get("version"): - version = updater.json["version_text"]["version"] - ver_tuple = updater.version_tuple_from_text(version) - - if ver_tuple < updater.current_version: - # User probably manually installed to get the up to date addon - # in here. Clear out the update flag using this function. - updater.print_verbose( - "{} updater: appears user updated, clearing flag".format( - updater.addon)) - updater.json_reset_restore() - return - atr = AddonUpdaterInstallPopup.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') - - -def background_update_callback(update_ready): - """Passed into the updater, background thread updater""" - global ran_auto_check_install_popup - updater.print_verbose("Running background update callback") - - # In case of error importing updater. - if updater.invalid_updater: - return - if not updater.show_popups: - return - if not update_ready: - return - - # See if we need add to the update handler to trigger the popup. - handlers = [] - if "scene_update_post" in dir(bpy.app.handlers): # 2.7x - handlers = bpy.app.handlers.scene_update_post - else: # 2.8+ - handlers = bpy.app.handlers.depsgraph_update_post - in_handles = updater_run_install_popup_handler in handlers - - if in_handles or ran_auto_check_install_popup: - return - - if "scene_update_post" in dir(bpy.app.handlers): # 2.7x - bpy.app.handlers.scene_update_post.append( - updater_run_install_popup_handler) - else: # 2.8+ - bpy.app.handlers.depsgraph_update_post.append( - updater_run_install_popup_handler) - ran_auto_check_install_popup = True - updater.print_verbose("Attempted popup prompt") - - -def post_update_callback(module_name, res=None): - """Callback for once the run_update function has completed. - - Only makes sense to use this if "auto_reload_post_update" == False, - i.e. don't auto-restart the addon. - - Arguments: - module_name: returns the module name from updater, but unused here. - res: If an error occurred, this is the detail string. - """ - - # In case of error importing updater. - if updater.invalid_updater: - return - - if res is None: - # This is the same code as in conditional at the end of the register - # function, ie if "auto_reload_post_update" == True, skip code. - updater.print_verbose( - "{} updater: Running post update callback".format(updater.addon)) - - atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') - global ran_update_success_popup - ran_update_success_popup = True - else: - # Some kind of error occurred and it was unable to install, offer - # manual download instead. - atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT', error=res) - return - - -def ui_refresh(update_status): - """Redraw the ui once an async thread has completed""" - for windowManager in bpy.data.window_managers: - for window in windowManager.windows: - for area in window.screen.areas: - area.tag_redraw() - - -def check_for_update_background(): - """Function for asynchronous background check. - - *Could* be called on register, but would be bad practice as the bare - minimum code should run at the moment of registration (addon ticked). - """ - if updater.invalid_updater: - return - global ran_background_check - if ran_background_check: - # Global var ensures check only happens once. - return - elif updater.update_ready is not None or updater.async_checking: - # Check already happened. - # Used here to just avoid constant applying settings below. - return - - # Apply the UI settings. - settings = get_user_preferences(bpy.context) - if not settings: - return - updater.set_check_interval(enabled=settings.auto_check_update, - months=settings.updater_interval_months, - days=settings.updater_interval_days, - hours=settings.updater_interval_hours, - minutes=settings.updater_interval_minutes) - - # Input is an optional callback function. This function should take a bool - # input, if true: update ready, if false: no update ready. - updater.check_for_update_async(background_update_callback) - ran_background_check = True - - -def check_for_update_nonthreaded(self, context): - """Can be placed in front of other operators to launch when pressed""" - if updater.invalid_updater: - return - - # Only check if it's ready, ie after the time interval specified should - # be the async wrapper call here. - settings = get_user_preferences(bpy.context) - if not settings: - if updater.verbose: - print("Could not get {} preferences, update check skipped".format( - __package__)) - return - updater.set_check_interval(enabled=settings.auto_check_update, - months=settings.updater_interval_months, - days=settings.updater_interval_days, - hours=settings.updater_interval_hours, - minutes=settings.updater_interval_minutes) - - (update_ready, version, link) = updater.check_for_update(now=False) - if update_ready: - atr = AddonUpdaterInstallPopup.bl_idname.split(".") - getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT') - else: - updater.print_verbose("No update ready") - self.report({'INFO'}, "No update ready") - - -def show_reload_popup(): - """For use in register only, to show popup after re-enabling the addon. - - Must be enabled by developer. - """ - if updater.invalid_updater: - return - saved_state = updater.json - global ran_update_success_popup - - has_state = saved_state is not None - just_updated = "just_updated" in saved_state - updated_info = saved_state["just_updated"] - - if not (has_state and just_updated and updated_info): - return - - updater.json_reset_postupdate() # So this only runs once. - - # No handlers in this case. - if not updater.auto_reload_post_update: - return - - # See if we need add to the update handler to trigger the popup. - handlers = [] - if "scene_update_post" in dir(bpy.app.handlers): # 2.7x - handlers = bpy.app.handlers.scene_update_post - else: # 2.8+ - handlers = bpy.app.handlers.depsgraph_update_post - in_handles = updater_run_success_popup_handler in handlers - - if in_handles or ran_update_success_popup: - return - - if "scene_update_post" in dir(bpy.app.handlers): # 2.7x - bpy.app.handlers.scene_update_post.append( - updater_run_success_popup_handler) - else: # 2.8+ - bpy.app.handlers.depsgraph_update_post.append( - updater_run_success_popup_handler) - ran_update_success_popup = True - - -# ----------------------------------------------------------------------------- -# Example UI integrations -# ----------------------------------------------------------------------------- -def update_notice_box_ui(self, context): - """Update notice draw, to add to the end or beginning of a panel. - - After a check for update has occurred, this function will draw a box - saying an update is ready, and give a button for: update now, open website, - or ignore popup. Ideal to be placed at the end / beginning of a panel. - """ - - if updater.invalid_updater: - return - - saved_state = updater.json - if not updater.auto_reload_post_update: - if "just_updated" in saved_state and saved_state["just_updated"]: - layout = self.layout - box = layout.box() - col = box.column() - alert_row = col.row() - alert_row.alert = True - alert_row.operator( - "wm.quit_blender", - text="Restart blender", - icon="ERROR") - col.label(text="to complete update") - return - - # If user pressed ignore, don't draw the box. - if "ignore" in updater.json and updater.json["ignore"]: - return - if not updater.update_ready: - return - - layout = self.layout - box = layout.box() - col = box.column(align=True) - col.alert = True - col.label(text="Update ready!", icon="ERROR") - col.alert = False - col.separator() - row = col.row(align=True) - split = row.split(align=True) - colL = split.column(align=True) - colL.scale_y = 1.5 - colL.operator(AddonUpdaterIgnore.bl_idname, icon="X", text="Ignore") - colR = split.column(align=True) - colR.scale_y = 1.5 - if not updater.manual_only: - colR.operator(AddonUpdaterUpdateNow.bl_idname, - text="Update", icon="LOOP_FORWARDS") - col.operator("wm.url_open", text="Open website").url = updater.website - # ops = col.operator("wm.url_open",text="Direct download") - # ops.url=updater.update_link - col.operator(AddonUpdaterInstallManually.bl_idname, - text="Install manually") - else: - # ops = col.operator("wm.url_open", text="Direct download") - # ops.url=updater.update_link - col.operator("wm.url_open", text="Get it now").url = updater.website - - -def update_settings_ui(self, context, element=None): - """Preferences - for drawing with full width inside user preferences - - A function that can be run inside user preferences panel for prefs UI. - Place inside UI draw using: - addon_updater_ops.update_settings_ui(self, context) - or by: - addon_updater_ops.update_settings_ui(context) - """ - - # Element is a UI element, such as layout, a row, column, or box. - if element is None: - element = self.layout - box = element.box() - - # In case of error importing updater. - if updater.invalid_updater: - box.label(text="Error initializing updater code:") - box.label(text=updater.error_msg) - return - settings = get_user_preferences(context) - if not settings: - box.label(text="Error getting updater preferences", icon='ERROR') - return - - # auto-update settings - box.label(text="Updater Settings") - row = box.row() - - # special case to tell user to restart blender, if set that way - if not updater.auto_reload_post_update: - saved_state = updater.json - if "just_updated" in saved_state and saved_state["just_updated"]: - row.alert = True - row.operator("wm.quit_blender", - text="Restart blender to complete update", - icon="ERROR") - return - - split = layout_split(row, factor=0.4) - sub_col = split.column() - sub_col.prop(settings, "auto_check_update") - sub_col = split.column() - - if not settings.auto_check_update: - sub_col.enabled = False - sub_row = sub_col.row() - sub_row.label(text="Interval between checks") - sub_row = sub_col.row(align=True) - check_col = sub_row.column(align=True) - check_col.prop(settings, "updater_interval_months") - check_col = sub_row.column(align=True) - check_col.prop(settings, "updater_interval_days") - check_col = sub_row.column(align=True) - - # Consider un-commenting for local dev (e.g. to set shorter intervals) - # check_col.prop(settings,"updater_interval_hours") - # check_col = sub_row.column(align=True) - # check_col.prop(settings,"updater_interval_minutes") - - # Checking / managing updates. - row = box.row() - col = row.column() - if updater.error is not None: - sub_col = col.row(align=True) - sub_col.scale_y = 1 - split = sub_col.split(align=True) - split.scale_y = 2 - if "ssl" in updater.error_msg.lower(): - split.enabled = True - split.operator(AddonUpdaterInstallManually.bl_idname, - text=updater.error) - else: - split.enabled = False - split.operator(AddonUpdaterCheckNow.bl_idname, - text=updater.error) - split = sub_col.split(align=True) - split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="", icon="FILE_REFRESH") - - elif updater.update_ready is None and not updater.async_checking: - col.scale_y = 2 - col.operator(AddonUpdaterCheckNow.bl_idname) - elif updater.update_ready is None: # async is running - sub_col = col.row(align=True) - sub_col.scale_y = 1 - split = sub_col.split(align=True) - split.enabled = False - split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, text="Checking...") - split = sub_col.split(align=True) - split.scale_y = 2 - split.operator(AddonUpdaterEndBackground.bl_idname, text="", icon="X") - - elif updater.include_branches and \ - len(updater.tags) == len(updater.include_branch_list) and not \ - updater.manual_only: - # No releases found, but still show the appropriate branch. - sub_col = col.row(align=True) - sub_col.scale_y = 1 - split = sub_col.split(align=True) - split.scale_y = 2 - update_now_txt = "Update directly to {}".format( - updater.include_branch_list[0]) - split.operator(AddonUpdaterUpdateNow.bl_idname, text=update_now_txt) - split = sub_col.split(align=True) - split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="", icon="FILE_REFRESH") - - elif updater.update_ready and not updater.manual_only: - sub_col = col.row(align=True) - sub_col.scale_y = 1 - split = sub_col.split(align=True) - split.scale_y = 2 - split.operator(AddonUpdaterUpdateNow.bl_idname, - text="Update now to " + str(updater.update_version)) - split = sub_col.split(align=True) - split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="", icon="FILE_REFRESH") - - elif updater.update_ready and updater.manual_only: - col.scale_y = 2 - dl_now_txt = "Download " + str(updater.update_version) - col.operator("wm.url_open", - text=dl_now_txt).url = updater.website - else: # i.e. that updater.update_ready == False. - sub_col = col.row(align=True) - sub_col.scale_y = 1 - split = sub_col.split(align=True) - split.enabled = False - split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="Addon is up to date") - split = sub_col.split(align=True) - split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="", icon="FILE_REFRESH") - - if not updater.manual_only: - col = row.column(align=True) - if updater.include_branches and len(updater.include_branch_list) > 0: - branch = updater.include_branch_list[0] - col.operator(AddonUpdaterUpdateTarget.bl_idname, - text="Install {} / old version".format(branch)) - else: - col.operator(AddonUpdaterUpdateTarget.bl_idname, - text="(Re)install addon version") - last_date = "none found" - backup_path = os.path.join(updater.stage_path, "backup") - if "backup_date" in updater.json and os.path.isdir(backup_path): - if updater.json["backup_date"] == "": - last_date = "Date not found" - else: - last_date = updater.json["backup_date"] - backup_text = "Restore addon backup ({})".format(last_date) - col.operator(AddonUpdaterRestoreBackup.bl_idname, text=backup_text) - - row = box.row() - row.scale_y = 0.7 - last_check = updater.json["last_check"] - if updater.error is not None and updater.error_msg is not None: - row.label(text=updater.error_msg) - elif last_check: - last_check = last_check[0: last_check.index(".")] - row.label(text="Last update check: " + last_check) - else: - row.label(text="Last update check: Never") - - -def update_settings_ui_condensed(self, context, element=None): - """Preferences - Condensed drawing within preferences. - - Alternate draw for user preferences or other places, does not draw a box. - """ - - # Element is a UI element, such as layout, a row, column, or box. - if element is None: - element = self.layout - row = element.row() - - # In case of error importing updater. - if updater.invalid_updater: - row.label(text="Error initializing updater code:") - row.label(text=updater.error_msg) - return - settings = get_user_preferences(context) - if not settings: - row.label(text="Error getting updater preferences", icon='ERROR') - return - - # Special case to tell user to restart blender, if set that way. - if not updater.auto_reload_post_update: - saved_state = updater.json - if "just_updated" in saved_state and saved_state["just_updated"]: - row.alert = True # mark red - row.operator( - "wm.quit_blender", - text="Restart blender to complete update", - icon="ERROR") - return - - col = row.column() - if updater.error is not None: - sub_col = col.row(align=True) - sub_col.scale_y = 1 - split = sub_col.split(align=True) - split.scale_y = 2 - if "ssl" in updater.error_msg.lower(): - split.enabled = True - split.operator(AddonUpdaterInstallManually.bl_idname, - text=updater.error) - else: - split.enabled = False - split.operator(AddonUpdaterCheckNow.bl_idname, - text=updater.error) - split = sub_col.split(align=True) - split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="", icon="FILE_REFRESH") - - elif updater.update_ready is None and not updater.async_checking: - col.scale_y = 2 - col.operator(AddonUpdaterCheckNow.bl_idname) - elif updater.update_ready is None: # Async is running. - sub_col = col.row(align=True) - sub_col.scale_y = 1 - split = sub_col.split(align=True) - split.enabled = False - split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, text="Checking...") - split = sub_col.split(align=True) - split.scale_y = 2 - split.operator(AddonUpdaterEndBackground.bl_idname, text="", icon="X") - - elif updater.include_branches and \ - len(updater.tags) == len(updater.include_branch_list) and not \ - updater.manual_only: - # No releases found, but still show the appropriate branch. - sub_col = col.row(align=True) - sub_col.scale_y = 1 - split = sub_col.split(align=True) - split.scale_y = 2 - now_txt = "Update directly to " + str(updater.include_branch_list[0]) - split.operator(AddonUpdaterUpdateNow.bl_idname, text=now_txt) - split = sub_col.split(align=True) - split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="", icon="FILE_REFRESH") - - elif updater.update_ready and not updater.manual_only: - sub_col = col.row(align=True) - sub_col.scale_y = 1 - split = sub_col.split(align=True) - split.scale_y = 2 - split.operator(AddonUpdaterUpdateNow.bl_idname, - text="Update now to " + str(updater.update_version)) - split = sub_col.split(align=True) - split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="", icon="FILE_REFRESH") - - elif updater.update_ready and updater.manual_only: - col.scale_y = 2 - dl_txt = "Download " + str(updater.update_version) - col.operator("wm.url_open", text=dl_txt).url = updater.website - else: # i.e. that updater.update_ready == False. - sub_col = col.row(align=True) - sub_col.scale_y = 1 - split = sub_col.split(align=True) - split.enabled = False - split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="Addon is up to date") - split = sub_col.split(align=True) - split.scale_y = 2 - split.operator(AddonUpdaterCheckNow.bl_idname, - text="", icon="FILE_REFRESH") - - row = element.row() - row.prop(settings, "auto_check_update") - - row = element.row() - row.scale_y = 0.7 - last_check = updater.json["last_check"] - if updater.error is not None and updater.error_msg is not None: - row.label(text=updater.error_msg) - elif last_check != "" and last_check is not None: - last_check = last_check[0: last_check.index(".")] - row.label(text="Last check: " + last_check) - else: - row.label(text="Last check: Never") - - -def skip_tag_function(self, tag): - """A global function for tag skipping. - - A way to filter which tags are displayed, e.g. to limit downgrading too - long ago. - - Args: - self: The instance of the singleton addon update. - tag: the text content of a tag from the repo, e.g. "v1.2.3". - - Returns: - bool: True to skip this tag name (ie don't allow for downloading this - version), or False if the tag is allowed. - """ - - # In case of error importing updater. - if self.invalid_updater: - return False - - # ---- write any custom code here, return true to disallow version ---- # - # - # # Filter out e.g. if 'beta' is in name of release - # if 'beta' in tag.lower(): - # return True - # ---- write any custom code above, return true to disallow version --- # - - if self.include_branches: - for branch in self.include_branch_list: - if tag["name"].lower() == branch: - return False - - # Function converting string to tuple, ignoring e.g. leading 'v'. - # Be aware that this strips out other text that you might otherwise - # want to be kept and accounted for when checking tags (e.g. v1.1a vs 1.1b) - tupled = self.version_tuple_from_text(tag["name"]) - if not isinstance(tupled, tuple): - return True - - # Select the min tag version - change tuple accordingly. - if self.version_min_update is not None: - if tupled < self.version_min_update: - return True # Skip if current version below this. - - # Select the max tag version. - if self.version_max_update is not None: - if tupled >= self.version_max_update: - return True # Skip if current version at or above this. - - # In all other cases, allow showing the tag for updating/reverting. - # To simply and always show all tags, this return False could be moved - # to the start of the function definition so all tags are allowed. - return False - - -def select_link_function(self, tag): - """Only customize if trying to leverage "attachments" in *GitHub* releases. - - A way to select from one or multiple attached downloadable files from the - server, instead of downloading the default release/tag source code. - """ - - # -- Default, universal case (and is the only option for GitLab/Bitbucket) - link = tag["zipball_url"] - - # -- Example: select the first (or only) asset instead source code -- - # if "assets" in tag and "browser_download_url" in tag["assets"][0]: - # link = tag["assets"][0]["browser_download_url"] - - # -- Example: select asset based on OS, where multiple builds exist -- - # # not tested/no error checking, modify to fit your own needs! - # # assume each release has three attached builds: - # # release_windows.zip, release_OSX.zip, release_linux.zip - # # This also would logically not be used with "branches" enabled - # if platform.system() == "Darwin": # ie OSX - # link = [asset for asset in tag["assets"] if 'OSX' in asset][0] - # elif platform.system() == "Windows": - # link = [asset for asset in tag["assets"] if 'windows' in asset][0] - # elif platform.system() == "Linux": - # link = [asset for asset in tag["assets"] if 'linux' in asset][0] - - return link - - -# ----------------------------------------------------------------------------- -# Register, should be run in the register module itself -# ----------------------------------------------------------------------------- -classes = ( - AddonUpdaterInstallPopup, - AddonUpdaterCheckNow, - AddonUpdaterUpdateNow, - AddonUpdaterUpdateTarget, - AddonUpdaterInstallManually, - AddonUpdaterUpdatedSuccessful, - AddonUpdaterRestoreBackup, - AddonUpdaterIgnore, - AddonUpdaterEndBackground -) - - -def register(bl_info): - """Registering the operators in this module""" - # Safer failure in case of issue loading module. - if updater.error: - print("Exiting updater registration, " + updater.error) - return - updater.clear_state() # Clear internal vars, avoids reloading oddities. - - # Confirm your updater "engine" (Github is default if not specified). - updater.engine = "Forgejo" - # updater.engine = "Github" - # updater.engine = "GitLab" - # updater.engine = "Bitbucket" - - # If using private repository, indicate the token here. - # Must be set after assigning the engine. - # **WARNING** Depending on the engine, this token can act like a password!! - # Only provide a token if the project is *non-public*, see readme for - # other considerations and suggestions from a security standpoint. - updater.private_token = None # "tokenstring" - - # Choose your own username, must match website (not needed for GitLab). - updater.user = "pointerstudio" - - # Choose your own repository, must match git name for GitHUb and Bitbucket, - # for GitLab use project ID (numbers only). - updater.repo = "font3d_blender_addon" - - # updater.addon = # define at top of module, MUST be done first - - # Website for manual addon download, optional but recommended to set. - updater.website = "https://git.pointer.click/pointerstudio/font3d_blender_addon" - - # Addon subfolder path. - # "sample/path/to/addon" - # default is "" or None, meaning root - updater.subfolder_path = "" - - # Used to check/compare versions. - updater.current_version = bl_info["version"] - - # Optional, to hard-set update frequency, use this here - however, this - # demo has this set via UI properties. - # updater.set_check_interval(enabled=False, months=0, days=0, hours=0, minutes=2) - - # Optional, consider turning off for production or allow as an option - # This will print out additional debugging info to the console - updater.verbose = True # make False for production default - - # Optional, customize where the addon updater processing subfolder is, - # essentially a staging folder used by the updater on its own - # Needs to be within the same folder as the addon itself - # Need to supply a full, absolute path to folder - # updater.updater_path = # set path of updater folder, by default: - # /addons/{__package__}/{__package__}_updater - - # Auto create a backup of the addon when installing other versions. - updater.backup_current = True # True by default - - # Sample ignore patterns for when creating backup of current during update. - updater.backup_ignore_patterns = ["__pycache__"] - # Alternate example patterns: - # updater.backup_ignore_patterns = [".git", "__pycache__", "*.bat", ".gitignore", "*.exe"] - - # Patterns for files to actively overwrite if found in new update file and - # are also found in the currently installed addon. Note that by default - # (ie if set to []), updates are installed in the same way as blender: - # .py files are replaced, but other file types (e.g. json, txt, blend) - # will NOT be overwritten if already present in current install. Thus - # if you want to automatically update resources/non py files, add them as a - # part of the pattern list below so they will always be overwritten by an - # update. If a pattern file is not found in new update, no action is taken - # NOTE: This does NOT delete anything proactively, rather only defines what - # is allowed to be overwritten during an update execution. - updater.overwrite_patterns = ["*.png", "*.jpg", "README.md", "LICENSE.txt"] - # updater.overwrite_patterns = [] - # other examples: - # ["*"] means ALL files/folders will be overwritten by update, was the - # behavior pre updater v1.0.4. - # [] or ["*.py","*.pyc"] matches default blender behavior, ie same effect - # if user installs update manually without deleting the existing addon - # first e.g. if existing install and update both have a resource.blend - # file, the existing installed one will remain. - # ["some.py"] means if some.py is found in addon update, it will overwrite - # any existing some.py in current addon install, if any. - # ["*.json"] means all json files found in addon update will overwrite - # those of same name in current install. - # ["*.png","README.md","LICENSE.txt"] means the readme, license, and all - # pngs will be overwritten by update. - - # Patterns for files to actively remove prior to running update. - # Useful if wanting to remove old code due to changes in filenames - # that otherwise would accumulate. Note: this runs after taking - # a backup (if enabled) but before placing in new update. If the same - # file name removed exists in the update, then it acts as if pattern - # is placed in the overwrite_patterns property. Note this is effectively - # ignored if clean=True in the run_update method. - updater.remove_pre_update_patterns = ["*.py", "*.pyc"] - # Note setting ["*"] here is equivalent to always running updates with - # clean = True in the run_update method, ie the equivalent of a fresh, - # new install. This would also delete any resources or user-made/modified - # files setting ["__pycache__"] ensures the pycache folder always removed. - # The configuration of ["*.py", "*.pyc"] is a safe option as this - # will ensure no old python files/caches remain in event different addon - # versions have different filenames or structures. - - # Allow branches like 'master' as an option to update to, regardless - # of release or version. - # Default behavior: releases will still be used for auto check (popup), - # but the user has the option from user preferences to directly - # update to the master branch or any other branches specified using - # the "install {branch}/older version" operator. - updater.include_branches = True - - # (GitHub only) This options allows using "releases" instead of "tags", - # which enables pulling down release logs/notes, as well as installs update - # from release-attached zips (instead of the auto-packaged code generated - # with a release/tag). Setting has no impact on BitBucket or GitLab repos. - updater.use_releases = False - # Note: Releases always have a tag, but a tag may not always be a release. - # Therefore, setting True above will filter out any non-annotated tags. - # Note 2: Using this option will also display (and filter by) the release - # name instead of the tag name, bear this in mind given the - # skip_tag_function filtering above. - - # Populate if using "include_branches" option above. - # Note: updater.include_branch_list defaults to ['master'] branch if set to - # none. Example targeting another multiple branches allowed to pull from: - # updater.include_branch_list = ['master', 'dev'] - updater.include_branch_list = ['main'] # None is the equivalent = ['master'] - - # Only allow manual install, thus prompting the user to open - # the addon's web page to download, specifically: updater.website - # Useful if only wanting to get notification of updates but not - # directly install. - updater.manual_only = False - - # Used for development only, "pretend" to install an update to test - # reloading conditions. - updater.fake_install = False # Set to true to test callback/reloading. - - # Show popups, ie if auto-check for update is enabled or a previous - # check for update in user preferences found a new version, show a popup - # (at most once per blender session, and it provides an option to ignore - # for future sessions); default behavior is set to True. - updater.show_popups = True - # note: if set to false, there will still be an "update ready" box drawn - # using the `update_notice_box_ui` panel function. - - # Override with a custom function on what tags - # to skip showing for updater; see code for function above. - # Set the min and max versions allowed to install. - # Optional, default None - # min install (>=) will install this and higher - updater.version_min_update = (0, 0, 0) - # updater.version_min_update = None # None or default for no minimum. - - # Max install (<) will install strictly anything lower than this version - # number, useful to limit the max version a given user can install (e.g. - # if support for a future version of blender is going away, and you don't - # want users to be prompted to install a non-functioning addon) - # updater.version_max_update = (9,9,9) - updater.version_max_update = None # None or default for no max. - - # Function defined above, customize as appropriate per repository - updater.skip_tag = skip_tag_function # min and max used in this function - - # Function defined above, optionally customize as needed per repository. - updater.select_link = select_link_function - - # Recommended false to encourage blender restarts on update completion - # Setting this option to True is NOT as stable as false (could cause - # blender crashes). - updater.auto_reload_post_update = False - - # The register line items for all operators/panels. - # If using bpy.utils.register_module(__name__) to register elsewhere - # in the addon, delete these lines (also from unregister). - for cls in classes: - # Apply annotations to remove Blender 2.8+ warnings, no effect on 2.7 - make_annotations(cls) - # Comment out this line if using bpy.utils.register_module(__name__) - bpy.utils.register_class(cls) - - # Special situation: we just updated the addon, show a popup to tell the - # user it worked. Could enclosed in try/catch in case other issues arise. - show_reload_popup() - - -def unregister(): - for cls in reversed(classes): - # Comment out this line if using bpy.utils.unregister_module(__name__). - bpy.utils.unregister_class(cls) - - # Clear global vars since they may persist if not restarting blender. - updater.clear_state() # Clear internal vars, avoids reloading oddities. - - global ran_auto_check_install_popup - ran_auto_check_install_popup = False - - global ran_update_success_popup - ran_update_success_popup = False - - global ran_background_check - ran_background_check = False diff --git a/butils.py b/butils.py index af7617d..9e22e7f 100644 --- a/butils.py +++ b/butils.py @@ -605,21 +605,9 @@ def is_mesh(o): return type(o.data) == bpy.types.Mesh def is_metrics_object(o): - if f"{utils.prefix()}_type" in o: - return o[f"{utils.prefix()}_type"] == 'metrics' return (re.match(".*_metrics$", o.name) != None or re.match(".*_metrics.[\d]{3}$", o.name) != None) and is_mesh(o) -def is_text_object(o): - if f"{utils.prefix()}_type" in o: - return o[f"{utils.prefix()}_type"] == 'textobject' - for t in bpy.context.scene.abc3d_data.available_texts: - if o == t.text_object: - return True - return False - def is_glyph(o): - if f"{utils.prefix()}_type" in o: - return o[f"{utils.prefix()}_type"] == 'glyph' try: return type(o.parent) is not type(None) \ and "glyphs" in o.parent.name \ @@ -628,14 +616,6 @@ def is_glyph(o): except ReferenceError as e: return False -def update_types(): - scene = bpy.context.scene - abc3d_data = scene.abc3d_data - for t in abc3d_data.available_texts: - t.text_object[f"{utils.prefix()}_type"] = "textobject" - for g in t.glyphs: - g.glyph_object[f"{utils.prefix()}_type"] = "glyph" - # blender bound_box vertices # # 3------7. @@ -698,11 +678,6 @@ def set_text_on_curve(text_properties, recursive=True): if len(text_properties.text) != len(text_properties.glyphs): regenerate = True - # blender bug - # https://projects.blender.org/blender/blender/issues/100661 - if mom.data.use_path: - regenerate = True - # if we regenerate.... delete objects if regenerate: completely_delete_objects(glyph_objects) @@ -774,12 +749,7 @@ def set_text_on_curve(text_properties, recursive=True): location, tangent, spline_index = calc_point_on_bezier_curve(mom, advance, True, True) if spline_index != previous_spline_index: is_newline = True - - if regenerate: - ob.location = mom.matrix_world @ (location + text_properties.translation) - else: - ob.location = (location + text_properties.translation) - + ob.location = mom.matrix_world @ (location + text_properties.translation) if not text_properties.ignore_orientation: mask = [0] input_rotations = [mathutils.Vector((0.0, 0.0, 0.0))] @@ -795,10 +765,7 @@ def set_text_on_curve(text_properties, recursive=True): ob.rotation_mode = 'QUATERNION' q = mathutils.Quaternion() q.rotate(text_properties.orientation) - if regenerate: - ob.rotation_quaternion = (mom.matrix_world @ motor[0] @ q.to_matrix().to_4x4()).to_quaternion() - else: - ob.rotation_quaternion = (motor[0] @ q.to_matrix().to_4x4()).to_quaternion() + ob.rotation_quaternion = (mom.matrix_world @ motor[0] @ q.to_matrix().to_4x4()).to_quaternion() else: q = mathutils.Quaternion() q.rotate(text_properties.orientation) @@ -845,8 +812,6 @@ def set_text_on_curve(text_properties, recursive=True): if regenerate: mom.select_set(True) - # https://projects.blender.org/blender/blender/issues/100661 - mom.data.use_path = False mom[f"{utils.prefix()}_type"] = "textobject" mom[f"{utils.prefix()}_linked_textobject"] = text_properties.text_id mom[f"{utils.prefix()}_font_name"] = text_properties.font_name diff --git a/common/utils.py b/common/utils.py index d45cd8c..4e2ccec 100644 --- a/common/utils.py +++ b/common/utils.py @@ -4,7 +4,7 @@ def get_version_major(): def get_version_minor(): return 0 def get_version_patch(): - return 2 + return 1 def get_version_string(): return f"{get_version_major()}.{get_version_minor()}.{get_version_patch}" def prefix(): @@ -30,8 +30,6 @@ def mapRange(in_value, in_min, in_max, out_min, out_max, clamp=False): return max(out_max, min(out_min, output)) else: return output - - import warnings import functools @@ -49,9 +47,6 @@ def deprecated(func): return func(*args, **kwargs) return new_func - -import subprocess -import sys def open_file_browser(directory): if sys.platform=='win32': os.startfile(directory)