2606 lines
100 KiB
Python
2606 lines
100 KiB
Python
# ##### BEGIN GPL LICENSE BLOCK #####
|
||
#
|
||
# This program is free software; you can redistribute it and/or
|
||
# modify it under the terms of the GNU General Public License
|
||
# as published by the Free Software Foundation; either version 2
|
||
# of the License, or (at your option) any later version.
|
||
#
|
||
# This program is distributed in the hope that it will be useful,
|
||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# GNU General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU General Public License
|
||
# along with this program; if not, write to the Free Software Foundation,
|
||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
#
|
||
# ##### END GPL LICENSE BLOCK #####
|
||
|
||
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)
|