''' 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 . ''' 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)