Core: Add 'relocate' operation on individual linked ID.

So far it was only possible to relocate a whole library, now one can
also relocate a single linked ID (pulling in all of its dependencies).

This is essentially linking the new data, remapping local usages of the
old linked data to the new one, removing no more used IDs, and updating
liboverrides if needed.
This commit is contained in:
Bastien Montagne
2025-03-13 19:24:48 +01:00
committed by Bastien Montagne
parent 7f724115f7
commit 0eba8caaf9
11 changed files with 547 additions and 171 deletions

View File

@@ -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,

View File

@@ -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);

View File

@@ -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<ID *> 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<ID *>(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<Scene *>(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<ViewLayer *>(
(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<ID *>(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<ID *>(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<Scene *>(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<ViewLayer *>(
(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<ID *>(lbarray[lba_idx]->first); id; id = id_next) {
id_next = static_cast<ID *>(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<ID *>(lbarray[lba_idx]->first); id; id = static_cast<ID *>(id->next)) {
if (id->lib) {
id->lib->id.tag &= ~ID_TAG_DOIT;
}
}
}
Library *lib, *lib_next;
for (lib = static_cast<Library *>(which_libbase(bmain, ID_LI)->first); lib; lib = lib_next) {
lib_next = static_cast<Library *>(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<ID *>(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);
}
/** \} */

View File

@@ -2014,8 +2014,16 @@ static void lib_override_library_remap(
*/
ID *id_reference_iter = static_cast<ID *>(
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);
}

View File

@@ -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 *>(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<int *>(&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
* \{ */

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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<const uint *>(&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);
}
/** \} */
/* -------------------------------------------------------------------- */

View File

@@ -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);

View File

@@ -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);