Files
blender-portable-repo/extensions/user_default/blenderkit/asset_bar_op.py
T
2026-03-17 15:25:32 -06:00

2606 lines
100 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ##### 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 math
import os
import re
import time
from typing import Any, Dict, Union
import bpy
from bpy.props import BoolProperty, StringProperty
from . import (
colors,
comments_utils,
global_vars,
paths,
ratings_utils,
search,
ui,
ui_panels,
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 .bl_ui_widgets.bl_ui_label import BL_UI_Label
from .bl_ui_widgets.bl_ui_widget import BL_UI_Widget
bk_logger = logging.getLogger(__name__)
active_area_pointer = 0
def get_area_height(self):
if type(self.context) != dict:
if self.context is None:
self.context = bpy.context
self.context = self.context.copy()
if self.context.get("area") is not None:
return self.context["area"].height
# else:
# maxw, maxa, region = utils.get_largest_area()
# if maxa:
# self.context['area'] = maxa
# self.context['window'] = maxw
# self.context['region'] = region
# self.update(self.x,self.y)
#
# return self.context['area'].height
return 100
BL_UI_Widget.get_area_height = get_area_height # type: ignore[method-assign]
def modal_inside(self, context, event):
ui_props = bpy.context.window_manager.blenderkitUI
if ui_props.turn_off:
ui_props.turn_off = False
self.finish()
if self._finished:
return {"FINISHED"}
user_preferences = bpy.context.preferences.addons[__package__].preferences
# HANDLE PHOTO THUMBNAIL SWITCH
if hasattr(self, "needs_tooltip_update") and self.needs_tooltip_update:
self.needs_tooltip_update = False
sr = search.get_search_results()
if sr and self.active_index < len(sr):
asset_data = sr[self.active_index]
if asset_data["assetType"].lower() == "printable":
if self.show_photo_thumbnail:
photo_img = ui.get_full_photo_thumbnail(asset_data)
if photo_img:
self.tooltip_image.set_image(photo_img.filepath)
self.tooltip_image.set_image_colorspace("")
else:
self.tooltip_image.set_image(
paths.get_addon_thumbnail_path("thumbnail_notready.jpg")
)
else:
set_thumb_check(
self.tooltip_image, asset_data, thumb_type="thumbnail"
)
if not context.area:
self.finish()
w, a, r = utils.get_largest_area(area_type="VIEW_3D")
if a is not None:
bpy.ops.view3d.run_assetbar_fix_context(keep_running=True, do_search=False)
return {"FINISHED"}
sr = search.get_search_results()
if sr is not None:
# this check runs more search, useful especially for first search. Could be moved to a better place where the check
# doesn't run that often.
# Calculate current max rows based on expanded state
if user_preferences.assetbar_expanded:
current_max_rows = user_preferences.maximized_assetbar_rows
else:
current_max_rows = 1
if len(sr) - ui_props.scroll_offset < (ui_props.wcount * current_max_rows) + 15:
self.search_more()
time_diff = time.time() - self.update_timer_start
if time_diff > self.update_timer_limit:
self.update_timer_start = time.time()
# self.update_buttons()
# progress bar
# change - let's try to optimize and redraw only when needed
change = False
for asset_button in self.asset_buttons:
if not asset_button.visible:
continue
if sr is not None and len(sr) > asset_button.asset_index:
asset_data = sr[asset_button.asset_index]
self.update_progress_bar(asset_button, asset_data)
if change:
context.region.tag_redraw()
# Check for tab shortcut keys directly in the modal function
if (
event.ctrl
and event.value == "PRESS"
and self.panel.is_in_rect(self.mouse_x, self.mouse_y)
):
if event.type == "T" and not event.shift:
bk_logger.info("Ctrl+T pressed - add new tab")
if hasattr(self, "new_tab_button"): # Only if we can add more tabs
self.add_new_tab(None)
return {"RUNNING_MODAL"}
elif event.type == "W" and not event.shift:
bk_logger.info("Ctrl+W pressed - close tab")
if len(global_vars.TABS["tabs"]) > 1: # Don't close last tab
self.remove_tab(self.close_tab_buttons[global_vars.TABS["active_tab"]])
return {"RUNNING_MODAL"}
elif event.type == "TAB":
bk_logger.info("Ctrl+Tab pressed - switch tab")
tabs = global_vars.TABS["tabs"]
current = global_vars.TABS["active_tab"]
if event.shift:
# Go to previous tab
new_index = (current - 1) % len(tabs)
else:
# Go to next tab
new_index = (current + 1) % len(tabs)
self.switch_to_history_step(new_index, tabs[new_index]["history_index"])
return {"RUNNING_MODAL"}
elif event.type in {
"ONE",
"TWO",
"THREE",
"FOUR",
"FIVE",
"SIX",
"SEVEN",
"EIGHT",
"NINE",
}:
# Convert numkey to index (0-based)
tab_idx = {
"ONE": 0,
"TWO": 1,
"THREE": 2,
"FOUR": 3,
"FIVE": 4,
"SIX": 5,
"SEVEN": 6,
"EIGHT": 7,
"NINE": 8,
}[event.type]
if tab_idx < len(global_vars.TABS["tabs"]):
bk_logger.info(f"Ctrl+{tab_idx+1} pressed - go to tab {tab_idx+1}")
self.switch_to_history_step(
tab_idx, global_vars.TABS["tabs"][tab_idx]["history_index"]
)
return {"RUNNING_MODAL"}
# Handle Alt+Left/Right for history navigation
elif (
event.alt
and event.value == "PRESS"
and self.panel.is_in_rect(self.mouse_x, self.mouse_y)
):
if event.type == "LEFT_ARROW":
bk_logger.info("Alt+Left pressed - history back")
active_tab = global_vars.TABS["tabs"][global_vars.TABS["active_tab"]]
if active_tab["history_index"] > 0:
self.history_back(None) # None instead of widget
return {"RUNNING_MODAL"}
elif event.type == "RIGHT_ARROW":
bk_logger.info("Alt+Right pressed - history forward")
active_tab = global_vars.TABS["tabs"][global_vars.TABS["active_tab"]]
if active_tab["history_index"] < len(active_tab["history"]) - 1:
self.history_forward(None) # None instead of widget
return {"RUNNING_MODAL"}
# ANY EVENT ACTIVATED = DON'T LET EVENTS THROUGH
if self.handle_widget_events(event):
return {"RUNNING_MODAL"}
if event.type in {"ESC"} and event.value == "PRESS":
# just escape dragging when dragging, not appending.
if not ui_props.dragging:
self.finish()
# return {"FINISHED"} # we can jump out immediately
self.mouse_x = event.mouse_region_x
self.mouse_y = event.mouse_region_y
# TRACKPAD SCROLL
if event.type == "TRACKPADPAN" and self.panel.is_in_rect(
self.mouse_x, self.mouse_y
):
# accumulate trackpad inputs
self.trackpad_x_accum -= event.mouse_x - event.mouse_prev_x
self.trackpad_y_accum += event.mouse_y - event.mouse_prev_y
step = 0
multiplier = 30
if abs(self.trackpad_x_accum) > abs(self.trackpad_y_accum) or self.hcount < 2:
step = math.floor(self.trackpad_x_accum / multiplier)
self.trackpad_x_accum -= step * multiplier
# reset the other axis not to accidentally scroll it
if step != 0:
self.trackpad_y_accum = 0
if abs(self.trackpad_y_accum) > 0 and self.hcount > 1:
step = self.wcount * math.floor(self.trackpad_x_accum / multiplier)
self.trackpad_y_accum -= step * multiplier
# reset the other axis not to accidentally scroll it
if step != 0:
self.trackpad_x_accum = 0
if step != 0:
self.scroll_offset += step
self.scroll_update()
return {"RUNNING_MODAL"}
# MOUSEWHEEL SCROLL
if event.type == "WHEELUPMOUSE" and self.panel.is_in_rect(
self.mouse_x, self.mouse_y
):
if self.hcount > 1:
self.scroll_offset -= self.wcount
else:
self.scroll_offset -= 2
self.scroll_update()
return {"RUNNING_MODAL"}
elif event.type == "WHEELDOWNMOUSE" and self.panel.is_in_rect(
self.mouse_x, self.mouse_y
):
if self.hcount > 1:
self.scroll_offset += self.wcount
else:
self.scroll_offset += 2
self.scroll_update()
return {"RUNNING_MODAL"}
if self.check_ui_resized(context) or self.check_new_search_results(context):
self.update_assetbar_sizes(context)
self.update_assetbar_layout(context)
# also update tooltip visibility
# if there's less results and active button is not visible, hide tooltip
# happened only when e.g. running new search from web browser (copying assetbaseid to clipboard)
# fixes issue #1766
if self.active_index >= len(search.get_search_results()):
self.hide_tooltip()
self.scroll_update(
always=True
) # one extra update for scroll for correct redraw, updates all buttons
# this was here to check if sculpt stroke is running, but obviously that didn't help,
# since the RELEASE event is caught by operator and thus there is no way to detect a stroke has ended...
if bpy.context.mode in ("SCULPT", "PAINT_TEXTURE"):
if (
event.type == "MOUSEMOVE"
): # ASSUME THAT SCULPT OPERATOR ACTUALLY STEALS THESE EVENTS,
# SO WHEN THERE ARE SOME WE CAN APPEND BRUSH...
bpy.context.window_manager["appendable"] = True
if event.type == "LEFTMOUSE":
if event.value == "PRESS":
bpy.context.window_manager["appendable"] = False
return {"PASS_THROUGH"}
def asset_bar_modal(self, context, event):
return modal_inside(self, context, event)
def asset_bar_invoke(self, context, event):
# sprinkling of black magic
result = self.on_invoke(context, event)
if not result:
return {"CANCELLED"}
if not context.window:
return {"CANCELLED"}
if not context.area:
return {"CANCELLED"}
args = (self, context)
self.register_handlers(args, context)
self.update_timer_limit = 0.5
self.update_timer_start = time.time()
self._timer = context.window_manager.event_timer_add(0.5, window=context.window)
context.window_manager.modal_handler_add(self)
global active_area_pointer
self.active_window_pointer = context.window.as_pointer()
self.active_area_pointer = context.area.as_pointer()
active_area_pointer = self.active_area_pointer
self.active_region_pointer = context.region.as_pointer()
return {"RUNNING_MODAL"}
# def set_mouse_down_right(self, mouse_down_right_func):
# self.mouse_down_right_func = mouse_down_right_func
# def mouse_down_right(self, x, y):
# if self.is_in_rect(x, y):
# self.__state = 1
# try:
# self.mouse_down_right_func(self)
# except Exception as e:
# bk_logger.warning(f"{e}")
# return True
# return False
# BL_UI_Button.mouse_down_right = mouse_down_right # type: ignore[method-assign]
# BL_UI_Button.set_mouse_down_right = set_mouse_down_right # type: ignore[attr-defined]
asset_bar_operator = None
def get_tooltip_data(asset_data):
tooltip_data = asset_data.get("tooltip_data")
if tooltip_data is not None:
return
author_text = ""
if global_vars.BKIT_AUTHORS:
author_id = int(asset_data["author"]["id"])
author = global_vars.BKIT_AUTHORS.get(author_id)
if author:
if len(author.firstName) > 0 or len(author.lastName) > 0:
author_text = f"by {author.firstName} {author.lastName}"
else:
bk_logger.warning(f"get_tooltip_data() AUTHOR NOT FOUND: {author_id}")
aname = asset_data["displayName"]
if len(aname) == 0:
# this shouldn't happen, but obviously did on server in addons section.
aname = ""
else:
aname = aname[0].upper() + aname[1:]
if len(aname) > 36:
aname = f"{aname[:33]}..."
rc = asset_data.get("ratingsCount")
show_rating_threshold = 0
rcount = 0
quality = "-"
if rc:
rcount = min(rc.get("quality", 0), rc.get("workingHours", 0))
if rcount > show_rating_threshold:
quality = str(round(asset_data["ratingsAverage"].get("quality")))
# Add pricing information
price_text = ""
price_color = colors.WHITE
price_background = (0, 0, 0, 0)
def format_price(value):
if value is None:
return ""
value_str = str(value).strip()
if not value_str:
return ""
if value_str.startswith("$"):
return value_str
return f"${value_str}"
# Check if asset is free or paid (works for all asset types)
is_free = asset_data.get("isFree", True)
can_download = asset_data.get("canDownload", True)
if asset_data.get("assetType") == "addon":
# Get pricing info from extensions cache.
# Pricing info is shown only for add-ons.
base_price = format_price(asset_data.get("basePrice"))
user_price = format_price(asset_data.get("userPrice"))
is_for_sale = asset_data.get("isForSale")
if utils.profile_is_validator():
segments = []
if user_price:
segments.append(f"User {user_price}")
if base_price:
segments.append(f"Base {base_price}")
price_text = " | ".join(segments)
price_background = colors.PURPLE_PRICE
elif is_for_sale and not can_download and user_price and base_price:
price_text = f"{user_price} (was {base_price})"
price_background = colors.PURPLE_PRICE
elif is_for_sale and not can_download and base_price:
price_text = base_price
price_background = colors.PURPLE_PRICE
elif not is_free and not is_for_sale:
price_text = "Full Plan"
price_background = colors.ORANGE_FULL
elif is_for_sale and can_download:
price_text = "Purchased"
price_background = colors.PURPLE_PRICE
else:
price_text = "Free"
price_background = colors.GREEN_PRICE
tooltip_data = {
"aname": aname,
"author_text": author_text,
"quality": quality,
"price_text": price_text,
"price_color": price_color,
"price_background": price_background,
}
asset_data["tooltip_data"] = tooltip_data
def set_thumb_check(
element: Union[BL_UI_Button, BL_UI_Image],
asset: Dict[str, Any],
thumb_type: str = "thumbnail_small",
) -> None:
"""Set image in case it is loaded in search results. Checks global_vars.DATA["images available"].
- if image download failed, it will be set to 'thumbnail_not_available.jpg'
- if image doesn't exist, it will be set to 'thumbnail_notready.jpg'
"""
directory = paths.get_temp_dir("%s_search" % asset["assetType"])
tpath = os.path.join(directory, asset[thumb_type])
if element.get_image_path() == tpath:
return # no need to update
image_ready = global_vars.DATA["images available"].get(tpath)
if image_ready is None:
tpath = paths.get_addon_thumbnail_path("thumbnail_notready.jpg")
if image_ready is False or asset[thumb_type] == "":
tpath = paths.get_addon_thumbnail_path("thumbnail_not_available.jpg")
if element.get_image_path() == tpath:
return
element.set_image(tpath)
element.set_image_colorspace("")
class BlenderKitAssetBarOperator(BL_UI_OT_draw_operator):
"""BlenderKit Asset Bar Operator."""
bl_idname = "view3d.blenderkit_asset_bar_widget"
bl_label = "BlenderKit asset bar refresh"
bl_description = "BlenderKit asset bar refresh"
bl_options = {"REGISTER"}
instances = []
do_search: BoolProperty( # type: ignore[valid-type]
name="Run Search", description="", default=True, options={"SKIP_SAVE"}
)
keep_running: BoolProperty( # type: ignore[valid-type]
name="Keep Running", description="", default=True, options={"SKIP_SAVE"}
)
category: StringProperty( # type: ignore[valid-type]
name="Category",
description="search only subtree of this category",
default="",
options={"SKIP_SAVE"},
)
tooltip: bpy.props.StringProperty( # type: ignore[valid-type]
default="Runs search and displays the asset bar at the same time"
)
show_photo_thumbnail: BoolProperty( # type: ignore[valid-type]
name="Show Photo Thumbnail",
description="Toggle between normal and photo thumbnail - use [ or ] to cycle through thumbnails. Currently used only for printables.",
default=False,
options={"SKIP_SAVE"},
)
@classmethod
def description(cls, context, properties):
"""Get the description for the asset bar operator."""
return properties.tooltip
def new_text(self, text, x, y, width=100, height=15, text_size=None, halign="LEFT"):
"""Create a new text label widget."""
label = BL_UI_Label(x, y, width, height)
label.text = text
if text_size is None:
text_size = 14
label.text_size = text_size
label.text_color = self.text_color
label._halign = halign
return label
def init_tooltip(self):
"""Initialize the tooltip panel and its widgets."""
self.tooltip_widgets = []
self.tooltip_scale = 1.0
# Fallbacks in case update_tooltip_size was not called yet
self.tooltip_width = getattr(self, "tooltip_width", self.tooltip_size)
image_height = getattr(self, "tooltip_image_height", self.tooltip_size)
info_height = getattr(
self,
"tooltip_info_height",
max(
int(image_height * self.bottom_panel_fraction),
self.asset_name_text_size * 3,
),
)
self.tooltip_image_height = image_height
self.tooltip_info_height = info_height
self.tooltip_height = self.tooltip_image_height + self.tooltip_info_height
self.labels_start = self.tooltip_image_height
# total_size = tooltip# + 2 * self.margin
self.tooltip_panel = BL_UI_Drag_Panel(
0, 0, self.tooltip_width, self.tooltip_height
)
self.tooltip_panel.bg_color = (0.0, 0.0, 0.0, 0.5)
self.tooltip_panel.visible = False
tooltip_image = BL_UI_Image(0, 0, 1, 1)
img_path = paths.get_addon_thumbnail_path("thumbnail_notready.jpg")
tooltip_image.set_image(img_path)
tooltip_image.set_image_size((self.tooltip_width, self.tooltip_image_height))
tooltip_image.set_image_position((0, 0))
tooltip_image.set_image_colorspace("")
self.tooltip_image = tooltip_image
self.tooltip_widgets.append(tooltip_image)
dark_panel = BL_UI_Widget(
0,
self.labels_start,
self.tooltip_width,
self.tooltip_info_height,
)
dark_panel.bg_color = (0.0, 0.0, 0.0, 0.7)
self.tooltip_dark_panel = dark_panel
self.tooltip_widgets.append(dark_panel)
name_label = self.new_text(
"",
self.tooltip_margin,
self.labels_start + self.tooltip_margin,
height=self.asset_name_text_size,
text_size=self.asset_name_text_size,
)
self.asset_name = name_label
self.tooltip_widgets.append(name_label)
self.gravatar_size = max(
int(self.tooltip_info_height - 2 * self.tooltip_margin),
self.asset_name_text_size,
)
authors_name = self.new_text(
"author",
self.tooltip_width - self.gravatar_size - self.tooltip_margin,
self.tooltip_height - self.author_text_size - self.tooltip_margin,
self.labels_start,
height=self.author_text_size,
text_size=self.author_text_size,
halign="RIGHT",
)
self.authors_name = authors_name
self.tooltip_widgets.append(authors_name)
gravatar_image = BL_UI_Image(
self.tooltip_width - self.gravatar_size - self.tooltip_margin,
self.tooltip_height - self.gravatar_size - self.tooltip_margin,
1,
1,
)
img_path = paths.get_addon_thumbnail_path("thumbnail_notready.jpg")
gravatar_image.set_image(img_path)
gravatar_image.set_image_size(
(
self.gravatar_size,
self.gravatar_size,
)
)
gravatar_image.set_image_position((0, 0))
gravatar_image.set_image_colorspace("")
self.gravatar_image = gravatar_image
self.tooltip_widgets.append(gravatar_image)
quality_star = BL_UI_Image(
self.tooltip_margin,
self.tooltip_height - self.tooltip_margin - self.asset_name_text_size,
1,
1,
)
img_path = paths.get_addon_thumbnail_path("star_grey.png")
quality_star.set_image(img_path)
quality_star.set_image_size(
(self.asset_name_text_size, self.asset_name_text_size)
)
quality_star.set_image_position((0, 0))
self.quality_star = quality_star
self.tooltip_widgets.append(quality_star)
quality_label = self.new_text(
"",
2 * self.tooltip_margin + self.asset_name_text_size,
self.tooltip_height - int(self.asset_name_text_size + self.tooltip_margin),
height=self.asset_name_text_size,
text_size=self.asset_name_text_size,
)
self.tooltip_widgets.append(quality_label)
self.quality_label = quality_label
# Add price label for addons
price_label = self.new_text(
"",
self.tooltip_margin,
self.tooltip_height
- int(self.asset_name_text_size + 2 * self.tooltip_margin),
height=self.asset_name_text_size,
text_size=self.asset_name_text_size,
)
price_label.background = True
price_label.padding = (3, 4)
price_label.text_color = (
1.0,
0.8,
0.2,
1.0,
) # Golden color for price
self.tooltip_widgets.append(price_label)
self.price_label = price_label
user_preferences = bpy.context.preferences.addons[__package__].preferences
offset = 0
if (
user_preferences.asset_popup_counter
< user_preferences.asset_popup_counter_max
) or utils.profile_is_validator():
# this is shown only to users who don't know yet about the popup card.
label = self.new_text(
"Right click for menu.",
self.tooltip_margin,
self.tooltip_height + self.tooltip_margin,
height=self.author_text_size,
text_size=self.author_text_size,
)
self.tooltip_widgets.append(label)
self.comments = label
offset += 1
if utils.profile_is_validator():
label.multiline = True
label.text = "No comments yet."
# version warning
version_warning = self.new_text(
"",
self.tooltip_margin,
self.tooltip_height
+ self.tooltip_margin
+ int(self.author_text_size * offset),
height=self.author_text_size,
text_size=self.author_text_size,
)
version_warning.text_color = self.warning_color
self.tooltip_widgets.append(version_warning)
self.version_warning = version_warning
def hide_tooltip(self):
"""Hide the tooltip panel and its widgets."""
self.tooltip_panel.visible = False
for w in self.tooltip_widgets:
w.visible = False
def show_tooltip(self):
"""Show the tooltip panel and its widgets."""
self.tooltip_panel.visible = True
self.tooltip_panel.active = False
for w in self.tooltip_widgets:
w.visible = True
def show_notifications(self, widget):
"""Show notifications on the asset bar."""
bpy.ops.wm.show_notifications()
if comments_utils.check_notifications_read():
widget.visible = False
def check_new_search_results(self, context):
"""checks if results were replaced.
this can happen from search, but also by switching results.
We should rather trigger that update from search. maybe let's add a uuid to the results?
"""
# Get search results from history
sr = search.get_search_results()
if not hasattr(self, "search_results_count"):
if not sr or len(sr) == 0:
self.search_results_count = 0
self.last_asset_type = ""
return True
self.search_results_count = len(sr)
self.last_asset_type = sr[0]["assetType"]
if sr is not None and len(sr) != self.search_results_count:
self.search_results_count = len(sr)
return True
return False
def get_region_size(self, context):
"""Get the size of the region."""
# just check the size of region..
region = context.region
area = context.area
ui_width = 0
tools_width = 0
for r in area.regions:
if r.type == "UI":
ui_width = r.width
if r.type == "TOOLS":
tools_width = r.width
total_width = region.width - tools_width - ui_width
return total_width, region.height
def check_ui_resized(self, context):
"""Check if the UI has been resized."""
# TODO this should only check if region was resized, not really care about the UI elements size.
region_width, region_height = self.get_region_size(context)
if not hasattr(self, "total_width"):
self.total_width = region_width
self.region_height = region_height
if region_height != self.region_height or region_width != self.total_width:
self.region_height = region_height
self.total_width = region_width
return True
return False
def update_tooltip_size(self, context):
"""Calculate all important sizes for the tooltip"""
region = context.region
ui_props = bpy.context.window_manager.blenderkitUI
ui_scale = self.get_ui_scale()
base_panel_height = self.tooltip_base_size_pixels * (
1 + self.bottom_panel_fraction
)
if hasattr(self, "tooltip_panel"):
tooltip_y_available_height = abs(
region.height - self.tooltip_panel.y_screen
)
# if tooltip is above, we need to reduce it's size if its y is out of region height
if self.tooltip_panel.y_screen <= 0:
tooltip_y_available_height = (
base_panel_height * ui_scale + self.tooltip_panel.y_screen
)
self.tooltip_panel.set_location(self.tooltip_panel.x, 0)
else:
tooltip_y_available_height = abs(
region.height - (self.bar_height + self.bar_y)
)
self.tooltip_scale = min(
1.0, tooltip_y_available_height / (base_panel_height * ui_scale)
)
self.asset_name_text_size = int(
0.039 * self.tooltip_base_size_pixels * ui_scale * self.tooltip_scale
)
self.author_text_size = int(self.asset_name_text_size * 0.8)
self.tooltip_size = int(
self.tooltip_base_size_pixels * ui_scale * self.tooltip_scale
)
self.tooltip_margin = int(
0.017 * self.tooltip_base_size_pixels * ui_scale * self.tooltip_scale
)
if ui_props.asset_type == "HDR":
self.tooltip_width = self.tooltip_size * 2
self.tooltip_image_height = self.tooltip_size
else:
self.tooltip_width = self.tooltip_size
self.tooltip_image_height = self.tooltip_size
self.tooltip_info_height = max(
int(self.tooltip_image_height * self.bottom_panel_fraction),
self.asset_name_text_size * 3,
)
self.labels_start = self.tooltip_image_height
self.tooltip_height = self.tooltip_image_height + self.tooltip_info_height
self.gravatar_size = max(
int(self.tooltip_info_height - 2 * self.tooltip_margin),
self.asset_name_text_size,
)
def get_ui_scale(self):
"""Get the UI scale"""
ui_scale = bpy.context.preferences.view.ui_scale
pixel_size = bpy.context.preferences.system.pixel_size
if pixel_size > 1:
# for a reason unknown,
# the pixel size is modified only on mac
# where pixel size is 2.0
ui_scale = pixel_size
return ui_scale
def update_assetbar_sizes(self, context):
"""Calculate all important sizes for the asset bar"""
region = context.region
area = context.area
ui_props = bpy.context.window_manager.blenderkitUI
user_preferences = bpy.context.preferences.addons[__package__].preferences
ui_scale = self.get_ui_scale()
# assetbar scaling
self.button_margin = int(0 * ui_scale)
self.assetbar_margin = int(2 * ui_scale)
self.thumb_size = int(user_preferences.thumb_size * ui_scale)
self.button_size = 2 * self.button_margin + self.thumb_size
self.other_button_size = int(30 * ui_scale)
self.icon_size = int(24 * ui_scale)
self.validation_icon_margin = int(3 * ui_scale)
reg_multiplier = 1
if not bpy.context.preferences.system.use_region_overlap:
reg_multiplier = 0
ui_width = 0
tools_width = 0
reg_multiplier = 1
if not bpy.context.preferences.system.use_region_overlap:
reg_multiplier = 0
for r in area.regions:
if r.type == "UI":
ui_width = r.width * reg_multiplier
if r.type == "TOOLS":
tools_width = r.width * reg_multiplier
self.bar_x = int(
tools_width + self.button_margin + ui_props.bar_x_offset * ui_scale
)
# self.bar_y = region.height - ui_props.bar_y_offset * ui_scale
self.bar_y = int(ui_props.bar_y_offset * ui_scale)
self.bar_end = int(ui_width + 180 * ui_scale + self.other_button_size)
self.bar_width = int(region.width - self.bar_x - self.bar_end)
self.wcount = math.floor((self.bar_width) / (self.button_size))
self.max_hcount = math.floor(
max(region.width, context.window.width) / self.button_size
)
self.max_wcount = user_preferences.maximized_assetbar_rows
history_step = search.get_active_history_step()
search_results = history_step.get("search_results")
# we need to init all possible thumb previews in advance/
# Calculate hcount based on expanded state
if search_results is not None and self.wcount > 0:
if user_preferences.assetbar_expanded:
max_rows = user_preferences.maximized_assetbar_rows
available_height = (
region.height
- self.bar_y
- 2 * self.assetbar_margin
- self.other_button_size
)
max_rows_by_height = math.floor(available_height / self.button_size)
max_rows = (
min(max_rows, max_rows_by_height) if max_rows_by_height > 0 else 1
)
else:
max_rows = 1
self.hcount = min(
max_rows,
math.ceil(len(search_results) / self.wcount),
)
self.hcount = max(self.hcount, 1)
else:
self.hcount = 1
self.bar_height = (self.button_size) * self.hcount + 2 * self.assetbar_margin
if ui_props.down_up == "UPLOAD":
self.reports_y = region.height - self.bar_y - 600
ui_props.reports_y = region.height - self.bar_y - 600
self.reports_x = self.bar_x
ui_props.reports_x = self.bar_x
else: # ui.bar_y - ui.bar_height - 100
self.reports_y = region.height - self.bar_y - self.bar_height - 50
ui_props.reports_y = region.height - self.bar_y - self.bar_height - 50
self.reports_x = self.bar_x
ui_props.reports_x = self.bar_x
def update_ui_size(self, context):
"""Calculate all important sizes for the asset bar and tooltip"""
self.update_assetbar_sizes(context)
self.update_tooltip_size(context)
def update_assetbar_layout(self, context):
"""Update the layout of the asset bar"""
# usually restarting asset_bar completely since the widgets are too hard to get working with updates.
self.scroll_update(always=True)
self.position_and_hide_buttons()
self.button_close.set_location(
self.bar_width - self.other_button_size, -self.other_button_size
)
self.button_expand.set_location(
self.bar_width - self.other_button_size, self.bar_height
)
# if hasattr(self, 'button_notifications'):
# self.button_notifications.set_location(self.bar_width - self.other_button_size * 2, -self.other_button_size)
self.button_scroll_up.set_location(self.bar_width, 0)
self.panel.width = self.bar_width
self.panel.height = self.bar_height
# Update tab area background position
self.tab_area_bg.width = self.bar_width
self.panel.set_location(self.bar_x, self.panel.y)
## the block bellow can be probably removed
# Update tab icons positions
for i, tab_button in enumerate(self.tab_buttons):
if hasattr(tab_button, "asset_type_icon"):
tab_button.asset_type_icon.set_location(
tab_button.x
+ int(
self.other_button_size * 0.05
), # Position at left with small margin
tab_button.y
+ int(
(self.other_button_size - tab_button.asset_type_icon.height) / 2
), # Center vertically
)
def update_tooltip_layout(self, context):
"""Update the layout of the tooltip"""
# update Tooltip size /scale for HDR or if area too small
self.tooltip_panel.width = self.tooltip_width
self.tooltip_panel.height = self.tooltip_height
self.tooltip_image.width = self.tooltip_width
self.tooltip_image.height = self.tooltip_image_height
self.labels_start = self.tooltip_image_height
self.tooltip_image.set_image_size(
(self.tooltip_width, self.tooltip_image_height)
)
self.tooltip_image.set_location(0, 0)
self.gravatar_image.set_location(
self.tooltip_width - self.gravatar_size - self.tooltip_margin,
self.tooltip_height - self.gravatar_size - self.tooltip_margin,
)
self.gravatar_image.set_image_size(
(
self.gravatar_size,
self.gravatar_size,
)
)
self.authors_name.set_location(
self.tooltip_width - self.gravatar_size - (self.tooltip_margin * 2),
self.tooltip_height - self.author_text_size - self.tooltip_margin,
)
self.authors_name.text_size = self.author_text_size
self.authors_name.height = self.author_text_size
self.asset_name.set_location(
self.tooltip_margin,
self.labels_start + self.tooltip_margin,
)
self.asset_name.text_size = self.asset_name_text_size
self.asset_name.height = self.asset_name_text_size
self.tooltip_dark_panel.set_location(
0,
self.labels_start,
)
self.tooltip_dark_panel.height = self.tooltip_info_height
self.tooltip_dark_panel.width = self.tooltip_width
self.quality_label.set_location(
2 * self.tooltip_margin + self.asset_name_text_size,
self.tooltip_height - int(self.asset_name_text_size + self.tooltip_margin),
)
self.quality_label.text_size = self.asset_name_text_size
self.quality_label.height = self.asset_name_text_size
self.quality_star.set_location(
self.tooltip_margin,
self.tooltip_height - self.tooltip_margin - self.asset_name_text_size,
)
self.quality_star.set_image_size(
(self.asset_name_text_size, self.asset_name_text_size)
)
# right after the asset name
self.price_label.set_location(
self.tooltip_margin,
self.labels_start + (self.tooltip_margin * 3) + self.asset_name.height,
)
self.price_label.width = self.tooltip_width - 2 * self.tooltip_margin
self.price_label.height = self.asset_name_text_size
self.price_label.text_size = self.asset_name_text_size
def update_layout(self, context, event):
"""update UI sizes after their recalculation"""
self.update_assetbar_layout(context)
self.update_tooltip_layout(context)
def asset_button_init(self, asset_x, asset_y, button_idx):
"""Initialize an asset button at the given position with the given index."""
button_bg_color = (0.2, 0.2, 0.2, 0.1)
button_hover_color = (0.8, 0.8, 0.8, 0.2)
fully_transparent_color = (0.2, 0.2, 0.2, 0.0)
new_button = BL_UI_Button(asset_x, asset_y, self.button_size, self.button_size)
new_button.bg_color = button_bg_color
new_button.hover_bg_color = button_hover_color
new_button.text = "" # asset_data['name']
new_button.set_image_size((self.thumb_size, self.thumb_size))
new_button.set_image_position((self.button_margin, self.button_margin))
new_button.button_index = button_idx
new_button.search_index = button_idx
new_button.set_mouse_down(self.drag_drop_asset)
new_button.set_mouse_down_right(self.asset_menu)
new_button.set_mouse_enter(self.enter_button)
new_button.set_mouse_exit(self.exit_button)
new_button.text_input = self.handle_key_input
# add validation icon to button
validation_icon = BL_UI_Image(
asset_x
+ self.button_size
- self.icon_size
- self.button_margin
- self.validation_icon_margin,
asset_y
+ self.button_size
- self.icon_size
- self.button_margin
- self.validation_icon_margin,
0,
0,
)
validation_icon.set_image_size((self.icon_size, self.icon_size))
validation_icon.set_image_position((0, 0))
self.validation_icons.append(validation_icon)
new_button.validation_icon = validation_icon
bookmark_button = BL_UI_Button(
asset_x
+ self.button_size
- self.icon_size
- self.button_margin
- self.validation_icon_margin,
asset_y + self.button_margin + self.validation_icon_margin,
self.icon_size,
self.icon_size,
)
bookmark_button.set_image_size((self.icon_size, self.icon_size))
bookmark_button.set_image_position((0, 0))
bookmark_button.button_index = button_idx
bookmark_button.search_index = button_idx
bookmark_button.text = ""
bookmark_button.set_mouse_down(self.bookmark_asset)
img_fp = paths.get_addon_thumbnail_path("bookmark_empty.png")
bookmark_button.set_image(img_fp)
bookmark_button.bg_color = fully_transparent_color
bookmark_button.hover_bg_color = self.button_bg_color
bookmark_button.select_bg_color = fully_transparent_color
bookmark_button.visible = False
new_button.bookmark_button = bookmark_button
self.bookmark_buttons.append(bookmark_button)
progress_bar = BL_UI_Widget(
asset_x, asset_y + self.button_size - 6, self.button_size, 6
)
progress_bar.bg_color = (0.0, 1.0, 0.0, 0.3)
new_button.progress_bar = progress_bar
self.progress_bars.append(progress_bar)
if utils.profile_is_validator():
red_alert = BL_UI_Widget(
asset_x - self.validation_icon_margin,
asset_y - self.validation_icon_margin,
self.button_size + 2 * self.validation_icon_margin,
self.button_size + 2 * self.validation_icon_margin,
)
red_alert.bg_color = (1.0, 0.0, 0.0, 0.0)
red_alert.visible = False
red_alert.active = False
new_button.red_alert = red_alert
self.red_alerts.append(red_alert)
# if result['downloaded'] > 0:
# ui_bgl.draw_rect(x, y, int(ui_props.thumb_size * result['downloaded'] / 100.0), 2, green)
return new_button
def init_ui(self):
"""Initialize the asset bar UI and its widgets."""
self.button_bg_color = (0.2, 0.2, 0.2, 1.0)
self.button_hover_color = (0.8, 0.8, 0.8, 1.0)
self.button_selected_color = (0.5, 0.5, 0.5, 1.0)
self.button_selected_color_dim = (0.3, 0.3, 0.3, 1.0)
self.buttons = []
self.asset_buttons = []
self.validation_icons = []
self.bookmark_buttons = []
self.progress_bars = []
self.red_alerts = []
self.widgets_panel = []
self.tab_buttons = []
self.close_tab_buttons = []
# Create panel with extended height
self.panel = BL_UI_Drag_Panel(
0,
0,
self.bar_width,
self.bar_height, # Use total height including tabs
)
self.panel.bg_color = (0.0, 0.0, 0.0, 0.9)
# Create tab area background
self.tab_area_bg = BL_UI_Widget(
0, # x position will be set in update_assetbar_layout
-self.other_button_size, # Position at top where tabs are
self.bar_width, # Same width as asset bar
self.other_button_size, # Same height as tab buttons
)
# dark blue
self.tab_area_bg.bg_color = colors.TOP_BAR_BLUE
# Add widgets to panel - add tab background first so it's behind everything
self.widgets_panel.append(self.tab_area_bg)
# we init max possible buttons.
button_idx = 0
for x in range(0, self.max_wcount):
for y in range(0, self.max_hcount):
# asset_x = self.assetbar_margin + a * (self.button_size)
# asset_y = self.assetbar_margin + b * (self.button_size)
# button_idx = x + y * self.max_wcount
asset_idx = button_idx + self.scroll_offset
# if asset_idx < len(sr):
new_button = self.asset_button_init(0, 0, button_idx)
new_button.asset_index = asset_idx
self.asset_buttons.append(new_button)
button_idx += 1
self.button_close = BL_UI_Button(
self.bar_width - self.other_button_size,
-self.other_button_size,
self.other_button_size,
self.other_button_size,
)
self.button_close.bg_color = self.button_bg_color
self.button_close.hover_bg_color = self.button_hover_color
self.button_close.text = "×"
self.button_close.text_size = self.other_button_size * 0.8
self.button_close.set_image_position((0, 0))
self.button_close.set_image_size(
(self.other_button_size, self.other_button_size)
)
self.button_close.set_mouse_down(self.cancel_press)
self.widgets_panel.append(self.button_close)
# Expand/collapse button (positioned at bottom of assetbar)
self.button_expand = BL_UI_Button(
self.bar_width - self.other_button_size,
self.bar_height,
self.other_button_size,
self.other_button_size,
)
self.button_expand.bg_color = self.button_bg_color
self.button_expand.hover_bg_color = self.button_hover_color
self.button_expand.text = ""
self.button_expand.text_size = self.other_button_size * 0.8
self.button_expand.set_image_position((0, 0))
self.button_expand.set_image_size(
(self.other_button_size, self.other_button_size)
)
self.button_expand.set_mouse_down(self.toggle_expand)
self.widgets_panel.append(self.button_expand)
self.scroll_width = 30
self.button_scroll_down = BL_UI_Button(
-self.scroll_width, 0, self.scroll_width, self.bar_height
)
self.button_scroll_down.bg_color = self.button_bg_color
self.button_scroll_down.hover_bg_color = self.button_hover_color
self.button_scroll_down.text = ""
self.button_scroll_down.set_image_size((self.scroll_width, self.button_size))
self.button_scroll_down.set_image_position(
(0, int((self.bar_height - self.button_size) / 2))
)
self.button_scroll_down.set_mouse_down(self.scroll_down)
self.widgets_panel.append(self.button_scroll_down)
self.button_scroll_up = BL_UI_Button(
self.bar_width, 0, self.scroll_width, self.bar_height
)
self.button_scroll_up.bg_color = self.button_bg_color
self.button_scroll_up.hover_bg_color = self.button_hover_color
self.button_scroll_up.text = ""
self.button_scroll_up.set_image_size((self.scroll_width, self.button_size))
self.button_scroll_up.set_image_position(
(0, int((self.bar_height - self.button_size) / 2))
)
self.button_scroll_up.set_mouse_down(self.scroll_up)
self.widgets_panel.append(self.button_scroll_up)
# Add tab navigation elements
button_size = self.other_button_size
margin = int(button_size * 0.05)
space = int(button_size * 0.4)
tab_icon_size = int(button_size * 0.7) # Size for the asset type icon
tab_width = (
button_size * 4 + tab_icon_size
) # Widen the tabs to accommodate type icon
# Back/Forward history buttons
self.history_back_button = BL_UI_Button(
margin, -button_size, button_size, button_size
)
self.history_back_button.bg_color = self.button_bg_color
self.history_back_button.hover_bg_color = self.button_hover_color
self.history_back_button.text = ""
icon_size = int(button_size * 0.6)
margin_lr = int((button_size - icon_size) / 2)
self.history_back_button.set_image(
paths.get_addon_thumbnail_path("history_back.png")
)
self.history_back_button.set_image_size((icon_size, icon_size))
self.history_back_button.set_image_position((margin_lr, margin_lr))
self.history_forward_button = BL_UI_Button(
margin * 2 + button_size,
-button_size,
button_size,
button_size,
)
self.history_forward_button.bg_color = self.button_bg_color
self.history_forward_button.hover_bg_color = self.button_hover_color
self.history_forward_button.text = ""
self.history_forward_button.set_image(
paths.get_addon_thumbnail_path("history_forward.png")
)
self.history_forward_button.set_image_size((icon_size, icon_size))
self.history_forward_button.set_image_position((margin_lr, margin_lr))
# Tab buttons
tabs = global_vars.TABS["tabs"]
tab_x_start = margin * 4 + button_size * 3 # Starting x position of first tab
tabs_end_x = 0
for i, tab in enumerate(tabs):
is_active = i == global_vars.TABS["active_tab"]
# Calculate positions
tab_x = tab_x_start + i * (
tab_width + button_size + margin + space
) # Space for tab and close button
# Tab button
tab_button = BL_UI_Button(
tab_x, # Position with spacing for close buttons
-button_size,
tab_width, # Width of tab
button_size,
)
tab_button.hover_bg_color = self.button_hover_color
tab_button.text = tab["name"]
tab_button.text_size = button_size * 0.5
tab_button.text_color = self.text_color
tab_button.bg_color = self.button_bg_color
if is_active:
tab_button.bg_color = self.button_selected_color
tab_button.tab_index = i # Store tab index
tab_button.set_mouse_down(self.switch_tab) # Add click handler
self.tab_buttons.append(tab_button)
# Set asset type icon as tab button image
tab_button.set_image_size((tab_icon_size, tab_icon_size))
tab_button.set_image_position(
(margin * 2, (button_size - tab_icon_size) / 2)
) # Center vertically
# Only create close button if there's more than one tab
close_x = tab_x + tab_width + margin # Position right after tab
close_tab = BL_UI_Button(
close_x,
-button_size,
button_size,
button_size,
)
close_tab.bg_color = self.button_bg_color
# slightly red
close_tab.hover_bg_color = (0.8, 0.0, 0.0, 0.2)
close_tab.text = "×" # Set text after creation
close_tab.text_size = button_size * 0.8
close_tab.text_color = self.text_color
if is_active:
close_tab.bg_color = self.button_selected_color_dim
close_tab.tab_index = i # Store tab index
# if there's only one tab, the button closes asset bar instead of closing tab
if len(tabs) > 1:
close_tab.set_mouse_down(self.remove_tab) # Add click handler
else:
close_tab.set_mouse_down(self.cancel_press)
self.close_tab_buttons.append(close_tab)
tabs_end_x = close_x + button_size
# New tab button - position after all tabs and close buttons
if len(tabs) > 0:
new_tab_x = (
space + tabs_end_x + margin * 2
) # After last tab and its close button
else:
new_tab_x = tab_x_start # If no tabs, start at the beginning
# if too close to the right side, let's not create this button
if new_tab_x + button_size < self.bar_width:
self.new_tab_button = BL_UI_Button(
new_tab_x,
-button_size,
button_size,
button_size,
)
# Change from default button color to slightly green
self.new_tab_button.bg_color = (0.2, 0.5, 0.2, 1.0) # Green tint
# Slightly lighter green on hover
self.new_tab_button.hover_bg_color = (0.3, 0.7, 0.3, 0.5)
self.new_tab_button.text = "+"
self.new_tab_button.text_size = button_size * 0.8
self.new_tab_button.text_color = self.text_color
self.new_tab_button.set_mouse_down(self.add_new_tab)
self.widgets_panel.append(self.new_tab_button)
# Then add all other widgets
self.widgets_panel.extend(
[
self.history_back_button,
self.history_forward_button,
]
)
self.widgets_panel.extend(self.tab_buttons)
self.widgets_panel.extend(self.close_tab_buttons)
# Back/Forward history buttons
self.history_back_button.set_mouse_down(self.history_back)
self.history_forward_button.set_mouse_down(self.history_forward)
# Set initial visibility based on history
active_tab = global_vars.TABS["tabs"][global_vars.TABS["active_tab"]]
self.history_back_button.visible = active_tab["history_index"] > 0
self.history_forward_button.visible = (
active_tab["history_index"] < len(active_tab["history"]) - 1
)
def set_element_images(self):
"""set ui elements images, has to be done after init of UI."""
# img_fp = paths.get_addon_thumbnail_path("vs_rejected.png")
# self.button_close.set_image(img_fp)
self.button_scroll_down.set_image(
paths.get_addon_thumbnail_path("arrow_left.png")
)
self.button_scroll_up.set_image(
paths.get_addon_thumbnail_path("arrow_right.png")
)
# if not comments_utils.check_notifications_read():
# img_fp = paths.get_addon_thumbnail_path('bell.png')
# self.button_notifications.set_image(img_fp)
# Update tab icons
self.update_tab_icons()
# Update expand button icon
self.update_expand_button_icon()
def update_tab_icons(self):
"""Update tab icons based on the active history step's asset type"""
tabs = global_vars.TABS["tabs"]
for i, tab_button in enumerate(self.tab_buttons):
if i >= len(tabs):
continue
tab = tabs[i]
history_index = tab["history_index"]
if history_index >= 0 and history_index < len(tab["history"]):
history_step = tab["history"][history_index]
ui_state = history_step.get("ui_state", {})
ui_props = ui_state.get("ui_props", {})
asset_type = ui_props.get("asset_type", "").lower()
# Set the icon based on asset type
if asset_type:
icon_path = paths.get_addon_thumbnail_path(
f"asset_type_{asset_type}.png"
)
if not paths.icon_path_exists(icon_path):
icon_path = paths.get_addon_thumbnail_path(
"asset_type_model.png"
)
tab_button.set_image(icon_path)
tab_button.set_image_colorspace("")
def update_expand_button_icon(self):
"""Update expand button icon based on current expanded state."""
user_preferences = bpy.context.preferences.addons[__package__].preferences
if user_preferences.assetbar_expanded:
# Show up arrow when expanded (to collapse)
self.button_expand.text = ""
else:
# Show down arrow when collapsed (to expand)
self.button_expand.text = ""
def position_and_hide_buttons(self):
"""Position asset buttons in the asset bar and hide unused buttons."""
# position and layout buttons
sr = search.get_search_results()
if sr is None:
sr = []
i = 0
for y in range(0, self.hcount):
for x in range(0, self.wcount):
asset_x = self.assetbar_margin + x * (self.button_size)
asset_y = self.assetbar_margin + y * (self.button_size)
button_idx = x + y * self.wcount
asset_idx = button_idx + self.scroll_offset
if len(self.asset_buttons) <= button_idx:
break
button = self.asset_buttons[button_idx]
button.set_location(asset_x, asset_y)
button.validation_icon.set_location(
asset_x
+ self.button_size
- self.icon_size
- self.button_margin
- self.validation_icon_margin,
asset_y
+ self.button_size
- self.icon_size
- self.button_margin
- self.validation_icon_margin,
)
button.bookmark_button.set_location(
asset_x
+ self.button_size
- self.icon_size
- self.button_margin
- self.validation_icon_margin,
asset_y + self.button_margin + self.validation_icon_margin,
)
button.progress_bar.set_location(
asset_x, asset_y + self.button_size - 6
)
if asset_idx < len(sr):
button.visible = True
button.validation_icon.visible = True
button.bookmark_button.visible = False
# button.progress_bar.visible = True
else:
button.visible = False
button.validation_icon.visible = False
button.bookmark_button.visible = False
button.progress_bar.visible = False
if utils.profile_is_validator():
button.red_alert.set_location(
asset_x - self.validation_icon_margin,
asset_y - self.validation_icon_margin,
)
i += 1
for a in range(i, len(self.asset_buttons)):
button = self.asset_buttons[a]
button.visible = False
button.validation_icon.visible = False
button.bookmark_button.visible = False
button.progress_bar.visible = False
self.button_scroll_down.height = self.bar_height
self.button_scroll_down.set_image_position(
(0, int((self.bar_height - self.button_size) / 2))
)
self.button_scroll_up.height = self.bar_height
self.button_scroll_up.set_image_position(
(0, int((self.bar_height - self.button_size) / 2))
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def on_init(self, context):
"""Initialize the asset bar operator."""
self.tooltip_base_size_pixels = 512
self.tooltip_scale = 1.0
self.bottom_panel_fraction = 0.18
self.needs_tooltip_update = False
self.update_ui_size(bpy.context)
# todo move all this to update UI size
ui_props = context.window_manager.blenderkitUI
self.draw_tooltip = False
# let's take saved scroll offset and use it to keep scroll between operator runs
self.last_scroll_offset = -10 # set to -10 so it updates on first run
self.scroll_offset = ui_props.scroll_offset
self.text_color = (0.9, 0.9, 0.9, 1.0)
self.warning_color = (0.9, 0.5, 0.5, 1.0)
self.init_ui()
self.init_tooltip()
self.hide_tooltip()
self.trackpad_x_accum = 0
self.trackpad_y_accum = 0
def setup_widgets(self, context, event):
"""Set up all widgets for the asset bar and tooltip."""
widgets_panel = []
widgets_panel.extend(self.widgets_panel)
widgets_panel.extend(self.buttons)
widgets_panel.extend(self.asset_buttons)
widgets_panel.extend(self.red_alerts)
# we try to put bookmark_buttons before others, because they're on top
widgets_panel.extend(self.bookmark_buttons)
widgets_panel.extend(self.validation_icons)
widgets_panel.extend(self.progress_bars)
widgets = [self.panel]
widgets += widgets_panel
widgets.append(self.tooltip_panel)
widgets += self.tooltip_widgets
self.init_widgets(context, widgets)
self.panel.add_widgets(widgets_panel)
self.tooltip_panel.add_widgets(self.tooltip_widgets)
def on_invoke(self, context, event):
"""Invoke the asset bar operator."""
self.context = context
self.instances.append(self)
if not context.area:
return False
self.on_init(context)
self.context = context
# start search if there isn't a search result yet
if not search.get_search_results():
search.search()
ui_props = context.window_manager.blenderkitUI
if ui_props.assetbar_on:
# TODO solve this otherwise to enable more asset bars?
# we don't want to run the assetbar many times, that's why it has a switch on/off behaviour,
# unless being called with 'keep_running'
if not self.keep_running:
# this sends message to the originally running operator, so it quits, and then it ends this one too.
# If it initiated a search, the search will finish in a thread. The switch off procedure is run
# by the 'original' operator, since if we get here, it means
# same operator is already running.
ui_props.turn_off = True
# if there was an error, we need to turn off these props so we can restart after 2 clicks
ui_props.assetbar_on = False
return False
ui_props.assetbar_on = True
global asset_bar_operator
asset_bar_operator = self
self.active_index = -1
self.check_new_search_results(context)
self.setup_widgets(context, event)
self.set_element_images()
self.position_and_hide_buttons()
self.hide_tooltip()
# for b in self.buttons:
# b.bookmark_button.visible=False
self.panel.set_location(self.bar_x, self.bar_y)
# to hide arrows accordingly
self.scroll_update(always=True)
self.window = context.window
self.area = context.area
self.scene = bpy.context.scene
return True
def on_finish(self, context):
# redraw all areas, since otherwise it stays to hang for some more time.
# bpy.types.SpaceView3D.draw_handler_remove(self._handle_2d_tooltip, 'WINDOW')
# to pass the operator to validation icons
global asset_bar_operator
asset_bar_operator = None
context.window_manager.event_timer_remove(self._timer)
ui_props = bpy.context.window_manager.blenderkitUI
ui_props.assetbar_on = False
ui_props.scroll_offset = self.scroll_offset
# for w in wm.windows:
# for a in w.screen.areas:
# a.tag_redraw()
self._finished = True
def update_tooltip_image(self, asset_id):
"""Update tooltip image when it finishes downloading and the downloaded image matches the active one."""
search_results = search.get_search_results()
if search_results is None:
return
if self.active_index == -1: # prev search got no results
return
if self.active_index >= len(search_results):
return
asset_data = search_results[self.active_index]
if asset_data["assetBaseId"] == asset_id:
set_thumb_check(self.tooltip_image, asset_data, thumb_type="thumbnail")
def update_comments_for_validators(self, asset_data):
"""Update the comments section in the tooltip for validator profiles."""
if not utils.profile_is_validator():
return
comments = global_vars.DATA.get("asset comments", {})
comments = comments.get(asset_data["assetBaseId"], [])
comment_text = "No comments yet."
if comments is not None:
comment_text = ""
# iterate comments from last to first
for comment in reversed(comments):
comment_text += f"{comment['userName']}:\n"
# strip urls and stuff
comment_lines = comment["comment"].split("\n")
for line in comment_lines:
urls, text = utils.has_url(line)
if urls:
comment_text += f"{text}{urls[0][0]}\n"
else:
comment_text += f"{text}\n"
comment_text += "\n"
self.comments.text = comment_text
# handlers
def enter_button(self, widget):
"""Handle mouse enter on an asset button."""
if not hasattr(widget, "button_index") or widget.button_index < 0:
return # click on left/right arrow button gave no attr button_index
# we should detect on which button_index scroll/left/right happened to refresh shown thumbnail
bpy.context.window.cursor_set("HAND")
search_index = widget.button_index + self.scroll_offset
if search_index < self.search_results_count:
self.show_tooltip()
if self.active_index != search_index:
self.active_index = search_index
sr = search.get_search_results()
if search_index >= len(sr):
return # issue #1481 - index can be sometimes over the length of search results
asset_data = sr[search_index]
self.draw_tooltip = True
# self.tooltip = asset_data['tooltip']
ui_props = bpy.context.window_manager.blenderkitUI
ui_props.active_index = search_index # + self.scroll_offset
# Update tooltip size based on asset type
if (
asset_data["assetType"].lower() == "printable"
and self.show_photo_thumbnail
):
photo_img = ui.get_full_photo_thumbnail(asset_data)
if photo_img:
self.tooltip_image.set_image(photo_img.filepath)
self.tooltip_image.set_image_colorspace("")
else:
self.tooltip_image.set_image(
paths.get_addon_thumbnail_path("thumbnail_notready.jpg")
)
else:
set_thumb_check(self.tooltip_image, asset_data, thumb_type="thumbnail")
get_tooltip_data(asset_data)
an = asset_data["displayName"]
max_name_length = 30
if len(an) > max_name_length + 3:
an = an[:30] + "..."
search_props = utils.get_search_props()
# if in top nodegroup category, show which type the nodegroup is
if (
asset_data["assetType"] == "nodegroup"
and search_props.search_category == "nodegroup"
):
an = f"{an} - {asset_data['dictParameters']['nodeType']} nodes"
self.asset_name.text = an
self.authors_name.text = asset_data["tooltip_data"]["author_text"]
# Hide ratings for addons
is_addon = asset_data.get("assetType") == "addon"
if not is_addon:
quality_text = asset_data["tooltip_data"]["quality"]
if utils.profile_is_validator():
quality_text += f" / {int(asset_data['score'])}"
self.quality_label.text = quality_text
self.quality_label.visible = True
self.quality_star.visible = True
else:
self.quality_label.visible = False
self.quality_star.visible = False
# Update price label for addons
price_text = asset_data["tooltip_data"].get("price_text", "")
price_color = asset_data["tooltip_data"].get(
"price_color", (1.0, 0.8, 0.2, 1.0)
)
price_background = asset_data["tooltip_data"].get(
"price_background", (0.2, 0.2, 0.2, 0.0)
)
self.price_label.text = price_text
self.price_label.text_color = price_color
self.price_label.visible = bool(price_text)
self.price_label.bg_color = price_background
# preview comments for validators
self.update_comments_for_validators(asset_data)
from_newer, difference = utils.asset_from_newer_blender_version(asset_data)
if from_newer:
if difference == "major":
self.version_warning.text = f"Made in Blender {asset_data['sourceAppVersion']}! Use at your own risk."
elif difference == "minor":
self.version_warning.text = f"Made in Blender {asset_data['sourceAppVersion']}! Caution advised."
else:
self.version_warning.text = f"Made in Blender {asset_data['sourceAppVersion']}! Some features may not work."
else:
self.version_warning.text = ""
author_id = int(asset_data["author"]["id"])
author = global_vars.BKIT_AUTHORS.get(author_id)
if author is None:
bk_logger.info("\n\n\nget_tooltip_data() AUTHOR NOT FOUND", author_id)
if author is not None and author.gravatarImg:
self.gravatar_image.set_image(author.gravatarImg)
else:
img_path = paths.get_addon_thumbnail_path("thumbnail_notready.jpg")
self.gravatar_image.set_image(img_path)
self.gravatar_image.set_image_colorspace("")
properties_width = 0
for r in bpy.context.area.regions:
if r.type == "UI":
properties_width = r.width
tooltip_x = min(
int(widget.x_screen),
int(
bpy.context.region.width
- self.tooltip_panel.width
- properties_width
),
)
# Calculate space above and below the button
ui_scale = self.get_ui_scale()
full_tooltip_height = self.tooltip_panel.height
space_above = widget.y_screen
space_below = bpy.context.region.height - (widget.y_screen + widget.height)
# If space below is insufficient (would make tooltip < 70% size), position above
if (
space_below < full_tooltip_height
and space_below < full_tooltip_height * 0.7
and space_below < space_above
):
tooltip_y = int(widget.y_screen - full_tooltip_height)
else:
tooltip_y = int(widget.y_screen + widget.height)
# need to set image here because of context issues.
img_path = paths.get_addon_thumbnail_path("star_grey.png")
self.quality_star.set_image(img_path)
# set location twice for size calculations updates.
self.tooltip_panel.set_location(tooltip_x, tooltip_y)
self.update_tooltip_size(bpy.context)
self.update_tooltip_layout(bpy.context)
self.tooltip_panel.set_location(self.tooltip_panel.x, self.tooltip_panel.y)
self.tooltip_panel.layout_widgets()
# show bookmark button - always on mouse enter
if widget.bookmark_button:
widget.bookmark_button.visible = True
# bpy.ops.wm.blenderkit_asset_popup('INVOKE_DEFAULT')
def exit_button(self, widget):
"""Handle mouse exit from an asset button."""
# this condition checks if there wasn't another button already entered, which can happen with small button gaps
if self.active_index == widget.button_index + self.scroll_offset:
ui_props = bpy.context.window_manager.blenderkitUI
ui_props.draw_tooltip = False
self.draw_tooltip = False
self.hide_tooltip()
self.active_index = -1
ui_props = bpy.context.window_manager.blenderkitUI
ui_props.active_index = self.active_index
bpy.context.window.cursor_set("DEFAULT")
# hide bookmark button - only when Not bookmarked
self.update_bookmark_icon(widget.bookmark_button)
# popup asset card on mouse down
# if utils.experimental_enabled():
# h = widget.get_area_height()
# if utils.experimental_enabled() and self.mouse_y<widget.y_screen:
# self.active_index = widget.button_index + self.scroll_offset
# bpy.ops.wm.blenderkit_asset_popup('INVOKE_DEFAULT')
def bookmark_asset(self, widget):
"""Bookmark the asset linked to this button."""
# bookmark the asset linked to this button
if not utils.user_logged_in():
bpy.ops.wm.blenderkit_login_dialog(
"INVOKE_DEFAULT",
message="Please login to bookmark your favorite assets.",
)
return
sr = search.get_search_results()
asset_data = sr[widget.asset_index] # + self.scroll_offset]
bpy.ops.wm.blenderkit_bookmark_asset(asset_id=asset_data["id"])
self.update_bookmark_icon(widget)
def drag_drop_asset(self, widget):
"""Start drag and drop operation for the asset linked to this button."""
now = time.time()
# avoid double click to download assets under panels, mainly category panel
if now - ui_panels.last_time_overlay_panel_active < 0.5:
return
# start drag drop
bpy.ops.view3d.asset_drag_drop(
"INVOKE_DEFAULT",
asset_search_index=widget.search_index + self.scroll_offset,
)
def cancel_press(self, widget):
"""Handle cancel/close button press."""
self.finish()
def toggle_expand(self, widget):
"""Toggle the expanded state of the assetbar."""
user_preferences = bpy.context.preferences.addons[__package__].preferences
user_preferences.assetbar_expanded = not user_preferences.assetbar_expanded
# Update the button icon
self.update_expand_button_icon()
# Restart the asset bar to apply the new layout
self.restart_asset_bar()
def asset_menu(self, widget):
"""Open the asset menu for the asset linked to this button."""
self.hide_tooltip()
bpy.ops.wm.blenderkit_asset_popup("INVOKE_DEFAULT")
# bpy.ops.wm.call_menu(name='OBJECT_MT_blenderkit_asset_menu')
def search_more(self):
"""Search for more assets."""
history_step = search.get_active_history_step()
sro = history_step.get("search_results_orig")
if sro is None:
return
if sro.get("next") is None:
return
search_props = utils.get_search_props()
active_history_step = search.get_active_history_step()
if active_history_step.get("is_searching"):
return
search.search(get_next=True)
def update_bookmark_icon(self, bookmark_button: BL_UI_Button):
"""Update the bookmark icon for a given bookmark button."""
history_step = search.get_active_history_step()
sr = history_step.get("search_results", [])
asset_index = bookmark_button.asset_index # type: ignore
# sometimes happened here that the asset_index was out of range
if asset_index >= len(sr):
return
asset_data = sr[asset_index]
rating = ratings_utils.get_rating_local(asset_data["id"])
if rating is not None and rating.bookmarks == 1:
icon = "bookmark_full.png"
visible = True
else:
icon = "bookmark_empty.png"
if self.active_index == bookmark_button.asset_index: # type: ignore
visible = True
else:
visible = False
bookmark_button.visible = visible
img_fp = paths.get_addon_thumbnail_path(icon)
bookmark_button.set_image(img_fp)
def update_progress_bar(self, asset_button, asset_data):
"""Update the progress bar for each button in asset bar.
Enabled addons are shown in green, disabled but installed in blue."""
pb = asset_button.progress_bar
if pb is None:
return
if asset_data["downloaded"] > 0:
pb.bg_color = colors.GREEN
# For addons, always show full bar when installed, with color based on enabled status
if asset_data.get("assetType") == "addon":
w = self.button_size # Full width for installed addons
is_enabled = asset_data.get("enabled", False)
if not is_enabled:
# Pale blue for installed but disabled addons
pb.bg_color = colors.BLUE
w = int(self.button_size * asset_data["downloaded"] / 100.0)
pb.width = w
pb.update(pb.x_screen, pb.y_screen)
pb.visible = True
else:
pb.visible = False
return
if bpy.context.region is not None:
bpy.context.region.tag_redraw()
def update_validation_icon(self, asset_button, asset_data: dict):
"""Update the validation icon for each button in asset bar."""
if utils.profile_is_validator():
rating = global_vars.RATINGS.get(asset_data["id"])
v_icon = ui.verification_icons[
asset_data.get("verificationStatus", "validated")
]
if v_icon is not None:
img_fp = paths.get_addon_thumbnail_path(v_icon)
asset_button.validation_icon.set_image(img_fp)
asset_button.validation_icon.visible = True
elif rating is None or rating.quality is None:
v_icon = "star_grey.png"
img_fp = paths.get_addon_thumbnail_path(v_icon)
asset_button.validation_icon.set_image(img_fp)
asset_button.validation_icon.visible = True
else:
asset_button.validation_icon.visible = False
else:
if asset_data.get("canDownload", True) == 0:
img_fp = paths.get_addon_thumbnail_path("locked.png")
asset_button.validation_icon.set_image(img_fp)
asset_button.validation_icon.visible = True
else:
asset_button.validation_icon.visible = False
def update_image(self, asset_id):
"""should be run after thumbs are retrieved so they can be updated"""
sr = search.get_search_results()
if not sr:
return
for asset_button in self.asset_buttons:
if asset_button.asset_index < len(sr):
asset_data = sr[asset_button.asset_index]
if asset_data["assetBaseId"] == asset_id:
set_thumb_check(
asset_button, asset_data, thumb_type="thumbnail_small"
)
def update_buttons(self):
"""Update asset buttons in the asset bar based on current search results and scroll offset."""
history_step = search.get_active_history_step()
sr = history_step.get("search_results")
if not sr:
return
for asset_button in self.asset_buttons:
if asset_button.visible:
asset_button.asset_index = (
asset_button.button_index + self.scroll_offset
)
if asset_button.asset_index < len(sr):
asset_button.visible = True
asset_data = sr[asset_button.asset_index]
if asset_data is None:
continue
# show indices for debug purposes
# asset_button.text = str(asset_button.asset_index)
set_thumb_check(
asset_button, asset_data, thumb_type="thumbnail_small"
)
# asset_button.set_image(img_filepath)
self.update_validation_icon(asset_button, asset_data)
# update bookmark buttons
asset_button.bookmark_button.asset_index = asset_button.asset_index
self.update_bookmark_icon(asset_button.bookmark_button)
self.update_progress_bar(asset_button, asset_data)
if (
utils.profile_is_validator()
and asset_data["verificationStatus"] == "uploaded"
):
over_limit = utils.is_upload_old(
asset_data.get("lastBlendUpload")
)
if over_limit:
redness = min(over_limit * 0.05, 0.7)
asset_button.red_alert.bg_color = (1, 0, 0, redness)
asset_button.red_alert.visible = True
else:
asset_button.red_alert.visible = False
elif utils.profile_is_validator():
asset_button.red_alert.visible = False
else:
asset_button.visible = False
asset_button.validation_icon.visible = False
asset_button.bookmark_button.visible = False
asset_button.progress_bar.visible = False
if utils.profile_is_validator():
asset_button.red_alert.visible = False
def scroll_update(self, always=False):
"""Update scroll position and visibility of scroll buttons."""
history_step = search.get_active_history_step()
sr = history_step.get("search_results")
sro = history_step.get("search_results_orig")
# orig_offset = self.scroll_offset
# empty results
if sr is None:
self.button_scroll_down.visible = False
self.button_scroll_up.visible = False
return
self.scroll_offset = min(
self.scroll_offset, len(sr) - (self.wcount * self.hcount)
)
self.scroll_offset = max(self.scroll_offset, 0)
# only update if scroll offset actually changed, otherwise this is unnecessary
if (
sro["count"] > len(sr)
and len(sr) - self.scroll_offset < (self.wcount * self.hcount) + 15
):
self.search_more()
if self.scroll_offset == 0:
self.button_scroll_down.visible = False
else:
self.button_scroll_down.visible = True
if self.scroll_offset >= sro["count"] - (self.wcount * self.hcount):
self.button_scroll_up.visible = False
else:
self.button_scroll_up.visible = True
# here we save some time by only updating the images if the scroll offset actually changed
if self.last_scroll_offset == self.scroll_offset and not always:
return
self.last_scroll_offset = self.scroll_offset
self.update_buttons()
def search_by_author(self, asset_index):
"""Search for assets by the author of the selected asset."""
history_step = search.get_active_history_step()
sr = history_step.get("search_results", [])
asset_data = sr[asset_index]
a = asset_data["author"]["id"]
if a is not None:
sprops = utils.get_search_props()
ui_props = bpy.context.window_manager.blenderkitUI
# if there is already an author id in the search keywords, remove it first, the author_id can be any so
# use regex to find it
# for validators, set verification status to ALL
if utils.profile_is_validator():
sprops.search_verification_status = "ALL"
ui_props.search_keywords = re.sub(
r"\+author_id:\d+", "", ui_props.search_keywords
)
ui_props.search_keywords += f"+author_id:{a}"
search.search()
return True
def search_similar(self, asset_index):
"""Search for similar assets to the selected asset."""
history_step = search.get_active_history_step()
sr = history_step.get("search_results", [])
asset_data = sr[asset_index]
keywords = search.get_search_similar_keywords(asset_data)
ui_props = bpy.context.window_manager.blenderkitUI
ui_props.search_keywords = keywords
search.search()
def search_in_category(self, asset_index):
"""Search for assets in the same category as the selected asset."""
history_step = search.get_active_history_step()
sr = history_step.get("search_results", [])
asset_data = sr[asset_index]
category = asset_data.get("category")
if category is None:
return True
ui_props = bpy.context.window_manager.blenderkitUI
ui_props.search_category = category
search.search()
def handle_key_input(self, event):
"""Handle keyboard shortcuts for asset bar operations."""
# Check if enough time has passed since last popup/text input activity
# to prevent shortcuts from triggering while typing in text fields
now = time.time()
if now - ui_panels.last_time_overlay_panel_active < 0.5:
return False
# Shortcut: Toggle between normal and photo thumbnail
if event.type in {"ONE"}:
if self.show_photo_thumbnail == True:
self.show_photo_thumbnail = False
self.needs_tooltip_update = True
if event.type in {"TWO"}:
if self.show_photo_thumbnail == False:
self.show_photo_thumbnail = True
self.needs_tooltip_update = True
if (
event.type in {"LEFT_BRACKET", "RIGHT_BRACKET"}
and not event.shift
and self.active_index > -1
):
self.show_photo_thumbnail = not self.show_photo_thumbnail
self.needs_tooltip_update = True
return True
# Shortcut: Search by author
if event.type == "A":
self.search_by_author(self.active_index)
return True
# Shortcut: Delete asset from hard-drive
if event.type == "X" and self.active_index > -1:
# delete downloaded files for this asset
sr = search.get_search_results()
asset_data = sr[self.active_index]
bk_logger.info(f'deleting asset from local drive: {asset_data["name"]}')
paths.delete_asset_debug(asset_data)
asset_data["downloaded"] = 0
return True
# Shortcut: Open Author's personal Webpage
if event.type == "W" and self.active_index > -1:
sr = search.get_search_results()
asset_data = sr[self.active_index]
author_id = int(asset_data["author"]["id"])
author = global_vars.BKIT_AUTHORS.get(author_id)
if author is None:
bk_logger.warning("author is none")
return True
utils.p("author:", author)
url = author.get("aboutMeUrl")
if url is None:
bk_logger.warning("url is none")
return True
bpy.ops.wm.url_open(url=url)
return True
# Shortcut: Search Similar
if event.type == "S" and self.active_index > -1:
self.search_similar(self.active_index)
return True
if event.type == "C" and self.active_index > -1:
self.search_in_category(self.active_index)
return True
if event.type == "B" and self.active_index > -1:
sr = search.get_search_results()
asset_data = sr[self.active_index]
bpy.ops.wm.blenderkit_bookmark_asset(asset_id=asset_data["id"])
return True
# Shortcut: Open Author's profile on BlenderKit
if event.type == "P" and self.active_index > -1:
sr = search.get_search_results()
asset_data = sr[self.active_index]
author_id = int(asset_data["author"]["id"])
author = global_vars.BKIT_AUTHORS.get(author_id)
if author is None:
return True
utils.p("author:", author)
url = paths.get_author_gallery_url(author.id)
bpy.ops.wm.url_open(url=url)
return True
# FastRateMenu
if event.type == "R" and self.active_index > -1 and not event.shift:
sr = search.get_search_results()
asset_data = sr[self.active_index]
if not utils.user_is_owner(asset_data=asset_data):
bpy.ops.wm.blenderkit_menu_rating_upload(
asset_name=asset_data["name"],
asset_id=asset_data["id"],
asset_type=asset_data["assetType"],
)
return True
if (
event.type == "V"
and event.shift
and self.active_index > -1
and utils.profile_is_validator()
):
sr = search.get_search_results()
asset_data = sr[self.active_index]
bpy.ops.object.blenderkit_change_status(
asset_id=asset_data["id"], state="validated"
)
return True
if (
event.type == "H"
and event.shift
and self.active_index > -1
and utils.profile_is_validator()
):
sr = search.get_search_results()
asset_data = sr[self.active_index]
bpy.ops.object.blenderkit_change_status(
asset_id=asset_data["id"], state="on_hold"
)
return True
if (
event.type == "U"
and event.shift
and self.active_index > -1
and utils.profile_is_validator()
):
sr = search.get_search_results()
asset_data = sr[self.active_index]
bpy.ops.object.blenderkit_change_status(
asset_id=asset_data["id"], state="uploaded"
)
return True
if (
event.type == "R"
and event.shift
and self.active_index > -1
and utils.profile_is_validator()
):
sr = search.get_search_results()
asset_data = sr[self.active_index]
bpy.ops.object.blenderkit_change_status(
asset_id=asset_data["id"], state="rejected"
)
return True
return False # Let other shortcuts be handled
def scroll_up(self, widget):
"""Scroll up in the asset bar."""
self.scroll_offset += self.wcount * self.hcount
self.scroll_update()
self.enter_button(widget)
def scroll_down(self, widget):
"""Scroll down in the asset bar."""
self.scroll_offset -= self.wcount * self.hcount
self.scroll_update()
self.enter_button(widget)
@classmethod
def unregister(cls):
"""Unregister the asset bar operator and clean up instances."""
bk_logger.debug(f"unregistering class {cls}")
instances_copy = cls.instances.copy()
for instance in instances_copy:
bk_logger.debug(f"- 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}")
cls.instances.remove(instance)
def restart_asset_bar(self):
"""Restart the asset bar UI."""
ui_props = bpy.context.window_manager.blenderkitUI
self.finish()
w, a, r = utils.get_largest_area(area_type="VIEW_3D")
if a is not None:
bpy.ops.view3d.run_assetbar_fix_context(keep_running=True, do_search=False)
def add_new_tab(self, widget):
"""Add a new tab when the + button is clicked."""
tabs = global_vars.TABS["tabs"]
new_tab = {
"name": f"Tab {len(tabs) + 1}", # Default name with incremented number
"history": [], # Empty history list
"history_index": -1, # No history yet
}
tabs.append(new_tab)
# Get current history step to copy its state and results
current_history_step = search.get_active_history_step()
# Create history step for the new tab, copying the current UI state
new_history_step = search.create_history_step(new_tab)
# Copy search results from current history step if they exist
if current_history_step.get("search_results"):
new_history_step["search_results"] = current_history_step["search_results"]
new_history_step["search_results_orig"] = current_history_step[
"search_results_orig"
]
new_history_step["is_searching"] = False
# Update search results count to trigger UI refresh
self.search_results_count = len(new_history_step["search_results"])
# Force scroll update to show results
self.scroll_update(always=True)
new_active_tab = len(tabs) - 1
self.switch_to_history_step(new_active_tab, 0)
# Write history step to tab
# Restart asset bar to show new tab
self.restart_asset_bar()
def remove_tab(self, widget):
"""Remove a tab when its close button is clicked."""
tabs = global_vars.TABS["tabs"]
# Don't remove the last tab
if len(tabs) <= 1:
return
tab_index = widget.tab_index
# If removing active tab, switch to previous tab
if global_vars.TABS["active_tab"] == tab_index:
global_vars.TABS["active_tab"] = max(0, tab_index - 1)
# If removing tab before active tab, adjust active tab index
elif global_vars.TABS["active_tab"] > tab_index:
global_vars.TABS["active_tab"] -= 1
# Remove the tab
tabs.pop(tab_index)
# Restart asset bar to update UI
self.restart_asset_bar()
def switch_to_history_step(self, tab_index, history_index):
"""Switch to a specific tab and history step."""
# Update UI properties without triggering update callbacks
ui_props = bpy.context.window_manager.blenderkitUI
# lock the search
ui_props.search_lock = True
if (
tab_index == global_vars.TABS["active_tab"]
and history_index == global_vars.TABS["tabs"][tab_index]["history_index"]
):
return # Already on this tab and history step
# make original tab original background color
self.tab_buttons[global_vars.TABS["active_tab"]].bg_color = self.button_bg_color
# make also tab close button original background color
self.close_tab_buttons[global_vars.TABS["active_tab"]].bg_color = (
self.button_bg_color
)
global_vars.TABS["active_tab"] = tab_index
global_vars.TABS["tabs"][tab_index]["history_index"] = history_index
# Get active history step of the selected tab
history_step = search.get_active_history_step()
ui_state = history_step["ui_state"]
# Update UI properties
for prop_name, value in ui_state["ui_props"].items():
if hasattr(ui_props, prop_name):
# print(f"Updating UI property: {prop_name} to {value}")
# strings need to be quoted
if isinstance(value, str):
exec(f"ui_props.{prop_name} = '{value}'")
else:
exec(f"ui_props.{prop_name} = {value}")
# Update search type specific properties
search_props = utils.get_search_props()
for prop_name, value in ui_state["search_props"].items():
if hasattr(search_props, prop_name):
# strings need to be quoted
if isinstance(value, str):
exec(f"search_props.{prop_name} = '{value}'")
else:
exec(f"search_props.{prop_name} = {value}")
# update tab label
# only if the button exists
if len(self.tab_buttons) > tab_index:
search.update_tab_name(global_vars.TABS["tabs"][tab_index])
# Restore scroll position
self.scroll_offset = history_step.get("scroll_offset", 0)
# Update history button visibility
active_tab = global_vars.TABS["tabs"][tab_index]
self.history_back_button.visible = active_tab["history_index"] > 0
self.history_forward_button.visible = (
active_tab["history_index"] < len(active_tab["history"]) - 1
)
# update tab colors
for tab_button in self.tab_buttons:
c_tab_index = tab_button.tab_index
if c_tab_index == tab_index:
tab_button.bg_color = self.button_selected_color
self.close_tab_buttons[tab_index].bg_color = (
self.button_selected_color_dim
)
else:
tab_button.bg_color = self.button_bg_color
self.close_tab_buttons[c_tab_index].bg_color = self.button_bg_color
# update filters
search.update_filters()
# Update UI to show current tab's search results
self.scroll_update(always=True)
# Update tab icons to reflect the current asset type
self.update_tab_icons()
# unlock the search
ui_props.search_lock = False
def history_back(self, widget):
"""Navigate to previous history step."""
active_tab = global_vars.TABS["tabs"][global_vars.TABS["active_tab"]]
if active_tab["history_index"] > 0:
self.switch_to_history_step(
global_vars.TABS["active_tab"], active_tab["history_index"] - 1
)
def history_forward(self, widget):
"""Navigate to next history step."""
active_tab = global_vars.TABS["tabs"][global_vars.TABS["active_tab"]]
if active_tab["history_index"] < len(active_tab["history"]) - 1:
self.switch_to_history_step(
global_vars.TABS["active_tab"], active_tab["history_index"] + 1
)
def switch_tab(self, widget):
"""Switch to the clicked tab and restore its UI state."""
self.switch_to_history_step(
widget.tab_index,
global_vars.TABS["tabs"][widget.tab_index]["history_index"],
)
def handle_bkclientjs_get_asset(task: search.client_tasks.Task):
"""Handle incoming bkclientjs/get_asset task after the user asked for download in online gallery. How it goes:
1. set search in the history
2. set the results in the history step
3. open the asset bar
We handle the task in asset_bar_op because we need access to the asset_bar_operator without circular import from search.
"""
bk_logger.info(f"handle_bkclientjs_get_asset: {task.result['asset_data']['name']}")
# Get asset data from task result
asset_data = task.result.get("asset_data")
if not asset_data:
bk_logger.error("No asset data found in task")
return
# Parse the asset data
parsed_asset_data = search.parse_result(asset_data)
if not parsed_asset_data:
bk_logger.error("Failed to parse asset data")
return
search.append_history_step(
search_keywords=f"asset_base_id:{asset_data['assetBaseId']}",
search_results=[parsed_asset_data],
asset_type=asset_data.get("assetType", "").upper(),
search_results_orig={"results": [asset_data], "count": 1},
)
# If asset bar is not open, try to open it
if asset_bar_operator is None:
try:
bpy.ops.view3d.run_assetbar_fix_context(keep_running=True, do_search=False) # type: ignore[attr-defined]
except Exception as e:
bk_logger.error(f"Failed to open asset bar: {e}")
return
# Force redraw of the region if asset bar exists
if asset_bar_operator and asset_bar_operator.area:
search.load_preview(parsed_asset_data)
asset_bar_operator.update_image(parsed_asset_data["assetBaseId"])
asset_bar_operator.area.tag_redraw()
BlenderKitAssetBarOperator.modal = asset_bar_modal # type: ignore[method-assign]
BlenderKitAssetBarOperator.invoke = asset_bar_invoke # type: ignore[method-assign]
def register():
bpy.utils.register_class(BlenderKitAssetBarOperator)
def unregister():
bpy.utils.unregister_class(BlenderKitAssetBarOperator)