2025-07-01
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore
|
||||
|
||||
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 3 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, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import gc
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import inspect
|
||||
from datetime import datetime
|
||||
import contextlib
|
||||
from itertools import chain
|
||||
|
||||
|
||||
import urllib.request
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import bpy
|
||||
|
||||
from ...addon_common.cookiecutter.cookiecutter import CookieCutter
|
||||
from ...addon_common.common.blender import get_path_from_addon_root
|
||||
from ...addon_common.common.boundvar import BoundVar, BoundBool, BoundFloat, BoundString
|
||||
from ...addon_common.common.globals import Globals
|
||||
from ...addon_common.common.inspect import ScopeBuilder
|
||||
from ...addon_common.common.profiler import profiler
|
||||
from ...addon_common.common.ui_core import UI_Element
|
||||
from ...addon_common.common.ui_styling import load_defaultstylings
|
||||
from ...addon_common.common.utils import delay_exec
|
||||
from ...addon_common.terminal.deepdebug import DeepDebug
|
||||
|
||||
from ...config.options import (
|
||||
options, themes, visualization,
|
||||
retopoflow_urls, retopoflow_product, retopoflow_files,
|
||||
build_platform,
|
||||
platform_system, platform_node, platform_release, platform_version, platform_machine, platform_processor,
|
||||
gpu_info,
|
||||
)
|
||||
|
||||
def get_environment_details():
|
||||
blender_version = '%d.%02d.%d' % bpy.app.version
|
||||
blender_branch = bpy.app.build_branch.decode('utf-8')
|
||||
blender_date = bpy.app.build_commit_date.decode('utf-8')
|
||||
|
||||
if retopoflow_product['git version']: build_info = f'RF git: {retopoflow_product["git version"]}'
|
||||
elif retopoflow_product['cgcookie built']:
|
||||
if retopoflow_product['github']: build_info = f'CG Cookie built for GitHub'
|
||||
elif retopoflow_product['blender market']: build_info = f'CG Cookie built for Blender Market'
|
||||
else: build_info = f'CG Cookie built for ??'
|
||||
else: build_info = f'Self built'
|
||||
|
||||
return [
|
||||
f'Environment:',
|
||||
f'',
|
||||
f'- RetopoFlow: {retopoflow_product["version"]}',
|
||||
f'- Build: {build_info}',
|
||||
f'- Blender: {blender_version} {blender_branch} {blender_date}',
|
||||
f'- Platform: {platform_system}, {platform_release}, {platform_version}, {platform_machine}, {platform_processor}',
|
||||
f'- GPU: {gpu_info}',
|
||||
f'- Timestamp: {datetime.today().isoformat(" ")}',
|
||||
f'',
|
||||
]
|
||||
|
||||
|
||||
def get_trace_details(undo_stack, msghash=None, message=None):
|
||||
trace_details = [f'Runtime:', f'']
|
||||
trace_details += [f'- Undo: {", ".join(undo_stack[:10])}']
|
||||
if msghash:
|
||||
trace_details += [f'- Error Hash: {msghash}']
|
||||
if message:
|
||||
trace_details += ['', 'Trace:', '']
|
||||
trace_details += [message]
|
||||
trace_details += ['']
|
||||
return trace_details
|
||||
|
||||
def get_debug_details():
|
||||
debug = DeepDebug.read()
|
||||
if not debug: return []
|
||||
return ['Debug:', ''] + debug.splitlines()
|
||||
|
||||
|
||||
|
||||
class RetopoFlow_UI_Alert:
|
||||
GitHub_checks = 0
|
||||
GitHub_limit = 10
|
||||
|
||||
@CookieCutter.Exception_Callback
|
||||
def handle_exception(self, e):
|
||||
print('RetopoFlow_UI_Alert.handle_exception', e)
|
||||
if False:
|
||||
for entry in inspect.stack():
|
||||
print(f' {entry}')
|
||||
message, h = Globals.debugger.get_exception_info_and_hash()
|
||||
message = '\n'.join(f'- {l}' for l in message.splitlines())
|
||||
self.alert_user(
|
||||
title='Exception caught',
|
||||
message=message,
|
||||
level='exception',
|
||||
msghash=h,
|
||||
)
|
||||
if hasattr(self, 'rftool'): self.rftool._reset()
|
||||
|
||||
def alert_user(self, message=None, title=None, level=None, msghash=None):
|
||||
scope = ScopeBuilder()
|
||||
|
||||
if not hasattr(self, '_msghashes'): self._msghashes = set()
|
||||
if not hasattr(self, 'alert_windows'): self.alert_windows = 0
|
||||
if msghash and msghash in self._msghashes: return # have already seen this error!!
|
||||
self._msghashes.add(msghash)
|
||||
|
||||
show_quit = False
|
||||
level = level.lower() if level else 'note'
|
||||
blender_version = '%d.%02d.%d' % bpy.app.version
|
||||
blender_branch = bpy.app.build_branch.decode('utf-8')
|
||||
blender_date = bpy.app.build_commit_date.decode('utf-8')
|
||||
darken = False
|
||||
|
||||
ui_checker = None
|
||||
ui_show = None
|
||||
message_orig = message
|
||||
report_details = ''
|
||||
msg_report = None
|
||||
issue_body_report = None
|
||||
|
||||
if title is None and self.rftool: title = self.rftool.name
|
||||
|
||||
def screenshot():
|
||||
ss_filename = retopoflow_files['screenshot filename']
|
||||
if getattr(bpy.data, 'filepath', ''):
|
||||
# loaded .blend file
|
||||
filepath = os.path.split(os.path.abspath(bpy.data.filepath))[0]
|
||||
filepath = os.path.join(filepath, ss_filename)
|
||||
else:
|
||||
# startup file
|
||||
filepath = os.path.abspath(ss_filename)
|
||||
bpy.ops.screen.screenshot(filepath=filepath)
|
||||
self.alert_user(message=f'Saved screenshot to "{filepath}"')
|
||||
def open_issues():
|
||||
bpy.ops.wm.url_open(url=retopoflow_urls['github issues'])
|
||||
def search():
|
||||
url = f'https://github.com/CGCookie/retopoflow/issues?q=is%3Aissue+{msghash}'
|
||||
bpy.ops.wm.url_open(url=url)
|
||||
def report():
|
||||
nonlocal issue_body_report
|
||||
nonlocal report_details
|
||||
|
||||
path = get_path_from_addon_root('help', 'issue_template_simple.md')
|
||||
issue_template = open(path, 'rt').read()
|
||||
data = {
|
||||
'title': f'{self.rftool.name}: {title}',
|
||||
'body': f'{issue_template}\n\n```\n{issue_body_report}\n```',
|
||||
}
|
||||
url = f'{retopoflow_urls["new github issue"]}?{urllib.parse.urlencode(data)}'
|
||||
bpy.ops.wm.url_open(url=url)
|
||||
|
||||
if msghash:
|
||||
ui_checker = UI_Element.DETAILS(classes='issue-checker', open=True)
|
||||
UI_Element.SUMMARY(innerText='Report an issue', parent=ui_checker)
|
||||
ui_label = UI_Element.ARTICLE(classes='mdown', parent=ui_checker)
|
||||
ui_buttons = UI_Element.DIV(parent=ui_checker, classes='action-buttons')
|
||||
|
||||
ui_label.set_markdown(mdown='Checking reported issues...')
|
||||
|
||||
def check_github():
|
||||
nonlocal win, ui_buttons
|
||||
buttons = 4
|
||||
try:
|
||||
if self.GitHub_checks < self.GitHub_limit:
|
||||
self.GitHub_checks += 1
|
||||
# attempt to see if this issue already exists!
|
||||
# note: limited to 60 requests/hour! see
|
||||
# https://developer.github.com/v3/#rate-limiting
|
||||
# https://developer.github.com/v3/search/#rate-limit
|
||||
|
||||
# make it unsecure to work around SSL issue
|
||||
# https://medium.com/@moreless/how-to-fix-python-ssl-certificate-verify-failed-97772d9dd14c
|
||||
import ssl
|
||||
if (not os.environ.get('PYTHONHTTPSVERIFY', '') and getattr(ssl, '_create_unverified_context', None)):
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
url = "https://api.github.com/repos/CGCookie/retopoflow/issues?state=all"
|
||||
response = urllib.request.urlopen(url)
|
||||
text = response.read().decode('utf-8')
|
||||
issues = json.loads(text)
|
||||
exists,solved,issueurl = False,False,None
|
||||
for issue in issues:
|
||||
if msghash not in issue['body']: continue
|
||||
issueurl = issue['html_url']
|
||||
exists = True
|
||||
if issue['state'] == 'closed': solved = True
|
||||
if not exists:
|
||||
print('GitHub: Not reported, yet')
|
||||
ui_label.set_markdown(mdown='This issue does not appear to be reported, yet.\n\nPlease consider reporting it so we can fix it.')
|
||||
else:
|
||||
if not solved:
|
||||
print('GitHub: Already reported!')
|
||||
ui_label.set_markdown('This issue appears to have been reported already.\n\nClick Open button to see the current status.')
|
||||
else:
|
||||
print('GitHub: Already solved!')
|
||||
ui_label.set_markdown('This issue appears to have been solved already!\n\nAn updated RetopoFlow should fix this issue.')
|
||||
def go():
|
||||
bpy.ops.wm.url_open(url=issueurl)
|
||||
UI_Element.BUTTON(innerText='Open', on_mouseclick=go, title='Open this issue on the RetopoFlow Issue Tracker', classes='fifth-size', parent=ui_buttons)
|
||||
buttons = 5
|
||||
else:
|
||||
ui_label.set_markdown('Could not run the check.\n\nPlease consider reporting it so we can fix it.')
|
||||
except Exception as e:
|
||||
ui_label.set_markdown('Sorry, but we could not reach the RetopoFlow Isssues Tracker.\n\nClick the Similar button to search for similar issues.')
|
||||
pass
|
||||
print('Caught exception while trying to pull issues from GitHub')
|
||||
print(f'URL: "{url}"')
|
||||
print(e)
|
||||
# ignore for now
|
||||
pass
|
||||
size = 'fourth-size' if buttons==4 else 'fifth-size'
|
||||
UI_Element.BUTTON(innerText='Screenshot', classes=f'action {size}', parent=ui_buttons, on_mouseclick=screenshot, title='Save a screenshot of Blender')
|
||||
UI_Element.BUTTON(innerText='Similar', classes=f'action {size}', parent=ui_buttons, on_mouseclick=search, title='Search the RetopoFlow Issue Tracker for similar issues')
|
||||
UI_Element.BUTTON(innerText='All Issues', classes=f'action {size}', parent=ui_buttons, on_mouseclick=open_issues, title='Open RetopoFlow Issue Tracker')
|
||||
UI_Element.BUTTON(innerText='Report', classes=f'action {size}', parent=ui_buttons, on_mouseclick=report, title='Report a new issue on the RetopoFlow Issue Tracker')
|
||||
|
||||
executor = ThreadPoolExecutor()
|
||||
executor.submit(check_github)
|
||||
|
||||
msg_report = ''
|
||||
issue_body_report = ''
|
||||
if level in {'note'}:
|
||||
title = 'Note' + (f': {title}' if title else '')
|
||||
message = message or 'a note'
|
||||
elif level in {'warning'}:
|
||||
title = 'Warning' + (f': {title}' if title else '')
|
||||
darken = True
|
||||
elif level in {'error'}:
|
||||
title = 'Error' + (f': {title}' if title else '!')
|
||||
show_quit = True
|
||||
darken = True
|
||||
elif level in {'assert', 'exception'}:
|
||||
self.save_emergency() # make an emergency save!
|
||||
|
||||
if level == 'assert':
|
||||
title = 'Assert Error' + (f': {title}' if title else '!')
|
||||
desc = 'An internal assertion has failed.'
|
||||
else:
|
||||
title = 'Unhandled Exception Caught' + (f': {title}' if title else '!')
|
||||
desc = 'An unhandled exception was thrown.'
|
||||
|
||||
message = '\n'.join([
|
||||
desc,
|
||||
'This was unexpected.',
|
||||
'',
|
||||
'If this happens again, please report as bug so we can fix it.',
|
||||
])
|
||||
|
||||
undo_stack_actions = self.undo_stack_actions() if hasattr(self, 'undo_stack_actions') else []
|
||||
msg_report = '\n'.join(chain(
|
||||
get_environment_details(),
|
||||
get_trace_details(undo_stack_actions, msghash=msghash, message=message_orig),
|
||||
get_debug_details(),
|
||||
))
|
||||
issue_body_report = '\n'.join(chain(
|
||||
get_environment_details(),
|
||||
get_trace_details(undo_stack_actions, msghash=msghash, message=message_orig),
|
||||
))
|
||||
|
||||
show_quit = True
|
||||
darken = True
|
||||
else:
|
||||
title = level.upper() + (f': {title}' if title else '')
|
||||
message = message or 'a note'
|
||||
|
||||
@scope.capture_fn
|
||||
def close():
|
||||
nonlocal win
|
||||
if win.parent:
|
||||
self.document.body.delete_child(win)
|
||||
self.alert_windows -= 1
|
||||
if self.document.sticky_element == win:
|
||||
self.document.sticky_element = None
|
||||
self.document.clear_last_under()
|
||||
@scope.capture_fn
|
||||
def mouseleave_event(e):
|
||||
nonlocal win
|
||||
if not win.is_hovered: close()
|
||||
@scope.capture_fn
|
||||
def keypress_event(e):
|
||||
if e.key == 'ESC': close()
|
||||
@scope.capture_fn
|
||||
def quit():
|
||||
self.done()
|
||||
@scope.capture_fn
|
||||
def copy_to_clipboard():
|
||||
nonlocal msg_report
|
||||
try: bpy.context.window_manager.clipboard = msg_report
|
||||
except: pass
|
||||
|
||||
if self.alert_windows >= 5:
|
||||
return
|
||||
#self.exit = True
|
||||
|
||||
scope.capture_var('level')
|
||||
|
||||
win = UI_Element.fromHTMLFile(
|
||||
get_path_from_addon_root('retopoflow', 'html', 'alert_dialog.html'),
|
||||
frame_depth=2,
|
||||
**scope
|
||||
)[0]
|
||||
self.document.body.append_child(win)
|
||||
win.getElementById('alert-title').innerText = title
|
||||
win.getElementById('alert-message').set_markdown(mdown=message, frame_depth=2, **scope)
|
||||
if not msg_report and not ui_checker:
|
||||
win.getElementById('alert-details').is_visible = False
|
||||
if msg_report: win.getElementById('alert-report').innerText = msg_report
|
||||
else: win.getElementById('alert-report').is_visible = False
|
||||
if ui_checker: win.getElementById('alert-checker').append_child(ui_checker)
|
||||
else: win.getElementById('alert-checker').is_visible = False
|
||||
if not show_quit:
|
||||
win.getElementById('alert-close').style = 'width:100%'
|
||||
win.getElementById('alert-quit').is_visible = False
|
||||
|
||||
self.document.focus(win)
|
||||
self.alert_windows += 1
|
||||
if level in {'warning', 'note', None}:
|
||||
win.style = 'width:600px;'
|
||||
self.document.force_clean(self.actions.context)
|
||||
self.document.center_on_mouse(win)
|
||||
# self.document.sticky_element = win
|
||||
win.dirty(cause='new window', parent=False, children=True)
|
||||
else:
|
||||
self.document.force_clean(self.actions.context)
|
||||
self.document.center_on_mouse(win)
|
||||
win.dirty(cause='new window', parent=False, children=True)
|
||||
if level in {'note', None}:
|
||||
win.add_eventListener('on_mouseleave', mouseleave_event)
|
||||
win.add_eventListener('on_keypress', keypress_event)
|
||||
Reference in New Issue
Block a user