# ##### 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 ##### import logging import random import time import bpy from bpy.props import BoolProperty, IntProperty, StringProperty from . import client_tasks, global_vars, paths, reports, tasks_queue, utils from .bl_ui_widgets.bl_ui_button import BL_UI_Button from .bl_ui_widgets.bl_ui_drag_panel import BL_UI_Drag_Panel from .bl_ui_widgets.bl_ui_draw_op import BL_UI_OT_draw_operator from .bl_ui_widgets.bl_ui_image import BL_UI_Image from .ui_bgl import get_text_size bk_logger = logging.getLogger(__name__) disclaimer_counter = 0 class BlenderKitDisclaimerOperator(BL_UI_OT_draw_operator): bl_idname = "view3d.blenderkit_disclaimer_widget" bl_label = "BlenderKit disclaimer" bl_description = "BlenderKit disclaimer" bl_options = {"REGISTER"} instances = [] message: StringProperty( # type: ignore[valid-type] name="message", description="message", default="Welcome to BlenderKit!", options={"SKIP_SAVE"}, ) url: StringProperty( # type: ignore[valid-type] name="url", description="ULR", default="www.blenderkit.com", options={"SKIP_SAVE"}, ) fadeout_time: IntProperty( # type: ignore[valid-type] name="Fadout time", description="after how many seconds do fadout", default=5, min=1, max=50, options={"SKIP_SAVE"}, ) tip: BoolProperty( # type: ignore[valid-type] name="Tip", description="Message is a tip, not from server", default=True, options={"SKIP_SAVE"}, ) def cancel_press(self, widget): self.finish() def open_link(self, widget): if self.url == "": return server_url = global_vars.SERVER if self.url[:4] != "http": self.url = server_url + self.url bpy.ops.wm.url_open(url=self.url) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) ui_scale = bpy.context.preferences.view.ui_scale text_size = int(14 * ui_scale) margin = int(10 * ui_scale) area_margin = int(50 * ui_scale) if self.tip: self.bg_color = (0.05, 0.05, 0.05, 0.5) self.hover_bg_color = (0.05, 0.05, 0.05, 1.0) else: self.bg_color = (0.127, 0.034, 1, 0.1) self.hover_bg_color = (0.127, 0.034, 1, 1.0) self.text_color = (0.9, 0.9, 0.9, 1) bk_logger.info("%s", self.message) pix_size = get_text_size( font_id=1, text=self.message, text_size=text_size, dpi=int(bpy.context.preferences.system.dpi / ui_scale), ) self.height = pix_size[1] + 2 * margin self.button_size = int(self.height) self.width = ( pix_size[0] + 2 * margin + 2 * self.button_size ) # adding logo and cancel button to width a = bpy.context.area self.panel = BL_UI_Drag_Panel( area_margin, a.height - self.height - area_margin, self.width, self.height ) self.panel.bg_color = (0.2, 0.2, 0.2, 0.02) self.logo = BL_UI_Image(0, 0, self.button_size, self.button_size) self.label = BL_UI_Button( self.button_size, 0, pix_size[0] + 2 * margin, self.height ) self.label.text = self.message self.label.text_size = text_size self.label.text_color = self.text_color self.label.bg_color = self.bg_color self.label.hover_bg_color = self.hover_bg_color self.label.set_mouse_down(self.open_link) self.button_close = BL_UI_Button( self.width - self.button_size, 0, self.button_size, self.button_size ) self.button_close.bg_color = self.bg_color self.button_close.hover_bg_color = self.hover_bg_color self.button_close.text = "" self.button_close.set_mouse_down(self.cancel_press) def on_invoke(self, context, event): # Add new widgets here (TODO: perhaps a better, more automated solution?) self.context = context self.instances.append(self) widgets_panel = [self.label, self.button_close, self.logo] widgets = [self.panel] widgets += widgets_panel # assign image to the cancel button img_fp = paths.get_addon_thumbnail_path("vs_rejected.png") img_size = int(self.button_size / 2) img_pos = int(img_size / 2) self.button_close.set_image(img_fp) self.button_close.set_image_size((img_size, img_size)) self.button_close.set_image_position((img_pos, img_pos)) img_fp = paths.get_addon_thumbnail_path("blenderkit_logo.png") self.logo.set_image(img_fp) self.logo.set_image_size((img_size, img_size)) self.logo.set_image_position((img_pos, img_pos)) # self.logo.set_image_position(0,0) self.init_widgets(context, widgets) self.panel.add_widgets(widgets_panel) self.start_time = time.time() def modal(self, context, event): if self._finished: return {"FINISHED"} if not context.area: # end if area disappears self.finish() return {"FINISHED"} # so region.tag_redraw() is not called later if self.handle_widget_events(event): self.start_time = time.time() self.reset_colours() return {"RUNNING_MODAL"} if event.type in {"ESC"}: self.finish() return {"FINISHED"} if event.type == "TIMER": run_time = time.time() - self.start_time if run_time > self.fadeout_time: self.fadeout() return {"PASS_THROUGH"} def reset_colours(self): for widget in self.widgets: widget.bg_color = self.bg_color widget.hover_bg_color = self.hover_bg_color if hasattr(widget, "text_color"): widget.text_color = self.text_color def fadeout(self): """Fade out widget after some time""" m = 0.08 all_zero = True for widget in self.widgets: # background color bc = widget.bg_color widget.bg_color = (bc[0], bc[1], bc[2], max(0, bc[3] - m)) if widget.bg_color[3] > 0: # wait for the last to fade out all_zero = False # text color if hasattr(widget, "text_color"): tc = widget.text_color widget.text_color = (tc[0], tc[1], tc[2], max(0, tc[3] - m)) if widget.text_color[3] > 0: # wait for the last to fade out all_zero = False if all_zero: self.finish() @classmethod def unregister(cls): bk_logger.debug(f"unregistering class {cls}") instances_copy = cls.instances.copy() for instance in instances_copy: bk_logger.debug(f"- class instance {instance}") try: instance.unregister_handlers(instance.context) except Exception as e: bk_logger.debug(f"-- error unregister_handlers(): {e}") try: instance.on_finish(instance.context) except Exception as e: bk_logger.debug(f"-- error calling on_finish() {e}") if bpy.context.region is not None: bpy.context.region.tag_redraw() cls.instances.remove(instance) def run_disclaimer_task(message: str, url: str, tip: bool): message = " ".join(message.split()) fake_context = utils.get_fake_context(bpy.context) if bpy.app.version < (4, 0, 0): bpy.ops.view3d.blenderkit_disclaimer_widget( # type: ignore[attr-defined] fake_context, "INVOKE_DEFAULT", message=message, url=url, fadeout_time=8, tip=tip, ) else: with bpy.context.temp_override(**fake_context): # type: ignore[attr-defined] bpy.ops.view3d.blenderkit_disclaimer_widget( # type: ignore[attr-defined] "INVOKE_DEFAULT", message=message, url=url, fadeout_time=8, tip=tip, ) def handle_disclaimer_task(task: client_tasks.Task): """Handles incoming disclaimer task. If there are any results, it shows them in disclaimer popup. If the results are empty, it shows random tip in the disclaimer popup. """ global disclaimer_counter disclaimer_counter = -1 if task.status == "finished": if task.result.get("count", 0) == 0: return show_random_tip() disclaimer = task.result["results"][0] preferences = bpy.context.preferences.addons[__package__].preferences # type: ignore if preferences.announcements_on_start is False: # type: ignore[union-attr] bk_logger.warning( f"Online announcements are disabled! Message hidden from GUI: {disclaimer['message']}" ) return show_random_tip() tasks_queue.add_task( (run_disclaimer_task, (disclaimer["message"], disclaimer["url"], False)), wait=0, ) return if task.status == "error": bk_logger.warning(f"Could not load disclaimer: {task.message}") return show_random_tip() def show_random_tip(): """Shows random tip in the disclaimer popup if tips_on_start are enabled.""" preferences = bpy.context.preferences.addons[__package__].preferences if preferences.tips_on_start is False: return tip = random.choice(global_vars.TIPS) tasks_queue.add_task((run_disclaimer_task, (tip[0], tip[1], True)), wait=0) def register(): bpy.utils.register_class(BlenderKitDisclaimerOperator) def unregister(): bpy.utils.unregister_class(BlenderKitDisclaimerOperator) @bpy.app.handlers.persistent def show_disclaimer_timer(): """Timer responsible for showing the tip disclaimer after the startup once. It waits for BlenderKit-Client to be online, then prompts Client to get the disclaimers and ends. If Client does not go online in few seconds, it shows the tips instead and ends. """ global disclaimer_counter if disclaimer_counter == -1: return if disclaimer_counter > 2: show_random_tip() return disclaimer_counter = disclaimer_counter + 1 return disclaimer_counter