1539 lines
58 KiB
Python
1539 lines
58 KiB
Python
# ##### 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
|