diff --git a/scripts/startup/bl_ui/space_outliner.py b/scripts/startup/bl_ui/space_outliner.py index 963726616cd..0b579aa62a9 100644 --- a/scripts/startup/bl_ui/space_outliner.py +++ b/scripts/startup/bl_ui/space_outliner.py @@ -10,6 +10,15 @@ from bpy.app.translations import ( ) +def has_selected_ids_in_context(context): + if hasattr(context, "id"): + return True + if len(context.selected_ids) > 0: + return True + + return False + + class OUTLINER_HT_header(Header): bl_space_type = 'OUTLINER' @@ -157,6 +166,26 @@ class OUTLINER_MT_view_pie(Menu): pie.operator("outliner.show_active", icon='ZOOM_SELECTED') +class OUTLINER_MT_id_data(Menu): + bl_label = "ID Data" + + @classmethod + def poll(cls, context): + return has_selected_ids_in_context(context) + + def draw(self, context): + layout = self.layout + + space = context.space_data + + layout.operator_enum("outliner.id_operation", "type") + + id_linked = getattr(context, "id", None) + if id_linked and id_linked.library: + layout.separator() + layout.operator("outliner.id_linked_relocate", text="Relocate") + + class OUTLINER_MT_edit_datablocks(Menu): bl_label = "Edit" @@ -261,7 +290,7 @@ class OUTLINER_MT_collection(Menu): layout.separator() - layout.operator_menu_enum("outliner.id_operation", "type", text="ID Data") + layout.menu("OUTLINER_MT_id_data") layout.separator() @@ -318,22 +347,13 @@ class OUTLINER_MT_object(Menu): layout.separator() - layout.operator_menu_enum("outliner.id_operation", "type", text="ID Data") + layout.menu("OUTLINER_MT_id_data") layout.separator() OUTLINER_MT_context_menu.draw_common_operators(layout) -def has_selected_ids_in_context(context): - if hasattr(context, "id"): - return True - if len(context.selected_ids) > 0: - return True - - return False - - class OUTLINER_MT_asset(Menu): bl_label = "Assets" @@ -525,6 +545,7 @@ classes = ( OUTLINER_MT_collection_new, OUTLINER_MT_collection_visibility, OUTLINER_MT_collection_view_layer, + OUTLINER_MT_id_data, OUTLINER_MT_object, OUTLINER_MT_asset, OUTLINER_MT_liboverride, diff --git a/source/blender/blenkernel/BKE_blendfile_link_append.hh b/source/blender/blenkernel/BKE_blendfile_link_append.hh index 4e6006e43a7..dd91c0eedcd 100644 --- a/source/blender/blenkernel/BKE_blendfile_link_append.hh +++ b/source/blender/blenkernel/BKE_blendfile_link_append.hh @@ -410,3 +410,11 @@ void BKE_blendfile_library_relocate(BlendfileLinkAppendContext *lapp_context, ReportList *reports, Library *library, bool do_reload); + +/** + * Relocate a single linked ID. + * + * NOTE: content of `lapp_context` after execution of that function should not be assumed valid + * anymore, and should immediately be freed. + */ +void BKE_blendfile_id_relocate(BlendfileLinkAppendContext &lapp_context, ReportList *reports); diff --git a/source/blender/blenkernel/intern/blendfile_link_append.cc b/source/blender/blenkernel/intern/blendfile_link_append.cc index 1f1414d3329..2ce22019e6b 100644 --- a/source/blender/blenkernel/intern/blendfile_link_append.cc +++ b/source/blender/blenkernel/intern/blendfile_link_append.cc @@ -30,6 +30,7 @@ #include "BLI_linklist.h" #include "BLI_listbase.h" #include "BLI_math_vector.h" +#include "BLI_set.hh" #include "BLI_string.h" #include "BLI_string_ref.hh" #include "BLI_utildefines.h" @@ -1686,12 +1687,12 @@ void BKE_blendfile_override(BlendfileLinkAppendContext *lapp_context, /** \name Library relocating code. * \{ */ -static void blendfile_library_relocate_remap(Main *bmain, - ID *old_id, - ID *new_id, - ReportList *reports, - const bool do_reload, - const int remap_flags) +static void blendfile_library_relocate_id_remap_do(Main *bmain, + ID *old_id, + ID *new_id, + ReportList *reports, + const bool do_reload, + const int remap_flags) { BLI_assert(old_id); if (do_reload) { @@ -1772,6 +1773,163 @@ static void blendfile_library_relocate_remap(Main *bmain, } } +static void blendfile_library_relocate_id_remap(Main *bmain, + ID *old_id, + ID *new_id, + ReportList *reports, + const bool do_reload, + const int remap_flags) +{ + blendfile_library_relocate_id_remap_do(bmain, old_id, new_id, reports, do_reload, remap_flags); + if (new_id == nullptr) { + return; + } + /* Usual special code for ShapeKeys snowflakes... */ + Key **old_key_p = BKE_key_from_id_p(old_id); + if (old_key_p == nullptr) { + return; + } + Key *old_key = *old_key_p; + Key *new_key = BKE_key_from_id(new_id); + if (old_key != nullptr) { + *old_key_p = nullptr; + id_us_min(&old_key->id); + blendfile_library_relocate_id_remap_do( + bmain, &old_key->id, &new_key->id, reports, do_reload, remap_flags); + *old_key_p = old_key; + id_us_plus_no_lib(&old_key->id); + } +} + +/** Delete now unused linked IDs and libraries. */ +static void blendfile_relocate_postprocess_cleanup(BlendfileLinkAppendContext &lapp_context) +{ + Main &bmain = *lapp_context.params->bmain; + + blender::Set ids_to_delete = {}; + ID *id_iter; + + /* Delete all no more used old IDs. */ + /* NOTE: While this looping over until we are sure we deleted everything is very far from + * efficient, doing otherwise would require a much more complex handling of indirectly linked IDs + * in steps above. Currently, in case of relocation, those are skipped in remapping phase, though + * in some cases (essentially internal links between IDs from the same library) remapping should + * happen. But getting this to work reliably would be very difficult, so since this is not a + * performance-critical code, better to go with the (relatively) simpler, brute-force approach + * here in 'removal of old IDs' step. */ + bool keep_looping = true; + while (keep_looping) { + keep_looping = false; + + for (BlendfileLinkAppendContextItem &item : lapp_context.items) { + ID *old_id = static_cast(item.userdata); + + if (old_id == nullptr) { + continue; + } + + if (GS(old_id->name) == ID_KE) { + /* Shape Keys are handled as part of their owning obdata (see below). This implies that + * there is no way to know when the old pointer gets invalid, so just clear it immediately. + */ + item.userdata = nullptr; + continue; + } + + /* In case the active scene was reloaded, the context pointers in + * `lapp_context->params->context` need to be updated before the old Scene ID is freed. */ + if (old_id == &lapp_context.params->context.scene->id) { + BLI_assert(GS(old_id->name) == ID_SCE); + Scene *new_scene = reinterpret_cast(item.new_id); + BLI_assert(new_scene != nullptr); + lapp_context.params->context.scene = new_scene; + if (lapp_context.params->context.view_layer != nullptr) { + ViewLayer *new_view_layer = BKE_view_layer_find( + new_scene, lapp_context.params->context.view_layer->name); + lapp_context.params->context.view_layer = static_cast( + (new_view_layer != nullptr) ? new_view_layer : new_scene->view_layers.first); + } + /* lapp_context->params->context.v3d should never be made invalid by newly linked data + * here, as it is UI data, ultimately owned by a #bScreen ID, which is not linkable. */ + } + + if (old_id->us == 0) { + ids_to_delete.add(old_id); + item.userdata = nullptr; + keep_looping = true; + Key *old_key = BKE_key_from_id(old_id); + if (old_key != nullptr) { + ids_to_delete.add(&old_key->id); + } + } + } + BKE_id_multi_delete(&bmain, ids_to_delete); + ids_to_delete.clear(); + } + + /* Some datablocks can get reloaded/replaced 'silently' because they are not linkable + * (shape keys e.g.), so we need another loop here to clear old ones if possible. */ + FOREACH_MAIN_ID_BEGIN (&bmain, id_iter) { + /* XXX That check may be a bit to generic/permissive? */ + if (id_iter->lib && (id_iter->flag & ID_TAG_PRE_EXISTING) && id_iter->us == 0) { + ids_to_delete.add(id_iter); + } + } + FOREACH_MAIN_ID_END; + BKE_id_multi_delete(&bmain, ids_to_delete); + ids_to_delete.clear(); + + /* Get rid of no more used libraries... */ + ListBase *libraries = which_libbase(&bmain, ID_LI); + LISTBASE_FOREACH (ID *, id_iter, libraries) { + ids_to_delete.add(id_iter); + } + FOREACH_MAIN_ID_BEGIN (&bmain, id_iter) { + if (id_iter->lib) { + ids_to_delete.remove(&id_iter->lib->id); + } + } + FOREACH_MAIN_ID_END; + BKE_id_multi_delete(&bmain, ids_to_delete); +} + +/** Update and resync as needed liboverrides. */ +static void blendfile_relocate_postprocess_liboverrides(BlendfileLinkAppendContext &lapp_context, + ReportList *reports) +{ + Main &bmain = *lapp_context.params->bmain; + + ID *id_iter; + FOREACH_MAIN_ID_BEGIN (&bmain, id_iter) { + if (ID_IS_LINKED(id_iter) || !ID_IS_OVERRIDE_LIBRARY_REAL(id_iter) || + (id_iter->tag & ID_TAG_PRE_EXISTING) == 0) + { + continue; + } + if ((id_iter->override_library->reference->tag & ID_TAG_MISSING) == 0) { + id_iter->tag &= ~ID_TAG_MISSING; + } + if ((id_iter->override_library->reference->tag & ID_TAG_PRE_EXISTING) == 0) { + BKE_lib_override_library_update(&bmain, id_iter); + } + } + FOREACH_MAIN_ID_END; + + BKE_library_main_rebuild_hierarchy(&bmain); + + /* Resync overrides if needed. */ + if (liboverride::is_auto_resync_enabled() && lapp_context.params->context.scene != nullptr) { + BlendFileReadReport report{}; + report.reports = reports; + BKE_lib_override_library_main_resync(&bmain, + lapp_context.params->context.scene, + lapp_context.params->context.view_layer, + &report); + /* We need to rebuild some of the deleted override rules (for UI feedback purpose). */ + BKE_lib_override_library_main_operations_create(&bmain, true, nullptr); + } +} + void BKE_blendfile_library_relocate(BlendfileLinkAppendContext *lapp_context, ReportList *reports, Library *library, @@ -1860,26 +2018,7 @@ void BKE_blendfile_library_relocate(BlendfileLinkAppendContext *lapp_context, for (BlendfileLinkAppendContextItem &item : lapp_context->items) { ID *old_id = static_cast(item.userdata); ID *new_id = item.new_id; - - blendfile_library_relocate_remap(bmain, old_id, new_id, reports, do_reload, remap_flags); - if (new_id == nullptr) { - continue; - } - /* Usual special code for ShapeKeys snowflakes... */ - Key **old_key_p = BKE_key_from_id_p(old_id); - if (old_key_p == nullptr) { - continue; - } - Key *old_key = *old_key_p; - Key *new_key = BKE_key_from_id(new_id); - if (old_key != nullptr) { - *old_key_p = nullptr; - id_us_min(&old_key->id); - blendfile_library_relocate_remap( - bmain, &old_key->id, &new_key->id, reports, do_reload, remap_flags); - *old_key_p = old_key; - id_us_plus_no_lib(&old_key->id); - } + blendfile_library_relocate_id_remap(bmain, old_id, new_id, reports, do_reload, remap_flags); } BKE_layer_collection_resync_allow(); BKE_main_collection_sync_remap(bmain); @@ -1887,135 +2026,77 @@ void BKE_blendfile_library_relocate(BlendfileLinkAppendContext *lapp_context, BKE_main_unlock(bmain); /* Delete all no more used old IDs. */ - /* NOTE: While this looping over until we are sure we deleted everything is very far from - * efficient, doing otherwise would require a much more complex handling of indirectly linked IDs - * in steps above. Currently, in case of relocation, those are skipped in remapping phase, though - * in some cases (essentially internal links between IDs from the same library) remapping should - * happen. But getting this to work reliably would be very difficult, so since this is not a - * performance-critical code, better to go with the (relatively) simpler, brute-force approach - * here in 'removal of old IDs' step. */ - bool keep_looping = true; - while (keep_looping) { - keep_looping = false; + blendfile_relocate_postprocess_cleanup(*lapp_context); - BKE_main_id_tag_all(bmain, ID_TAG_DOIT, false); - for (BlendfileLinkAppendContextItem &item : lapp_context->items) { - ID *old_id = static_cast(item.userdata); - - if (old_id == nullptr) { - continue; - } - - if (GS(old_id->name) == ID_KE) { - /* Shape Keys are handled as part of their owning obdata (see below). This implies that - * there is no way to know when the old pointer gets invalid, so just clear it immediately. - */ - item.userdata = nullptr; - continue; - } - - /* In case the active scene was reloaded, the context pointers in - * `lapp_context->params->context` need to be updated before the old Scene ID is freed. */ - if (old_id == &lapp_context->params->context.scene->id) { - BLI_assert(GS(old_id->name) == ID_SCE); - Scene *new_scene = reinterpret_cast(item.new_id); - BLI_assert(new_scene != nullptr); - lapp_context->params->context.scene = new_scene; - if (lapp_context->params->context.view_layer != nullptr) { - ViewLayer *new_view_layer = BKE_view_layer_find( - new_scene, lapp_context->params->context.view_layer->name); - lapp_context->params->context.view_layer = static_cast( - (new_view_layer != nullptr) ? new_view_layer : new_scene->view_layers.first); - } - /* lapp_context->params->context.v3d should never become invalid by newly linked data here. - */ - } - - if (old_id->us == 0) { - old_id->tag |= ID_TAG_DOIT; - item.userdata = nullptr; - keep_looping = true; - Key *old_key = BKE_key_from_id(old_id); - if (old_key != nullptr) { - old_key->id.tag |= ID_TAG_DOIT; - } - } - } - BKE_id_multi_tagged_delete(bmain); - /* Should not be needed, all tagged IDs should have been deleted above, just 'in case'. */ - BKE_main_id_tag_all(bmain, ID_TAG_DOIT, false); - } - - /* Some datablocks can get reloaded/replaced 'silently' because they are not linkable - * (shape keys e.g.), so we need another loop here to clear old ones if possible. */ - lbarray = BKE_main_lists_get(*bmain); - lba_idx = lbarray.size(); - while (lba_idx--) { - ID *id, *id_next; - for (id = static_cast(lbarray[lba_idx]->first); id; id = id_next) { - id_next = static_cast(id->next); - /* XXX That check may be a bit to generic/permissive? */ - if (id->lib && (id->flag & ID_TAG_PRE_EXISTING) && id->us == 0) { - BKE_id_free(bmain, id); - } - } - } - - /* Get rid of no more used libraries... */ - BKE_main_id_tag_idcode(bmain, ID_LI, ID_TAG_DOIT, true); - lbarray = BKE_main_lists_get(*bmain); - lba_idx = lbarray.size(); - while (lba_idx--) { - ID *id; - for (id = static_cast(lbarray[lba_idx]->first); id; id = static_cast(id->next)) { - if (id->lib) { - id->lib->id.tag &= ~ID_TAG_DOIT; - } - } - } - Library *lib, *lib_next; - for (lib = static_cast(which_libbase(bmain, ID_LI)->first); lib; lib = lib_next) { - lib_next = static_cast(lib->id.next); - if (lib->id.tag & ID_TAG_DOIT) { - id_us_clear_real(&lib->id); - if (lib->id.us == 0) { - BKE_id_delete(bmain, lib); - } - } - } - - /* Update overrides of reloaded linked data-blocks. */ - ID *id; - FOREACH_MAIN_ID_BEGIN (bmain, id) { - if (ID_IS_LINKED(id) || !ID_IS_OVERRIDE_LIBRARY_REAL(id) || - (id->tag & ID_TAG_PRE_EXISTING) == 0) - { - continue; - } - if ((id->override_library->reference->tag & ID_TAG_MISSING) == 0) { - id->tag &= ~ID_TAG_MISSING; - } - if ((id->override_library->reference->tag & ID_TAG_PRE_EXISTING) == 0) { - BKE_lib_override_library_update(bmain, id); - } - } - FOREACH_MAIN_ID_END; - - BKE_library_main_rebuild_hierarchy(bmain); - - /* Resync overrides if needed. */ - if (liboverride::is_auto_resync_enabled() && lapp_context->params->context.scene != nullptr) { - BlendFileReadReport report{}; - report.reports = reports; - BKE_lib_override_library_main_resync(bmain, - lapp_context->params->context.scene, - lapp_context->params->context.view_layer, - &report); - /* We need to rebuild some of the deleted override rules (for UI feedback purpose). */ - BKE_lib_override_library_main_operations_create(bmain, true, nullptr); - } + /* Update and resync liboverrides of reloaded linked data-blocks. */ + blendfile_relocate_postprocess_liboverrides(*lapp_context, reports); BKE_main_collection_sync(bmain); } +void BKE_blendfile_id_relocate(BlendfileLinkAppendContext &lapp_context, ReportList *reports) +{ + if (lapp_context.items.empty()) { + /* Nothing to relocate. */ + return; + } + /* Only support relocating one ID at a time currently. */ + BLI_assert(lapp_context.items.size() == 1); + + Main *bmain = lapp_context.params->bmain; + + /* Relocate only works on linked data currently. */ + BLI_assert((lapp_context.params->flag & FILE_LINK) != 0); + + /* Tag everything, its generally useful to know what is new. + * + * Take extra care `BKE_main_id_flag_all(bmain, ID_TAG_PRE_EXISTING, false)` is called after! */ + BKE_main_id_tag_all(bmain, ID_TAG_PRE_EXISTING, true); + + /* XXX We'd need re-entrant locking on Main for this to work... */ + // BKE_main_lock(bmain); + + BKE_blendfile_link(&lapp_context, reports); + + // BKE_main_unlock(bmain); + + /* Finalize relocation (remap ID usages, rebuild LibOverrides if needed, etc.). */ + + /* The first item should be the root of the relocation, and the only one containing a non-null + * `userdata`. */ + BlendfileLinkAppendContextItem &root_item = lapp_context.items.front(); + BLI_assert(root_item.userdata); + ID *old_id = static_cast(root_item.userdata); + ID *new_id = root_item.new_id; + BLI_assert(GS(old_id->name) == GS(new_id->name)); +#ifndef NDEBUG + for (BlendfileLinkAppendContextItem &item : lapp_context.items) { + BLI_assert(&item == &root_item || item.userdata == nullptr); + } +#endif + + BKE_main_lock(bmain); + BKE_layer_collection_resync_forbid(); + + /* Do not affect indirect usages. */ + const int remap_flags = ID_REMAP_SKIP_NEVER_NULL_USAGE | ID_REMAP_SKIP_INDIRECT_USAGE; + blendfile_library_relocate_id_remap(bmain, old_id, new_id, reports, false, remap_flags); + + BKE_layer_collection_resync_allow(); + BKE_main_collection_sync_remap(bmain); + BKE_main_unlock(bmain); + + /* Delete all no more used old IDs. */ + blendfile_relocate_postprocess_cleanup(lapp_context); + + /* Update and resync liboverrides of reloaded linked data-blocks. */ + blendfile_relocate_postprocess_liboverrides(lapp_context, reports); + + BKE_main_collection_sync(bmain); + + /* Important we unset, otherwise these object won't + * link into other scenes from this blend file. */ + BKE_main_id_tag_all(bmain, ID_TAG_PRE_EXISTING, false); +} + /** \} */ diff --git a/source/blender/blenkernel/intern/lib_override.cc b/source/blender/blenkernel/intern/lib_override.cc index 2502ce4adfd..754e46bdd15 100644 --- a/source/blender/blenkernel/intern/lib_override.cc +++ b/source/blender/blenkernel/intern/lib_override.cc @@ -2014,8 +2014,16 @@ static void lib_override_library_remap( */ ID *id_reference_iter = static_cast( BLI_ghashIterator_getKey(&linkedref_to_old_override_iter)); + + /* NOTE: Usually `id_reference_iter->lib == id_root_reference->lib` should always be true. + * However, there are some cases where it is not, e.g. if the linked reference of a liboverride + * is relocated to another ID in another library. */ +#if 0 BLI_assert(id_reference_iter->lib == id_root_reference->lib); UNUSED_VARS_NDEBUG(id_root_reference); +#else + UNUSED_VARS(id_root_reference); +#endif if (!id_reference_iter->newid) { remapper_overrides_reference_to_old.add(id_reference_iter, id_override_old_iter); } diff --git a/source/blender/editors/space_outliner/outliner_edit.cc b/source/blender/editors/space_outliner/outliner_edit.cc index ca120ac6d77..a3c28998bb9 100644 --- a/source/blender/editors/space_outliner/outliner_edit.cc +++ b/source/blender/editors/space_outliner/outliner_edit.cc @@ -66,6 +66,7 @@ #include "RNA_define.hh" #include "RNA_enum_types.hh" #include "RNA_path.hh" +#include "RNA_prototypes.hh" #include "GPU_material.hh" @@ -912,6 +913,70 @@ void OUTLINER_OT_id_paste(wmOperatorType *ot) /** \} */ +/* -------------------------------------------------------------------- */ +/** \name Linked ID Relocate Operator + * \{ */ + +static wmOperatorStatus outliner_id_relocate_invoke(bContext *C, + wmOperator *op, + const wmEvent * /*event*/) +{ + PointerRNA id_linked_ptr = CTX_data_pointer_get_type(C, "id", &RNA_ID); + ID *id_linked = static_cast(id_linked_ptr.data); + + if (!id_linked) { + BKE_report(op->reports, RPT_ERROR_INVALID_INPUT, "There is no active data-block"); + return OPERATOR_CANCELLED; + } + if (!ID_IS_LINKED(id_linked) || !BKE_idtype_idcode_is_linkable(GS(id_linked->name))) { + BKE_reportf(op->reports, + RPT_ERROR_INVALID_INPUT, + "The active data-block '%s' is not a valid linked one", + BKE_id_name(*id_linked)); + return OPERATOR_CANCELLED; + } + if (BKE_library_ID_is_indirectly_used(CTX_data_main(C), id_linked)) { + BKE_reportf(op->reports, + RPT_ERROR_INVALID_INPUT, + "The active data-block '%s' is used by other linked data", + BKE_id_name(*id_linked)); + return OPERATOR_CANCELLED; + } + + wmOperatorType *ot = WM_operatortype_find("WM_OT_id_linked_relocate", false); + PointerRNA op_props; + + WM_operator_properties_create_ptr(&op_props, ot); + RNA_int_set(&op_props, "id_session_uid", *reinterpret_cast(&id_linked->session_uid)); + + const wmOperatorStatus ret = WM_operator_name_call_ptr( + C, ot, WM_OP_INVOKE_DEFAULT, &op_props, nullptr); + + WM_operator_properties_free(&op_props); + + /* If the matching WM operator invoke was successful, it was added to modal handlers. This + * operator however is _not_ modal, and will memleak if it returns this status. */ + return (ret == OPERATOR_RUNNING_MODAL) ? OPERATOR_FINISHED : ret; +} + +void OUTLINER_OT_id_linked_relocate(wmOperatorType *ot) +{ + ot->name = "Relocate Linked ID"; + ot->idname = "OUTLINER_OT_id_linked_relocate"; + ot->description = + "Replace the active linked ID (and its dependencies if any) by another one, from the same " + "or a different library"; + + ot->invoke = outliner_id_relocate_invoke; + ot->poll = ED_operator_region_outliner_active; + + /* Flags. No undo, no registering, all the actual work/changes is done by the matching WM + * operator. */ + ot->flag = 0; +} + +/** \} */ + /* -------------------------------------------------------------------- */ /** \name Library Relocate Operator * \{ */ diff --git a/source/blender/editors/space_outliner/outliner_intern.hh b/source/blender/editors/space_outliner/outliner_intern.hh index dd515bac063..5161d664082 100644 --- a/source/blender/editors/space_outliner/outliner_intern.hh +++ b/source/blender/editors/space_outliner/outliner_intern.hh @@ -485,6 +485,7 @@ void OUTLINER_OT_lib_relocate(wmOperatorType *ot); void OUTLINER_OT_lib_reload(wmOperatorType *ot); void OUTLINER_OT_id_delete(wmOperatorType *ot); +void OUTLINER_OT_id_linked_relocate(wmOperatorType *ot); void OUTLINER_OT_show_one_level(wmOperatorType *ot); void OUTLINER_OT_show_active(wmOperatorType *ot); diff --git a/source/blender/editors/space_outliner/outliner_ops.cc b/source/blender/editors/space_outliner/outliner_ops.cc index e7595abe245..c2482551cfc 100644 --- a/source/blender/editors/space_outliner/outliner_ops.cc +++ b/source/blender/editors/space_outliner/outliner_ops.cc @@ -36,6 +36,7 @@ void outliner_operatortypes() WM_operatortype_append(OUTLINER_OT_id_operation); WM_operatortype_append(OUTLINER_OT_id_delete); WM_operatortype_append(OUTLINER_OT_id_remap); + WM_operatortype_append(OUTLINER_OT_id_linked_relocate); WM_operatortype_append(OUTLINER_OT_id_copy); WM_operatortype_append(OUTLINER_OT_id_paste); WM_operatortype_append(OUTLINER_OT_data_operation); diff --git a/source/blender/windowmanager/intern/wm_event_system.cc b/source/blender/windowmanager/intern/wm_event_system.cc index 1cf1f4b0727..47f4aed0d86 100644 --- a/source/blender/windowmanager/intern/wm_event_system.cc +++ b/source/blender/windowmanager/intern/wm_event_system.cc @@ -1596,6 +1596,13 @@ static wmOperatorStatus wm_operator_invoke(bContext *C, return wmOperatorStatus(WM_operator_poll(C, ot)); } + if (STREQ("WM_OT_id_linked_relocate", ot->idname)) { + printf("foo\n"); + } + if (STREQ("OUTLINER_OT_id_linked_relocate", ot->idname)) { + printf("bar\n"); + } + if (WM_operator_poll(C, ot)) { wmWindowManager *wm = CTX_wm_manager(C); const intptr_t undo_id_prev = wm_operator_undo_active_id(wm); diff --git a/source/blender/windowmanager/intern/wm_files_link.cc b/source/blender/windowmanager/intern/wm_files_link.cc index 6588a39f54a..f25cd832c10 100644 --- a/source/blender/windowmanager/intern/wm_files_link.cc +++ b/source/blender/windowmanager/intern/wm_files_link.cc @@ -397,15 +397,20 @@ static wmOperatorStatus wm_link_append_exec(bContext *C, wmOperator *op) return OPERATOR_FINISHED; } -static void wm_link_append_properties_common(wmOperatorType *ot, bool is_link) +static void wm_link_append_properties_common(wmOperatorType *ot, + const bool is_link, + const bool is_relocate) { PropertyRNA *prop; /* Better not save _any_ settings for this operator. */ /* Properties. */ - prop = RNA_def_boolean( - ot->srna, "link", is_link, "Link", "Link the objects or data-blocks rather than appending"); + prop = RNA_def_boolean(ot->srna, + "link", + is_link || is_relocate, + "Link", + "Link the objects or data-blocks rather than appending"); RNA_def_property_flag(prop, PROP_SKIP_SAVE | PROP_HIDDEN); prop = RNA_def_boolean( @@ -427,15 +432,18 @@ static void wm_link_append_properties_common(wmOperatorType *ot, bool is_link) prop = RNA_def_boolean(ot->srna, "active_collection", - true, + !is_relocate, "Active Collection", "Put new objects on the active collection"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); + /* NOTE: do not force instancing when relocating, as direct data (the selected ID) status should + * not change on that regard, and other dependencies would be indirectly linked and therefore + * should not require any enforced instancing when linked. */ prop = RNA_def_boolean( ot->srna, "instance_collections", - is_link, + is_link && !is_relocate, "Instance Collections", "Create instances for collections, rather than adding them directly to the scene"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); @@ -443,7 +451,7 @@ static void wm_link_append_properties_common(wmOperatorType *ot, bool is_link) prop = RNA_def_boolean( ot->srna, "instance_object_data", - true, + !is_relocate, "Instance Object Data", "Create instances for object data which are not referenced by any objects"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); @@ -470,7 +478,7 @@ void WM_OT_link(wmOperatorType *ot) FILE_DEFAULTDISPLAY, FILE_SORT_DEFAULT); - wm_link_append_properties_common(ot, true); + wm_link_append_properties_common(ot, true, false); } void WM_OT_append(wmOperatorType *ot) @@ -494,7 +502,7 @@ void WM_OT_append(wmOperatorType *ot) FILE_DEFAULTDISPLAY, FILE_SORT_DEFAULT); - wm_link_append_properties_common(ot, false); + wm_link_append_properties_common(ot, false, false); RNA_def_boolean(ot->srna, "set_fake", false, @@ -508,6 +516,180 @@ void WM_OT_append(wmOperatorType *ot) "Localize all appended data, including those indirectly linked from other libraries"); } +static wmOperatorStatus wm_id_linked_relocate_exec(bContext *C, wmOperator *op) +{ + Main *bmain = CTX_data_main(C); + Scene *scene = CTX_data_scene(C); + ViewLayer *view_layer = CTX_data_view_layer(C); + BlendfileLinkAppendContext *lapp_context; + char filepath[FILE_MAX_LIBEXTRA], root[FILE_MAXDIR], libname[FILE_MAX_LIBEXTRA], + relname[FILE_MAX]; + char *group, *name; + + RNA_string_get(op->ptr, "filename", relname); + RNA_string_get(op->ptr, "directory", root); + + BLI_path_join(filepath, sizeof(filepath), root, relname); + + /* Test if we have a valid data. */ + const bool is_librarypath_valid = BKE_blendfile_library_path_explode( + filepath, libname, &group, &name); + + /* NOTE: Need to also check filepath, as typically libname is an empty string here (when trying + * to append from current file from the file-browser e.g.). */ + if (BLI_path_cmp(BKE_main_blendfile_path(bmain), filepath) == 0 || + BLI_path_cmp(BKE_main_blendfile_path(bmain), libname) == 0) + { + BKE_reportf(op->reports, RPT_ERROR, "'%s': cannot use current file as library", filepath); + return OPERATOR_CANCELLED; + } + if (!is_librarypath_valid) { + BKE_reportf(op->reports, RPT_ERROR, "'%s': not a library", filepath); + return OPERATOR_CANCELLED; + } + if (!group || !name) { + BKE_reportf(op->reports, RPT_ERROR, "'%s': nothing indicated", filepath); + return OPERATOR_CANCELLED; + } + + int flag = wm_link_append_flag(op); + BLI_assert(flag & FILE_LINK); + + const short id_type_code = BKE_idtype_idcode_from_name(group); + + const int tmp_id_session_uid = RNA_int_get(op->ptr, "id_session_uid"); + const uint id_session_uid = *reinterpret_cast(&tmp_id_session_uid); + /* NOTE: Creating a full ID map for a single lookup is not worth it. */ + ID *linked_id = BKE_libblock_find_session_uid(bmain, id_session_uid); + + { + const char *linked_id_name = BKE_id_name(*linked_id); + + if (!linked_id || !ID_IS_LINKED(linked_id)) { + BKE_reportf(op->reports, RPT_ERROR, "No valid existing linked ID given to relocate"); + return OPERATOR_CANCELLED; + } + if (GS(linked_id->name) != id_type_code) { + BKE_reportf(op->reports, + RPT_ERROR, + "Selected ID '%s' is a %s, cannot be used to relocate existing linked ID '%s' " + "which is a %s", + name, + group, + linked_id_name, + BKE_idtype_idcode_to_name(GS(linked_id->name))); + return OPERATOR_CANCELLED; + } + if (STREQ(linked_id_name, name) && STREQ(linked_id->lib->runtime->filepath_abs, libname)) { + BKE_reportf(op->reports, + RPT_ERROR, + "Selected ID '%s' seems to be the same as the relocated ID '%s', use 'Reload' " + "operation instead", + name, + linked_id_name); + return OPERATOR_CANCELLED; + } + } + + /* From here down, no error returns. */ + + if (view_layer && RNA_boolean_get(op->ptr, "autoselect")) { + BKE_view_layer_base_deselect_all(scene, view_layer); + } + + /* Never enforce instantiation of anything when relocating. */ + flag &= ~(BLO_LIBLINK_COLLECTION_INSTANCE | BLO_LIBLINK_OBDATA_INSTANCE); + + /* Tag everything, its generally useful to know what is new. + * + * Take extra care `BKE_main_id_flag_all(bmain, ID_TAG_PRE_EXISTING, false)` is called after! */ + BKE_main_id_tag_all(bmain, ID_TAG_PRE_EXISTING, true); + + /* We define our working data... + * Note that here, each item 'uses' one library, and only one. */ + LibraryLink_Params lapp_params; + BLO_library_link_params_init_with_context( + &lapp_params, bmain, flag, 0, scene, view_layer, CTX_wm_view3d(C)); + + lapp_context = BKE_blendfile_link_append_context_new(&lapp_params); + BKE_blendfile_link_append_context_embedded_blendfile_set( + lapp_context, datatoc_startup_blend, datatoc_startup_blend_size); + + BKE_blendfile_link_append_context_library_add(lapp_context, libname, nullptr); + BlendfileLinkAppendContextItem *item = BKE_blendfile_link_append_context_item_add( + lapp_context, name, id_type_code, linked_id); + BKE_blendfile_link_append_context_item_library_index_enable(lapp_context, item, 0); + + BKE_blendfile_link_append_context_init_done(lapp_context); + + BKE_blendfile_id_relocate(*lapp_context, op->reports); + + BKE_blendfile_link_append_context_finalize(lapp_context); + + BKE_blendfile_link_append_context_free(lapp_context); + + /* TODO(sergey): Use proper flag for tagging here. */ + + /* TODO(dalai): Temporary solution! + * Ideally we only need to tag the new objects themselves, not the scene. + * This way we'll avoid flush of collection properties + * to all objects and limit update to the particular object only. + * But afraid first we need to change collection evaluation in DEG + * according to depsgraph manifesto. */ + if (scene) { + DEG_id_tag_update(&scene->id, 0); + } + + /* Recreate dependency graph to include new objects. */ + DEG_relations_tag_update(bmain); + + /* TODO: align `G.filepath_last_library` with other directory storage + * (like last opened image, etc). */ + STRNCPY(G.filepath_last_library, root); + + WM_event_add_notifier(C, NC_WINDOW, nullptr); + + return OPERATOR_FINISHED; +} + +void WM_OT_id_linked_relocate(wmOperatorType *ot) +{ + ot->name = "Relocate Linked ID"; + ot->idname = "WM_OT_id_linked_relocate"; + ot->description = + "Relocate a linked ID, i.e. select another ID to link, and remap its local usages to that " + "newly linked data-block). Currently only designed as an internal operator, not directly " + "exposed to the user"; + + ot->invoke = wm_link_append_invoke; + ot->exec = wm_id_linked_relocate_exec; + ot->poll = wm_link_append_poll; + + ot->flag = OPTYPE_INTERNAL | OPTYPE_REGISTER | OPTYPE_UNDO; + + PropertyRNA *prop = RNA_def_int(ot->srna, + "id_session_uid", + MAIN_ID_SESSION_UID_UNSET, + 0, + INT_MAX, + "Linked ID Session UID", + "Unique runtime identifier for the linked ID to relocate", + 0, + INT_MAX); + RNA_def_property_flag(prop, PROP_HIDDEN); + + WM_operator_properties_filesel(ot, + FILE_TYPE_FOLDER | FILE_TYPE_BLENDER | FILE_TYPE_BLENDERLIB, + FILE_LOADLIB, + FILE_OPENFILE, + WM_FILESEL_FILEPATH | WM_FILESEL_DIRECTORY | WM_FILESEL_FILENAME | + WM_FILESEL_RELPATH | WM_FILESEL_SHOW_PROPS, + FILE_DEFAULTDISPLAY, + FILE_SORT_DEFAULT); + + wm_link_append_properties_common(ot, true, true); +} + /** \} */ /* -------------------------------------------------------------------- */ diff --git a/source/blender/windowmanager/intern/wm_operators.cc b/source/blender/windowmanager/intern/wm_operators.cc index d8c8a55686a..12d830a058f 100644 --- a/source/blender/windowmanager/intern/wm_operators.cc +++ b/source/blender/windowmanager/intern/wm_operators.cc @@ -4185,6 +4185,7 @@ void wm_operatortypes_register() WM_operatortype_append(WM_OT_revert_mainfile); WM_operatortype_append(WM_OT_link); WM_operatortype_append(WM_OT_append); + WM_operatortype_append(WM_OT_id_linked_relocate); WM_operatortype_append(WM_OT_lib_relocate); WM_operatortype_append(WM_OT_lib_reload); WM_operatortype_append(WM_OT_recover_last_session); diff --git a/source/blender/windowmanager/wm_files.hh b/source/blender/windowmanager/wm_files.hh index 54e784041f7..979bae258c2 100644 --- a/source/blender/windowmanager/wm_files.hh +++ b/source/blender/windowmanager/wm_files.hh @@ -127,6 +127,7 @@ void WM_OT_clear_recent_files(wmOperatorType *ot); void WM_OT_link(wmOperatorType *ot); void WM_OT_append(wmOperatorType *ot); +void WM_OT_id_linked_relocate(wmOperatorType *ot); void WM_OT_lib_relocate(wmOperatorType *ot); void WM_OT_lib_reload(wmOperatorType *ot);