Asset System: New Asset Browser editor

This introduces the User Interface part of the Asset Browser, based on the
design in T54642.

Additions:
* New Asset Browser (internally a sub-editor of the File Browser).
* Navigation region showing asset categories.
* Main region showing the assets of the selected asset library with previews.
  The assets may be stored over multiple .blends in the directory that's
  "mounted" as asset library in the Preferences. They will all be shown in this
  list.
* Header with an asset library dropdown, allowing to choose the active asset
  library to show. Options are the "Current File" as asset library and all
  custom libraries.
* Display popover, filter popover and search box (partially dummies, see
  T82680).
* Sidebar showing the metadata of the currently active file (name, preview,
  description and tags), which can be edited for assets in the "Current File"
  asset library. (For others it will reset on reload.)
* The sidebar includes a button to load a custom preview image from a file.
* Make asset files draggable (with preview image).
* If a library with invalid path is selected, a message is drawn in the main
  region to help the user understand what's wrong.
* Operators to add and remove asset tags. Exposed in the sidebar.
* "Only Assets" option for Link/Append.
* Internal utilities for asset UI scripts.

For screenshots or demo videos, please see D9725. Or the 2.92 release notes.

Note that there are many things to be tweaked and polished in the Asset Browser
UI still. For example, the filter and display popovers are mostly dummies. See
T82680.

Part of the first Asset Browser milestone. Check the #asset_browser_milestone_1
project milestone on developer.blender.org.

Differential Revision: https://developer.blender.org/D9725

Reviewed by: Brecht Van Lommel, Hans Goudey
This commit is contained in:
Julian Eisel 2020-12-14 14:07:42 +01:00
parent 70474e1a7c
commit 2d6a69ae4e
14 changed files with 678 additions and 57 deletions

@ -24,6 +24,7 @@ Utility modules associated with the bpy module.
__all__ = (
"anim_utils",
"asset_utils",
"object_utils",
"io_utils",
"image_utils",

@ -0,0 +1,63 @@
# ##### 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 #####
# <pep8 compliant>
"""
Helpers for asset management tasks.
"""
import bpy
from bpy.types import (
Context,
)
__all__ = (
"SpaceAssetInfo",
)
class SpaceAssetInfo:
@classmethod
def is_asset_browser(cls, space_data: bpy.types.Space):
return space_data.type == 'FILE_BROWSER' and space_data.browse_mode == 'ASSETS'
@classmethod
def is_asset_browser_poll(cls, context: Context):
return cls.is_asset_browser(context.space_data)
@classmethod
def get_active_asset(cls, context: Context):
if hasattr(context, "active_file"):
active_file = context.active_file
return active_file.asset_data if active_file else None
class AssetBrowserPanel:
bl_space_type = 'FILE_BROWSER'
@classmethod
def poll(cls, context):
return SpaceAssetInfo.is_asset_browser_poll(context)
class AssetMetaDataPanel:
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'TOOL_PROPS'
@classmethod
def poll(cls, context):
active_file = context.active_file
return SpaceAssetInfo.is_asset_browser_poll(context) and active_file and active_file.asset_data

@ -27,6 +27,7 @@ if "bpy" in locals():
_modules = [
"add_mesh_torus",
"anim",
"assets",
"clip",
"console",
"constraint",

@ -0,0 +1,74 @@
# ##### 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 bpy
from bpy_extras.asset_utils import (
SpaceAssetInfo,
)
class ASSET_OT_tag_add(bpy.types.Operator):
"""Add a new keyword tag to the active asset"""
bl_idname = "asset.tag_add"
bl_label = "Add Asset Tag"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return SpaceAssetInfo.is_asset_browser_poll(context) and SpaceAssetInfo.get_active_asset(context)
def execute(self, context):
active_asset = SpaceAssetInfo.get_active_asset(context)
active_asset.tags.new("Unnamed Tag")
return {'FINISHED'}
class ASSET_OT_tag_remove(bpy.types.Operator):
"""Remove an existing keyword tag from the active asset"""
bl_idname = "asset.tag_remove"
bl_label = "Remove Asset Tag"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
if not SpaceAssetInfo.is_asset_browser_poll(context):
return False
active_asset = SpaceAssetInfo.get_active_asset(context)
if not active_asset:
return False
return active_asset.active_tag in range(len(active_asset.tags))
def execute(self, context):
active_asset = SpaceAssetInfo.get_active_asset(context)
tag = active_asset.tags[active_asset.active_tag]
active_asset.tags.remove(tag)
active_asset.active_tag -= 1
return {'FINISHED'}
classes = (
ASSET_OT_tag_add,
ASSET_OT_tag_remove,
)

@ -17,27 +17,79 @@
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
import bpy
from bpy.types import Header, Panel, Menu, UIList
from bpy_extras import (
asset_utils,
)
class FILEBROWSER_HT_header(Header):
bl_space_type = 'FILE_BROWSER'
def draw(self, context):
def draw_asset_browser_buttons(self, context):
layout = self.layout
st = context.space_data
space_data = context.space_data
params = space_data.params
if st.active_operator is None:
row = layout.row(align=True)
row.prop(params, "asset_library", text="")
# External libraries don't auto-refresh, add refresh button.
if params.asset_library != 'LOCAL':
row.operator("file.refresh", text="", icon="FILE_REFRESH")
layout.separator_spacer()
# Uses prop_with_popover() as popover() only adds the triangle icon in headers.
layout.prop_with_popover(
params,
"display_type",
panel="FILEBROWSER_PT_display",
text="",
icon_only=True,
)
layout.prop_with_popover(
params,
"display_type",
panel="FILEBROWSER_PT_filter",
text="",
icon='FILTER',
icon_only=True,
)
layout.prop(params, "filter_search", text="", icon='VIEWZOOM')
layout.operator(
"screen.region_toggle",
text="",
icon='PREFERENCES',
depress=is_option_region_visible(context, space_data)
).region_type = 'TOOL_PROPS'
def draw(self, context):
from bpy_extras.asset_utils import SpaceAssetInfo
layout = self.layout
space_data = context.space_data
if space_data.active_operator is None:
layout.template_header()
FILEBROWSER_MT_editor_menus.draw_collapsible(context, layout)
# can be None when save/reload with a file selector open
if SpaceAssetInfo.is_asset_browser(space_data):
layout.separator()
self.draw_asset_browser_buttons(context)
else:
layout.separator_spacer()
layout.separator_spacer()
layout.template_running_jobs()
if not context.screen.show_statusbar:
layout.template_running_jobs()
class FILEBROWSER_PT_display(Panel):
@ -144,6 +196,9 @@ class FILEBROWSER_PT_filter(Panel):
row.label(icon='BLANK1') # Indentation
sub = row.column(align=True)
sub.prop(params, "use_filter_asset_only")
filter_id = params.filter_id
for identifier in dir(filter_id):
if identifier.startswith("category_"):
@ -160,6 +215,11 @@ def panel_poll_is_upper_region(region):
return region.alignment in {'LEFT', 'RIGHT'}
def panel_poll_is_asset_browsing(context):
from bpy_extras.asset_utils import SpaceAssetInfo
return SpaceAssetInfo.is_asset_browser_poll(context)
class FILEBROWSER_UL_dir(UIList):
def draw_item(self, _context, layout, _data, item, icon, _active_data, _active_propname, _index):
direntry = item
@ -187,7 +247,7 @@ class FILEBROWSER_PT_bookmarks_volumes(Panel):
@classmethod
def poll(cls, context):
return panel_poll_is_upper_region(context.region)
return panel_poll_is_upper_region(context.region) and not panel_poll_is_asset_browsing(context)
def draw(self, context):
layout = self.layout
@ -207,7 +267,7 @@ class FILEBROWSER_PT_bookmarks_system(Panel):
@classmethod
def poll(cls, context):
return not context.preferences.filepaths.hide_system_bookmarks and panel_poll_is_upper_region(context.region)
return not context.preferences.filepaths.hide_system_bookmarks and panel_poll_is_upper_region(context.region) and not panel_poll_is_asset_browsing(context)
def draw(self, context):
layout = self.layout
@ -241,7 +301,7 @@ class FILEBROWSER_PT_bookmarks_favorites(Panel):
@classmethod
def poll(cls, context):
return panel_poll_is_upper_region(context.region)
return panel_poll_is_upper_region(context.region) and not panel_poll_is_asset_browsing(context)
def draw(self, context):
layout = self.layout
@ -278,7 +338,7 @@ class FILEBROWSER_PT_bookmarks_recents(Panel):
@classmethod
def poll(cls, context):
return not context.preferences.filepaths.hide_recent_locations and panel_poll_is_upper_region(context.region)
return not context.preferences.filepaths.hide_recent_locations and panel_poll_is_upper_region(context.region) and not panel_poll_is_asset_browsing(context)
def draw(self, context):
layout = self.layout
@ -302,7 +362,7 @@ class FILEBROWSER_PT_advanced_filter(Panel):
@classmethod
def poll(cls, context):
# only useful in append/link (library) context currently...
return context.space_data.params.use_library_browsing and panel_poll_is_upper_region(context.region)
return context.space_data.params.use_library_browsing and panel_poll_is_upper_region(context.region) and not panel_poll_is_asset_browsing(context)
def draw(self, context):
layout = self.layout
@ -314,12 +374,26 @@ class FILEBROWSER_PT_advanced_filter(Panel):
if params.use_filter_blendid:
layout.separator()
col = layout.column(align=True)
col.prop(params, "use_filter_asset_only")
filter_id = params.filter_id
for identifier in dir(filter_id):
if identifier.startswith("filter_"):
col.prop(filter_id, identifier, toggle=True)
def is_option_region_visible(context, space):
if not space.active_operator:
return False
for region in context.area.regions:
if region.type == 'TOOL_PROPS' and region.width <= 1:
return False
return True
class FILEBROWSER_PT_directory_path(Panel):
bl_space_type = 'FILE_BROWSER'
bl_region_type = 'UI'
@ -334,16 +408,6 @@ class FILEBROWSER_PT_directory_path(Panel):
return True
def is_option_region_visible(self, context, space):
if not space.active_operator:
return False
for region in context.area.regions:
if region.type == 'TOOL_PROPS' and region.width <= 1:
return False
return True
def draw(self, context):
layout = self.layout
space = context.space_data
@ -388,7 +452,7 @@ class FILEBROWSER_PT_directory_path(Panel):
"screen.region_toggle",
text="",
icon='PREFERENCES',
depress=self.is_option_region_visible(context, space)
depress=is_option_region_visible(context, space)
).region_type = 'TOOL_PROPS'
@ -482,6 +546,89 @@ class FILEBROWSER_MT_context_menu(Menu):
layout.prop_menu_enum(params, "sort_method")
class ASSETBROWSER_PT_navigation_bar(asset_utils.AssetBrowserPanel, Panel):
bl_label = "Asset Navigation"
bl_region_type = 'TOOLS'
bl_options = {'HIDE_HEADER'}
def draw(self, context):
layout = self.layout
space_file = context.space_data
col = layout.column()
col.scale_x = 1.3
col.scale_y = 1.3
col.prop(space_file.params, "asset_category", expand=True)
class ASSETBROWSER_PT_metadata(asset_utils.AssetBrowserPanel, Panel):
bl_region_type = 'TOOL_PROPS'
bl_label = "Asset Metadata"
bl_options = {'HIDE_HEADER'}
def draw(self, context):
layout = self.layout
active_file = context.active_file
active_asset = asset_utils.SpaceAssetInfo.get_active_asset(context)
layout.use_property_split = True
if not active_file or not active_asset:
layout.label(text="No asset selected.")
return
box = layout.box()
box.template_icon(icon_value=active_file.preview_icon_id, scale=5.0)
if bpy.ops.ed.lib_id_load_custom_preview.poll():
box.operator("ed.lib_id_load_custom_preview", icon='FILEBROWSER', text="Load Custom")
layout.prop(active_file, "name")
class ASSETBROWSER_PT_metadata_details(asset_utils.AssetBrowserPanel, Panel):
bl_region_type = 'TOOL_PROPS'
bl_label = "Details"
bl_parent_id = "ASSETBROWSER_PT_metadata"
def draw(self, context):
layout = self.layout
active_asset = asset_utils.SpaceAssetInfo.get_active_asset(context)
layout.use_property_split = True
if active_asset:
layout.prop(active_asset, "description")
class ASSETBROWSER_PT_metadata_tags(asset_utils.AssetMetaDataPanel, Panel):
bl_label = "Tags"
def draw(self, context):
layout = self.layout
asset_data = asset_utils.SpaceAssetInfo.get_active_asset(context)
row = layout.row()
row.template_list("ASSETBROWSER_UL_metadata_tags", "asset_tags", asset_data, "tags",
asset_data, "active_tag", rows=4)
col = row.column(align=True)
col.operator("asset.tag_add", icon='ADD', text="")
col.operator("asset.tag_remove", icon='REMOVE', text="")
class ASSETBROWSER_UL_metadata_tags(UIList):
def draw_item(self, _context, layout, _data, item, icon, _active_data, _active_propname, _index):
tag = item
row = layout.row(align=True)
# Non-editable entries would show grayed-out, which is bad in this specific case, so switch to mere label.
if tag.is_property_readonly("name"):
row.label(text=tag.name, icon_value=icon)
else:
row.prop(tag, "name", text="", emboss=False, icon_value=icon)
classes = (
FILEBROWSER_HT_header,
FILEBROWSER_PT_display,
@ -498,6 +645,11 @@ classes = (
FILEBROWSER_MT_view,
FILEBROWSER_MT_select,
FILEBROWSER_MT_context_menu,
ASSETBROWSER_PT_navigation_bar,
ASSETBROWSER_PT_metadata,
ASSETBROWSER_PT_metadata_details,
ASSETBROWSER_PT_metadata_tags,
ASSETBROWSER_UL_metadata_tags,
)
if __name__ == "__main__": # only for live edit.

@ -206,8 +206,12 @@ void UI_fontstyle_draw_ex(const uiFontStyle *fs,
BLF_disable(fs->uifont_id, font_flag);
*r_xofs = xofs;
*r_yofs = yofs;
if (r_xofs) {
*r_xofs = xofs;
}
if (r_yofs) {
*r_yofs = yofs;
}
}
void UI_fontstyle_draw(const uiFontStyle *fs,

@ -25,6 +25,7 @@
#include <math.h>
#include <string.h>
#include "BLI_alloca.h"
#include "BLI_blenlib.h"
#include "BLI_fileops_types.h"
#include "BLI_math.h"
@ -134,6 +135,7 @@ static void draw_tile(int sx, int sy, int width, int height, int colorid, int sh
}
static void file_draw_icon(uiBlock *block,
const FileDirEntry *file,
const char *path,
int sx,
int sy,
@ -157,8 +159,29 @@ static void file_draw_icon(uiBlock *block,
UI_but_func_tooltip_set(but, file_draw_tooltip_func, BLI_strdup(path));
if (drag) {
/* path is no more static, cannot give it directly to but... */
UI_but_drag_set_path(but, BLI_strdup(path), true);
/* TODO duplicated from file_draw_preview(). */
ID *id;
if ((id = filelist_file_get_id(file))) {
UI_but_drag_set_id(but, id);
}
else if (file->typeflag & FILE_TYPE_ASSET) {
ImBuf *preview_image = filelist_file_getimage(file);
char blend_path[FILE_MAX_LIBEXTRA];
if (BLO_library_path_explode(path, blend_path, NULL, NULL)) {
UI_but_drag_set_asset(but,
file->name,
BLI_strdup(blend_path),
file->blentype,
icon,
preview_image,
UI_DPI_FAC);
}
}
else {
/* path is no more static, cannot give it directly to but... */
UI_but_drag_set_path(but, BLI_strdup(path), true);
}
}
}
@ -200,6 +223,65 @@ static void file_draw_string(int sx,
});
}
/**
* \param r_sx, r_sy: The lower right corner of the last line drawn. AKA the cursor position on
* completion.
*/
static void file_draw_string_multiline(int sx,
int sy,
const char *string,
int wrap_width,
int line_height,
const uchar text_col[4],
int *r_sx,
int *r_sy)
{
rcti rect;
if (string[0] == '\0' || wrap_width < 1) {
return;
}
const uiStyle *style = UI_style_get();
int font_id = style->widgetlabel.uifont_id;
int len = strlen(string);
rctf textbox;
BLF_wordwrap(font_id, wrap_width);
BLF_enable(font_id, BLF_WORD_WRAP);
BLF_boundbox(font_id, string, len, &textbox);
BLF_disable(font_id, BLF_WORD_WRAP);
/* no text clipping needed, UI_fontstyle_draw does it but is a bit too strict
* (for buttons it works) */
rect.xmin = sx;
rect.xmax = sx + wrap_width;
/* Need to increase the clipping rect by one more line, since the #UI_fontstyle_draw_ex() will
* actually start drawing at (ymax - line-height). */
rect.ymin = sy - round_fl_to_int(BLI_rctf_size_y(&textbox)) - line_height;
rect.ymax = sy;
struct ResultBLF result;
UI_fontstyle_draw_ex(&style->widget,
&rect,
string,
text_col,
&(struct uiFontStyleDraw_Params){
.align = UI_STYLE_TEXT_LEFT,
.word_wrap = true,
},
len,
NULL,
NULL,
&result);
if (r_sx) {
*r_sx = result.width;
}
if (r_sy) {
*r_sy = rect.ymin + line_height;
}
}
void file_calc_previews(const bContext *C, ARegion *region)
{
SpaceFile *sfile = CTX_wm_space_file(C);
@ -210,6 +292,7 @@ void file_calc_previews(const bContext *C, ARegion *region)
}
static void file_draw_preview(uiBlock *block,
const FileDirEntry *file,
const char *path,
int sx,
int sy,
@ -218,7 +301,6 @@ static void file_draw_preview(uiBlock *block,
const int icon,
FileLayout *layout,
const bool is_icon,
const int typeflags,
const bool drag,
const bool dimmed,
const bool is_link)
@ -232,7 +314,7 @@ static void file_draw_preview(uiBlock *block,
float scale;
int ex, ey;
bool show_outline = !is_icon &&
(typeflags & (FILE_TYPE_IMAGE | FILE_TYPE_MOVIE | FILE_TYPE_BLENDER));
(file->typeflag & (FILE_TYPE_IMAGE | FILE_TYPE_MOVIE | FILE_TYPE_BLENDER));
BLI_assert(imb != NULL);
@ -273,14 +355,14 @@ static void file_draw_preview(uiBlock *block,
float col[4] = {1.0f, 1.0f, 1.0f, 1.0f};
if (is_icon) {
if (typeflags & FILE_TYPE_DIR) {
if (file->typeflag & FILE_TYPE_DIR) {
UI_GetThemeColor4fv(TH_ICON_FOLDER, col);
}
else {
UI_GetThemeColor4fv(TH_TEXT, col);
}
}
else if (typeflags & FILE_TYPE_FTFONT) {
else if (file->typeflag & FILE_TYPE_FTFONT) {
UI_GetThemeColor4fv(TH_TEXT, col);
}
@ -288,7 +370,7 @@ static void file_draw_preview(uiBlock *block,
col[3] *= 0.3f;
}
if (!is_icon && typeflags & FILE_TYPE_BLENDERLIB) {
if (!is_icon && file->typeflag & FILE_TYPE_BLENDERLIB) {
/* Datablock preview images use premultiplied alpha. */
GPU_blend(GPU_BLEND_ALPHA_PREMULT);
}
@ -324,7 +406,7 @@ static void file_draw_preview(uiBlock *block,
icon_color[2] = 255;
}
icon_x = xco + (ex / 2.0f) - (icon_size / 2.0f);
icon_y = yco + (ey / 2.0f) - (icon_size * ((typeflags & FILE_TYPE_DIR) ? 0.78f : 0.75f));
icon_y = yco + (ey / 2.0f) - (icon_size * ((file->typeflag & FILE_TYPE_DIR) ? 0.78f : 0.75f));
UI_icon_draw_ex(
icon_x, icon_y, icon, icon_aspect / U.dpi_fac, icon_opacity, 0.0f, icon_color, false);
}
@ -346,13 +428,13 @@ static void file_draw_preview(uiBlock *block,
/* Link to folder or non-previewed file. */
uchar icon_color[4];
UI_GetThemeColor4ubv(TH_BACK, icon_color);
icon_x = xco + ((typeflags & FILE_TYPE_DIR) ? 0.14f : 0.23f) * scaledx;
icon_y = yco + ((typeflags & FILE_TYPE_DIR) ? 0.24f : 0.14f) * scaledy;
icon_x = xco + ((file->typeflag & FILE_TYPE_DIR) ? 0.14f : 0.23f) * scaledx;
icon_y = yco + ((file->typeflag & FILE_TYPE_DIR) ? 0.24f : 0.14f) * scaledy;
UI_icon_draw_ex(
icon_x, icon_y, arrow, icon_aspect / U.dpi_fac * 1.8, 0.3f, 0.0f, icon_color, false);
}
}
else if (icon && !is_icon && !(typeflags & FILE_TYPE_FTFONT)) {
else if (icon && !is_icon && !(file->typeflag & FILE_TYPE_FTFONT)) {
/* Smaller, fainter icon at bottom-left for preview image thumbnail, but not for fonts. */
float icon_x, icon_y;
const uchar dark[4] = {0, 0, 0, 255};
@ -385,8 +467,22 @@ static void file_draw_preview(uiBlock *block,
/* dragregion */
if (drag) {
ID *id;
if ((id = filelist_file_get_id(file))) {
UI_but_drag_set_id(but, id);
}
/* path is no more static, cannot give it directly to but... */
UI_but_drag_set_image(but, BLI_strdup(path), icon, imb, scale, true);
else if (file->typeflag & FILE_TYPE_ASSET) {
char blend_path[FILE_MAX_LIBEXTRA];
if (BLO_library_path_explode(path, blend_path, NULL, NULL)) {
UI_but_drag_set_asset(
but, file->name, BLI_strdup(blend_path), file->blentype, icon, imb, scale);
}
}
else {
UI_but_drag_set_image(but, BLI_strdup(path), icon, imb, scale, true);
}
}
GPU_blend(GPU_BLEND_NONE);
@ -821,6 +917,7 @@ void file_draw_list(const bContext *C, ARegion *region)
}
file_draw_preview(block,
file,
path,
sx,
sy,
@ -829,13 +926,13 @@ void file_draw_list(const bContext *C, ARegion *region)
icon,
layout,
is_icon,
file->typeflag,
do_drag,
is_hidden,
is_link);
}
else {
file_draw_icon(block,
file,
path,
sx,
sy - layout->tile_border_y,
@ -906,3 +1003,66 @@ void file_draw_list(const bContext *C, ARegion *region)
layout->curr_size = params->thumbnail_size;
}
static void file_draw_invalid_library_hint(const SpaceFile *sfile, const ARegion *region)
{
const FileAssetSelectParams *asset_params = ED_fileselect_get_asset_params(sfile);
char library_ui_path[PATH_MAX];
file_path_to_ui_path(asset_params->base_params.dir, library_ui_path, sizeof(library_ui_path));
uchar text_col[4];
uchar text_alert_col[4];
UI_GetThemeColor4ubv(TH_TEXT, text_col);
UI_GetThemeColor4ubv(TH_REDALERT, text_alert_col);
const View2D *v2d = &region->v2d;
const int pad = sfile->layout->tile_border_x;
const int width = BLI_rctf_size_x(&v2d->tot) - (2 * pad);
const int line_height = sfile->layout->textheight;
int sx = v2d->tot.xmin + pad;
/* For some reason no padding needed. */
int sy = v2d->tot.ymax;
{
const char *message = TIP_("Library not found");
const int draw_string_str_len = strlen(message) + 2 + sizeof(library_ui_path);
char *draw_string = alloca(draw_string_str_len);
BLI_snprintf(draw_string, draw_string_str_len, "%s: %s", message, library_ui_path);
file_draw_string_multiline(sx, sy, draw_string, width, line_height, text_alert_col, NULL, &sy);
}
/* Next line, but separate it a bit further. */
sy -= line_height;
{
UI_icon_draw(sx, sy - UI_UNIT_Y, ICON_INFO);
const char *suggestion = TIP_(
"Set up the library or edit libraries in the Preferences, File Paths section.");
file_draw_string_multiline(
sx + UI_UNIT_X, sy, suggestion, width - UI_UNIT_X, line_height, text_col, NULL, NULL);
}
}
/**
* Draw a string hint if the file list is invalid.
* \return true if the list is invalid and a hint was drawn.
*/
bool file_draw_hint_if_invalid(const SpaceFile *sfile, const ARegion *region)
{
FileAssetSelectParams *asset_params = ED_fileselect_get_asset_params(sfile);
/* Only for asset browser. */
if (!ED_fileselect_is_asset_browser(sfile)) {
return false;
}
/* Check if the library exists. */
if ((asset_params->asset_library.type == FILE_ASSET_LIBRARY_LOCAL) ||
filelist_is_dir(sfile->files, asset_params->base_params.dir)) {
return false;
}
file_draw_invalid_library_hint(sfile, region);
return true;
}

@ -38,6 +38,7 @@ struct View2D;
void file_calc_previews(const bContext *C, ARegion *region);
void file_draw_list(const bContext *C, ARegion *region);
bool file_draw_hint_if_invalid(const SpaceFile *sfile, const ARegion *region);
void file_draw_check_ex(bContext *C, struct ScrArea *area);
void file_draw_check(bContext *C);
@ -117,3 +118,5 @@ void file_execute_region_panels_register(struct ARegionType *art);
/* file_utils.c */
void file_tile_boundbox(const ARegion *region, FileLayout *layout, const int file, rcti *r_bounds);
void file_path_to_ui_path(const char *path, char *r_pathi, int max_size);

@ -40,6 +40,7 @@
# include "BLI_winstuff.h"
#endif
#include "ED_asset.h"
#include "ED_fileselect.h"
#include "ED_screen.h"
#include "ED_select_utils.h"
@ -2695,6 +2696,29 @@ static bool file_delete_poll(bContext *C)
return poll;
}
static bool file_delete_single(const FileSelectParams *params,
FileDirEntry *file,
const char **r_error_message)
{
if (file->typeflag & FILE_TYPE_ASSET) {
ID *id = filelist_file_get_id(file);
if (!id) {
*r_error_message = "File is not a local data-block asset.";
return false;
}
ED_asset_clear_id(id);
}
else {
char str[FILE_MAX];
BLI_join_dirfile(str, sizeof(str), params->dir, file->relpath);
if (BLI_delete_soft(str, r_error_message) != 0 || BLI_exists(str)) {
return false;
}
}
return true;
}
static int file_delete_exec(bContext *C, wmOperator *op)
{
wmWindowManager *wm = CTX_wm_manager(C);
@ -2708,9 +2732,7 @@ static int file_delete_exec(bContext *C, wmOperator *op)
for (int i = 0; i < numfiles; i++) {
if (filelist_entry_select_index_get(sfile->files, i, CHECK_ALL)) {
FileDirEntry *file = filelist_file(sfile->files, i);
char str[FILE_MAX];
BLI_join_dirfile(str, sizeof(str), params->dir, file->relpath);
if (BLI_delete_soft(str, &error_message) != 0 || BLI_exists(str)) {
if (!file_delete_single(params, file, &error_message)) {
report_error = true;
}
}

@ -18,12 +18,14 @@
* \ingroup spfile
*/
#include "BLI_fileops.h"
#include "BLI_listbase.h"
#include "BLI_path_util.h"
#include "BLI_rect.h"
#include "BLO_readfile.h"
#include "BLI_string.h"
#include "BKE_context.h"
#include "BLO_readfile.h"
#include "ED_fileselect.h"
#include "ED_screen.h"
@ -44,3 +46,14 @@ void file_tile_boundbox(const ARegion *region, FileLayout *layout, const int fil
ymax - layout->tile_h - layout->tile_border_y,
ymax);
}
/**
* If \a path leads to a .blend, remove the trailing slash (if needed).
*/
void file_path_to_ui_path(const char *path, char *r_path, int max_size)
{
char tmp_path[PATH_MAX];
BLI_strncpy(tmp_path, path, sizeof(tmp_path));
BLI_path_slash_rstrip(tmp_path);
BLI_strncpy(r_path, BLO_has_bfile_extension(tmp_path) ? tmp_path : path, max_size);
}

@ -57,6 +57,23 @@
#include "filelist.h"
#include "fsmenu.h"
static ARegion *file_ui_region_ensure(ScrArea *area, ARegion *region_prev)
{
ARegion *region;
if ((region = BKE_area_find_region_type(area, RGN_TYPE_UI)) != NULL) {
return region;
}
region = MEM_callocN(sizeof(ARegion), "execute region for file");
BLI_insertlinkafter(&area->regionbase, region_prev, region);
region->regiontype = RGN_TYPE_UI;
region->alignment = RGN_ALIGN_TOP;
region->flag = RGN_FLAG_DYNAMIC_SIZE;
return region;
}
static ARegion *file_execute_region_ensure(ScrArea *area, ARegion *region_prev)
{
ARegion *region;
@ -223,15 +240,30 @@ static void file_ensure_valid_region_state(bContext *C,
SpaceFile *sfile,
FileSelectParams *params)
{
ARegion *region_ui = BKE_area_find_region_type(area, RGN_TYPE_UI);
ARegion *region_props = BKE_area_find_region_type(area, RGN_TYPE_TOOL_PROPS);
ARegion *region_execute = BKE_area_find_region_type(area, RGN_TYPE_EXECUTE);
ARegion *region_tools = BKE_area_find_region_type(area, RGN_TYPE_TOOLS);
bool needs_init = false; /* To avoid multiple ED_area_init() calls. */
BLI_assert(region_tools);
if (sfile->browse_mode == FILE_BROWSE_MODE_ASSETS) {
ARegion *region_execute = file_execute_region_ensure(area, region_tools);
ARegion *region_props = file_tool_props_region_ensure(area, region_execute);
/* Hide specific regions by default. */
region_props->flag |= RGN_FLAG_HIDDEN;
region_execute->flag |= RGN_FLAG_HIDDEN;
ARegion *region_ui = BKE_area_find_region_type(area, RGN_TYPE_UI);
if (region_ui) {
ED_region_remove(C, area, region_ui);
needs_init = true;
}
}
/* If there's an file-operation, ensure we have the option and execute region */
if (sfile->op && (region_props == NULL)) {
region_execute = file_execute_region_ensure(area, region_ui);
region_props = file_tool_props_region_ensure(area, region_execute);
else if (sfile->op) {
ARegion *region_ui = file_ui_region_ensure(area, region_tools);
ARegion *region_execute = file_execute_region_ensure(area, region_ui);
ARegion *region_props = file_tool_props_region_ensure(area, region_execute);
if (params->flag & FILE_HIDE_TOOL_PROPS) {
region_props->flag |= RGN_FLAG_HIDDEN;
@ -243,12 +275,19 @@ static void file_ensure_valid_region_state(bContext *C,
needs_init = true;
}
/* If there's _no_ file-operation, ensure we _don't_ have the option and execute region */
else if ((sfile->op == NULL) && (region_props != NULL)) {
BLI_assert(region_execute != NULL);
else {
ARegion *region_props = BKE_area_find_region_type(area, RGN_TYPE_TOOL_PROPS);
ARegion *region_execute = BKE_area_find_region_type(area, RGN_TYPE_EXECUTE);
ARegion *region_ui = file_ui_region_ensure(area, region_tools);
UNUSED_VARS(region_ui);
ED_region_remove(C, area, region_props);
ED_region_remove(C, area, region_execute);
needs_init = true;
if (region_props) {
BLI_assert(region_execute);
ED_region_remove(C, area, region_props);
ED_region_remove(C, area, region_execute);
needs_init = true;
}
}
if (needs_init) {
@ -530,7 +569,9 @@ static void file_main_region_draw(const bContext *C, ARegion *region)
file_highlight_set(sfile, region, event->x, event->y);
}
file_draw_list(C, region);
if (!file_draw_hint_if_invalid(sfile, region)) {
file_draw_list(C, region);
}
/* reset view matrix */
UI_view2d_view_restore(C);
@ -714,6 +755,25 @@ static void file_dropboxes(void)
WM_dropbox_add(lb, "FILE_OT_filepath_drop", filepath_drop_poll, filepath_drop_copy);
}
static int file_space_subtype_get(ScrArea *area)
{
SpaceFile *sfile = area->spacedata.first;
return sfile->browse_mode;
}
static void file_space_subtype_set(ScrArea *area, int value)
{
SpaceFile *sfile = area->spacedata.first;
sfile->browse_mode = value;
}
static void file_space_subtype_item_extend(bContext *UNUSED(C),
EnumPropertyItem **item,
int *totitem)
{
RNA_enum_items_add(item, totitem, rna_enum_space_file_browse_mode_items);
}
const char *file_context_dir[] = {"active_file", "active_id", NULL};
static int /*eContextResult*/ file_context(const bContext *C,
@ -779,6 +839,9 @@ void ED_spacetype_file(void)
st->operatortypes = file_operatortypes;
st->keymap = file_keymap;
st->dropboxes = file_dropboxes;
st->space_subtype_item_extend = file_space_subtype_item_extend;
st->space_subtype_get = file_space_subtype_get;
st->space_subtype_set = file_space_subtype_set;
st->context = file_context;
st->id_remap = file_id_remap;

@ -270,6 +270,8 @@ static void rna_Area_ui_type_update(bContext *C, PointerRNA *ptr)
st->space_subtype_set(area, area->butspacetype_subtype);
}
area->butspacetype_subtype = 0;
ED_area_tag_refresh(area);
}
static void rna_View2D_region_to_view(struct View2D *v2d, float x, float y, float result[2])

@ -2574,6 +2574,18 @@ static const EnumPropertyItem *rna_FileAssetSelectParams_asset_library_itemf(
return item;
}
static void rna_FileAssetSelectParams_asset_category_set(PointerRNA *ptr, uint64_t value)
{
FileSelectParams *params = ptr->data;
params->filter_id = value;
}
static uint64_t rna_FileAssetSelectParams_asset_category_get(PointerRNA *ptr)
{
FileSelectParams *params = ptr->data;
return params->filter_id;
}
static void rna_FileBrowser_FileSelectEntry_name_get(PointerRNA *ptr, char *value)
{
const FileDirEntry *entry = ptr->data;
@ -6207,6 +6219,47 @@ static void rna_def_fileselect_asset_params(BlenderRNA *brna)
StructRNA *srna;
PropertyRNA *prop;
/* XXX copied from rna_def_fileselect_idfilter. */
static const EnumPropertyItem asset_category_items[] = {
{FILTER_ID_SCE, "SCENES", ICON_SCENE_DATA, "Scenes", "Show scenes"},
{FILTER_ID_AC, "ANIMATIONS", ICON_ANIM_DATA, "Animations", "Show animation data"},
{FILTER_ID_OB | FILTER_ID_GR,
"OBJECTS_AND_COLLECTIONS",
ICON_GROUP,
"Objects & Collections",
"Show objects and collections"},
{FILTER_ID_AR | FILTER_ID_CU | FILTER_ID_LT | FILTER_ID_MB | FILTER_ID_ME
/* XXX avoid warning */
// | FILTER_ID_HA | FILTER_ID_PT | FILTER_ID_VO
,
"GEOMETRY",
ICON_MESH_DATA,
"Geometry",
"Show meshes, curves, lattice, armatures and metaballs data"},
{FILTER_ID_LS | FILTER_ID_MA | FILTER_ID_NT | FILTER_ID_TE,
"SHADING",
ICON_MATERIAL_DATA,
"Shading",
"Show materials, nodetrees, textures and Freestyle's linestyles"},
{FILTER_ID_IM | FILTER_ID_MC | FILTER_ID_MSK | FILTER_ID_SO,
"IMAGES_AND_SOUNDS",
ICON_IMAGE_DATA,
"Images & Sounds",
"Show images, movie clips, sounds and masks"},
{FILTER_ID_CA | FILTER_ID_LA | FILTER_ID_LP | FILTER_ID_SPK | FILTER_ID_WO | FILTER_ID_WS,
"ENVIRONMENTS",
ICON_WORLD_DATA,
"Environment",
"Show worlds, lights, cameras and speakers"},
{FILTER_ID_BR | FILTER_ID_GD | FILTER_ID_PA | FILTER_ID_PAL | FILTER_ID_PC | FILTER_ID_TXT |
FILTER_ID_VF | FILTER_ID_CF,
"MISC",
ICON_GREASEPENCIL,
"Miscellaneous",
"Show other data types"},
{0, NULL, 0, NULL, NULL},
};
srna = RNA_def_struct(brna, "FileAssetSelectParams", "FileSelectParams");
RNA_def_struct_ui_text(
srna, "Asset Select Parameters", "Settings for the file selection in Asset Browser mode");
@ -6219,6 +6272,15 @@ static void rna_def_fileselect_asset_params(BlenderRNA *brna)
"rna_FileAssetSelectParams_asset_library_itemf");
RNA_def_property_ui_text(prop, "Asset Library", "");
RNA_def_property_update(prop, NC_SPACE | ND_SPACE_FILE_PARAMS, NULL);
prop = RNA_def_property(srna, "asset_category", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_items(prop, asset_category_items);
RNA_def_property_enum_funcs(prop,
"rna_FileAssetSelectParams_asset_category_get",
"rna_FileAssetSelectParams_asset_category_set",
NULL);
RNA_def_property_ui_text(prop, "Asset Category", "Determine which kind of assets to display");
RNA_def_property_update(prop, NC_SPACE | ND_SPACE_FILE_LIST, NULL);
}
static void rna_def_filemenu_entry(BlenderRNA *brna)

@ -5993,8 +5993,9 @@ static void rna_def_userdef_filepaths_asset_library(BlenderRNA *brna)
RNA_def_struct_name_property(srna, prop);
RNA_def_property_update(prop, 0, "rna_userdef_update");
prop = RNA_def_property(srna, "path", PROP_STRING, PROP_FILEPATH);
RNA_def_property_ui_text(prop, "Path", "Path to a .blend file to use as an asset library");
prop = RNA_def_property(srna, "path", PROP_STRING, PROP_DIRPATH);
RNA_def_property_ui_text(
prop, "Path", "Path to a directory with .blend files to use as an asset library");
RNA_def_property_update(prop, 0, "rna_userdef_update");
}