mirror of
https://github.com/blender/blender
synced 2026-09-29 04:37:17 +03:00
UI: I/O: Match format labels with Import menu
Part of https://projects.blender.org/blender/blender/issues/162410 Add extensions dynamically to format labels rather than having the extension list hardcoded and duplicated. Extensions are added to the label dynamically based off the FileHandler bl_file_extensions field which is the source of truth now. This allows the extension list to be included for the Import and Export menu, and relevant add menus, but not the UI List for collection export. Details: - Several extensions: replace ; by / (.glb;.gltf -> .glb/.gltf). - Two or more extensions starting with the same first three letters: concatenate with as many common letters as possible, and add * (.usd;.usda;.usdc;.usdz -> .usd*). Pull Request: https://projects.blender.org/blender/blender/pulls/163438
This commit is contained in:
parent
3423e8fed9
commit
ebbd2ab38d
9 changed files with 170 additions and 19 deletions
|
|
@ -84,7 +84,9 @@ class IO_FH_svg_as_curves(bpy.types.FileHandler):
|
|||
|
||||
|
||||
def menu_func_import(self, context):
|
||||
self.layout.operator(ImportSVG.bl_idname, text="SVG as Curves (.svg)")
|
||||
self.layout.operator(
|
||||
ImportSVG.bl_idname,
|
||||
text=bpy.types.FileHandler.label_with_extensions("IO_FH_svg_as_curves"))
|
||||
|
||||
|
||||
classes = [
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ if "bpy" in locals():
|
|||
|
||||
|
||||
import bpy
|
||||
from bpy.app.translations import pgettext_iface as iface_
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
|
|
@ -713,11 +714,14 @@ def export_panel_animation(layout, operator):
|
|||
|
||||
|
||||
def menu_func_import(self, context):
|
||||
self.layout.operator(ImportFBX.bl_idname, text="FBX (.fbx) (Legacy)")
|
||||
self.layout.operator(
|
||||
ImportFBX.bl_idname,
|
||||
text=bpy.types.FileHandler.label_with_extensions("IO_FH_fbx") + " " + iface_("(Legacy)"))
|
||||
|
||||
|
||||
def menu_func_export(self, context):
|
||||
self.layout.operator(ExportFBX.bl_idname, text="FBX (.fbx)")
|
||||
self.layout.operator(
|
||||
ExportFBX.bl_idname, text=bpy.types.FileHandler.label_with_extensions("IO_FH_fbx"))
|
||||
|
||||
|
||||
classes = (
|
||||
|
|
|
|||
|
|
@ -1908,7 +1908,8 @@ class ExportGLTF2(bpy.types.Operator, ExportGLTF2_Base, ExportHelper):
|
|||
|
||||
|
||||
def menu_func_export(self, context):
|
||||
self.layout.operator(ExportGLTF2.bl_idname, text='glTF 2.0 (.glb/.gltf)')
|
||||
self.layout.operator(
|
||||
ExportGLTF2.bl_idname, text=bpy.types.FileHandler.label_with_extensions('IO_FH_gltf2'))
|
||||
|
||||
|
||||
class ImportGLTF2(Operator, ConvertGLTF2_Base, ImportHelper):
|
||||
|
|
@ -2277,7 +2278,7 @@ class IO_FH_gltf2(bpy.types.FileHandler):
|
|||
|
||||
|
||||
def menu_func_import(self, context):
|
||||
self.layout.operator(ImportGLTF2.bl_idname, text='glTF 2.0 (.glb/.gltf)')
|
||||
self.layout.operator(ImportGLTF2.bl_idname, text=bpy.types.FileHandler.label_with_extensions("IO_FH_gltf2"))
|
||||
|
||||
|
||||
classes = (
|
||||
|
|
|
|||
|
|
@ -384,24 +384,32 @@ class TOPBAR_MT_file_import(Menu):
|
|||
bl_owner_use_filter = False
|
||||
|
||||
def draw(self, _context):
|
||||
FileHandler = bpy.types.FileHandler
|
||||
if bpy.app.build_options.alembic:
|
||||
self.layout.operator("wm.alembic_import", text="Alembic (.abc)")
|
||||
self.layout.operator(
|
||||
"wm.alembic_import", text=FileHandler.label_with_extensions("IO_FH_alembic"))
|
||||
if bpy.app.build_options.usd:
|
||||
self.layout.operator(
|
||||
"wm.usd_import", text="Universal Scene Description (.usd*)")
|
||||
"wm.usd_import", text=FileHandler.label_with_extensions("IO_FH_usd"))
|
||||
|
||||
if bpy.app.build_options.io_gpencil:
|
||||
self.layout.operator("wm.grease_pencil_import_svg", text="SVG as Grease Pencil")
|
||||
self.layout.operator(
|
||||
"wm.grease_pencil_import_svg",
|
||||
text=FileHandler.label_with_extensions("IO_FH_grease_pencil_svg"))
|
||||
|
||||
if bpy.app.build_options.io_wavefront_obj:
|
||||
self.layout.operator("wm.obj_import", text="Wavefront (.obj)")
|
||||
self.layout.operator(
|
||||
"wm.obj_import", text=FileHandler.label_with_extensions("IO_FH_obj"))
|
||||
if bpy.app.build_options.io_ply:
|
||||
self.layout.operator("wm.ply_import", text="Stanford PLY (.ply)")
|
||||
self.layout.operator(
|
||||
"wm.ply_import", text=FileHandler.label_with_extensions("IO_FH_ply"))
|
||||
if bpy.app.build_options.io_stl:
|
||||
self.layout.operator("wm.stl_import", text="STL (.stl)")
|
||||
self.layout.operator(
|
||||
"wm.stl_import", text=FileHandler.label_with_extensions("IO_FH_stl"))
|
||||
|
||||
if bpy.app.build_options.io_fbx:
|
||||
self.layout.operator("wm.fbx_import", text="FBX (.fbx)")
|
||||
self.layout.operator(
|
||||
"wm.fbx_import", text=FileHandler.label_with_extensions("IO_FH_fbx"))
|
||||
|
||||
|
||||
class TOPBAR_MT_file_export(Menu):
|
||||
|
|
@ -410,6 +418,8 @@ class TOPBAR_MT_file_export(Menu):
|
|||
bl_owner_use_filter = False
|
||||
|
||||
def draw(self, context):
|
||||
FileHandler = bpy.types.FileHandler
|
||||
|
||||
row = self.layout.row()
|
||||
row.operator("wm.collection_export_all")
|
||||
row.enabled = context.view_layer.has_export_collections
|
||||
|
|
@ -417,10 +427,11 @@ class TOPBAR_MT_file_export(Menu):
|
|||
self.layout.separator()
|
||||
|
||||
if bpy.app.build_options.alembic:
|
||||
self.layout.operator("wm.alembic_export", text="Alembic (.abc)")
|
||||
self.layout.operator(
|
||||
"wm.alembic_export", text=FileHandler.label_with_extensions("IO_FH_alembic"))
|
||||
if bpy.app.build_options.usd:
|
||||
self.layout.operator(
|
||||
"wm.usd_export", text="Universal Scene Description (.usd*)")
|
||||
"wm.usd_export", text=FileHandler.label_with_extensions("IO_FH_usd"))
|
||||
|
||||
if bpy.app.build_options.io_gpencil:
|
||||
# PUGIXML library dependency.
|
||||
|
|
@ -431,11 +442,14 @@ class TOPBAR_MT_file_export(Menu):
|
|||
self.layout.operator("wm.grease_pencil_export_pdf", text="Grease Pencil as PDF")
|
||||
|
||||
if bpy.app.build_options.io_wavefront_obj:
|
||||
self.layout.operator("wm.obj_export", text="Wavefront (.obj)")
|
||||
self.layout.operator(
|
||||
"wm.obj_export", text=FileHandler.label_with_extensions("IO_FH_obj"))
|
||||
if bpy.app.build_options.io_ply:
|
||||
self.layout.operator("wm.ply_export", text="Stanford PLY (.ply)")
|
||||
self.layout.operator(
|
||||
"wm.ply_export", text=FileHandler.label_with_extensions("IO_FH_ply"))
|
||||
if bpy.app.build_options.io_stl:
|
||||
self.layout.operator("wm.stl_export", text="STL (.stl)")
|
||||
self.layout.operator(
|
||||
"wm.stl_export", text=FileHandler.label_with_extensions("IO_FH_stl"))
|
||||
|
||||
|
||||
class TOPBAR_MT_file_external_data(Menu):
|
||||
|
|
|
|||
|
|
@ -55,6 +55,11 @@ struct FileHandlerType {
|
|||
* Generate a default file name for use with this file handler.
|
||||
*/
|
||||
std::string get_default_filename(StringRefNull name);
|
||||
|
||||
/**
|
||||
* Return `label` with the supported file extensions appended, e.g. "Alembic (.abc)".
|
||||
*/
|
||||
std::string label_with_extensions() const;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -144,4 +144,61 @@ std::string FileHandlerType::get_default_filename(const StringRefNull name)
|
|||
return filename;
|
||||
}
|
||||
|
||||
static std::string extensions_common_prefix(const Span<std::string> extensions)
|
||||
{
|
||||
std::string prefix = extensions.first();
|
||||
for (const std::string &extension : extensions.drop_front(1)) {
|
||||
const int64_t max_len = std::min<int64_t>(prefix.size(), extension.size());
|
||||
int64_t i = 0;
|
||||
while (i < max_len && prefix[i] == extension[i]) {
|
||||
i++;
|
||||
}
|
||||
prefix.resize(i);
|
||||
}
|
||||
return prefix;
|
||||
}
|
||||
|
||||
static bool extensions_group_is_collapsible(const Span<std::string> extensions)
|
||||
{
|
||||
const std::string prefix = extensions_common_prefix(extensions);
|
||||
const int64_t prefix_letters = prefix.size() - (!prefix.empty() && prefix[0] == '.' ? 1 : 0);
|
||||
return prefix_letters >= 3 && std::ranges::all_of(extensions, [&](const auto &extension) {
|
||||
return extension.size() <= prefix.size() + 1;
|
||||
});
|
||||
}
|
||||
|
||||
std::string FileHandlerType::label_with_extensions() const
|
||||
{
|
||||
if (file_extensions.is_empty()) {
|
||||
return label;
|
||||
}
|
||||
|
||||
std::string extensions;
|
||||
const Span<std::string> all_extensions = file_extensions.as_span();
|
||||
int64_t group_start = 0;
|
||||
while (group_start < all_extensions.size()) {
|
||||
int64_t group_size = 1;
|
||||
while (group_start + group_size < all_extensions.size() &&
|
||||
extensions_group_is_collapsible(all_extensions.slice(group_start, group_size + 1)))
|
||||
{
|
||||
group_size++;
|
||||
}
|
||||
|
||||
const Span<std::string> group = all_extensions.slice(group_start, group_size);
|
||||
if (!extensions.empty()) {
|
||||
extensions += "/";
|
||||
}
|
||||
if (group.size() > 1) {
|
||||
extensions += extensions_common_prefix(group) + "*";
|
||||
}
|
||||
else {
|
||||
extensions += group.first();
|
||||
}
|
||||
|
||||
group_start += group_size;
|
||||
}
|
||||
|
||||
return std::string(label) + " (" + extensions + ")";
|
||||
}
|
||||
|
||||
} // namespace blender::bke
|
||||
|
|
|
|||
|
|
@ -116,4 +116,51 @@ TEST_F(FileHandlerTest, remove)
|
|||
EXPECT_EQ(file_handlers()[4].get(), test_file_handlers[5]);
|
||||
EXPECT_EQ(file_handlers()[5].get(), test_file_handlers[6]);
|
||||
}
|
||||
|
||||
static void expect_label_with_extensions(const char *label,
|
||||
Vector<std::string> file_extensions,
|
||||
const std::string &expected)
|
||||
{
|
||||
FileHandlerType file_handler{};
|
||||
STRNCPY(file_handler.label, label);
|
||||
file_handler.file_extensions = std::move(file_extensions);
|
||||
EXPECT_EQ(file_handler.label_with_extensions(), expected);
|
||||
}
|
||||
|
||||
TEST_F(FileHandlerTest, label_with_extensions)
|
||||
{
|
||||
/* No extensions: label is returned unchanged. */
|
||||
expect_label_with_extensions("Wavefront OBJ", {}, "Wavefront OBJ");
|
||||
|
||||
/* Single extension. */
|
||||
expect_label_with_extensions("Wavefront OBJ", {".obj"}, "Wavefront OBJ (.obj)");
|
||||
|
||||
/* No common prefix: joined with '/'. */
|
||||
expect_label_with_extensions("Collada", {".dae", ".zae"}, "Collada (.dae/.zae)");
|
||||
|
||||
/* Common prefix ".ab" has only 2 letters excluding the leading dot: below the 3-letter
|
||||
* threshold required to collapse. */
|
||||
expect_label_with_extensions("Prefix Too Short", {".ab", ".abc"}, "Prefix Too Short (.ab/.abc)");
|
||||
|
||||
/* Common prefix ".ble" has enough letters to collapse, but ".blend" is longer than
|
||||
* `prefix.size() + 1`, so the extensions are not collapsible. */
|
||||
expect_label_with_extensions(
|
||||
"Extension Too Long", {".ble", ".blend"}, "Extension Too Long (.ble/.blend)");
|
||||
|
||||
/* Common prefix has enough letters and no extension exceeds the length limit:
|
||||
* collapses to `<prefix>*`. */
|
||||
expect_label_with_extensions("Universal Scene Description",
|
||||
{".usd", ".usda", ".usdc"},
|
||||
"Universal Scene Description (.usd*)");
|
||||
|
||||
/* Common prefix ".abc" has exactly 3 letters excluding the leading dot: the minimum
|
||||
* required to collapse. */
|
||||
expect_label_with_extensions(
|
||||
"Prefix Length Boundary", {".abc", ".abcd"}, "Prefix Length Boundary (.abc*)");
|
||||
|
||||
/* ".foo" and ".fooz" form a collapsible group, ".bar" does not share a prefix with
|
||||
* either and forms its own group. */
|
||||
expect_label_with_extensions(
|
||||
"Multiple Groups", {".foo", ".fooz", ".bar"}, "Multiple Groups (.foo*/.bar)");
|
||||
}
|
||||
} // namespace blender::bke::tests
|
||||
|
|
|
|||
|
|
@ -1159,7 +1159,8 @@ static void collection_importer_menu_draw(const bContext * /*C*/, Menu *menu)
|
|||
bool at_least_one = false;
|
||||
for (const auto &fh : bke::file_handlers()) {
|
||||
if (STREQ(fh->idname, "IO_FH_usd") && WM_operatortype_find(fh->import_operator, true)) {
|
||||
PointerRNA op_ptr = layout.op("COLLECTION_OT_importer_add", fh->label, ICON_NONE);
|
||||
PointerRNA op_ptr = layout.op(
|
||||
"COLLECTION_OT_importer_add", fh->label_with_extensions(), ICON_NONE);
|
||||
RNA_string_set(&op_ptr, "name", fh->idname);
|
||||
at_least_one = true;
|
||||
}
|
||||
|
|
@ -1178,7 +1179,8 @@ static void collection_exporter_menu_draw(const bContext * /*C*/, Menu *menu)
|
|||
bool at_least_one = false;
|
||||
for (const auto &fh : bke::file_handlers()) {
|
||||
if (WM_operatortype_find(fh->export_operator, true)) {
|
||||
PointerRNA op_ptr = layout.op("COLLECTION_OT_exporter_add", fh->label, ICON_NONE);
|
||||
PointerRNA op_ptr = layout.op(
|
||||
"COLLECTION_OT_exporter_add", fh->label_with_extensions(), ICON_NONE);
|
||||
RNA_string_set(&op_ptr, "name", fh->idname);
|
||||
at_least_one = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1679,6 +1679,14 @@ static StructRNA *rna_FileHandler_refine(PointerRNA *file_handler_ptr)
|
|||
RNA_FileHandler;
|
||||
}
|
||||
|
||||
static void rna_FileHandler_label_with_extensions(const char *idname, char *result)
|
||||
{
|
||||
const bke::FileHandlerType *file_handler = bke::file_handler_find(idname);
|
||||
BLI_strncpy(result,
|
||||
file_handler ? file_handler->label_with_extensions().c_str() : "",
|
||||
FH_MAX_FILE_EXTENSIONS_STR + OP_MAX_TYPENAME + 3);
|
||||
}
|
||||
|
||||
} // namespace blender
|
||||
|
||||
#else /* RNA_RUNTIME */
|
||||
|
|
@ -2612,6 +2620,17 @@ static void rna_def_file_handler(BlenderRNA *brna)
|
|||
RNA_def_function_return(func, RNA_def_boolean(func, "is_usable", false, "", ""));
|
||||
parm = RNA_def_pointer(func, "context", "Context", "", "The context");
|
||||
RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED);
|
||||
|
||||
func = RNA_def_function(srna, "label_with_extensions", "rna_FileHandler_label_with_extensions");
|
||||
RNA_def_function_ui_description(
|
||||
func, "Return the label of the file handler with the given ID, with its file extensions");
|
||||
RNA_def_function_flag(func, FUNC_NO_SELF);
|
||||
parm = RNA_def_string(func, "idname", nullptr, sizeof(bke::FileHandlerType::idname), "", "");
|
||||
RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED);
|
||||
parm = RNA_def_string(
|
||||
func, "result", nullptr, FH_MAX_FILE_EXTENSIONS_STR + OP_MAX_TYPENAME + 3, "result", "");
|
||||
RNA_def_parameter_flags(parm, PROP_THICK_WRAP, ParameterFlag(0));
|
||||
RNA_def_function_output(func, parm);
|
||||
}
|
||||
|
||||
static void rna_def_layout_panel_state(BlenderRNA *brna)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue