/* SPDX-License-Identifier: GPL-2.0-or-later * Copyright 2004 Blender Foundation. All rights reserved. */ /** \file * \ingroup spoutliner */ #include #include "MEM_guardedalloc.h" #include "DNA_ID.h" #include "DNA_anim_types.h" #include "DNA_collection_types.h" #include "DNA_object_types.h" #include "DNA_scene_types.h" #include "BLI_blenlib.h" #include "BLI_dynstr.h" #include "BLI_path_util.h" #include "BLI_utildefines.h" #include "BLT_translation.h" #include "BKE_action.h" #include "BKE_animsys.h" #include "BKE_appdir.h" #include "BKE_armature.h" #include "BKE_blender_copybuffer.h" #include "BKE_context.h" #include "BKE_idtype.h" #include "BKE_layer.h" #include "BKE_lib_id.h" #include "BKE_lib_override.h" #include "BKE_lib_query.h" #include "BKE_lib_remap.h" #include "BKE_main.h" #include "BKE_object.h" #include "BKE_report.h" #include "BKE_workspace.h" #include "DEG_depsgraph.h" #include "DEG_depsgraph_build.h" #include "ED_keyframing.h" #include "ED_outliner.h" #include "ED_screen.h" #include "ED_select_utils.h" #include "WM_api.h" #include "WM_types.h" #include "UI_interface.h" #include "UI_view2d.h" #include "RNA_access.h" #include "RNA_define.h" #include "RNA_enum_types.h" #include "RNA_path.h" #include "GPU_material.h" #include "outliner_intern.hh" #include "tree/tree_element_rna.hh" #include "tree/tree_iterator.hh" using namespace blender::ed::outliner; namespace blender::ed::outliner { static void outliner_show_active(SpaceOutliner *space_outliner, ARegion *region, TreeElement *te, ID *id); /* -------------------------------------------------------------------- */ /** \name Highlight on Cursor Motion Operator * \{ */ static int outliner_highlight_update(bContext *C, wmOperator * /*op*/, const wmEvent *event) { /* stop highlighting if out of area */ if (!ED_screen_area_active(C)) { return OPERATOR_PASS_THROUGH; } /* Drag and drop does own highlighting. */ wmWindowManager *wm = CTX_wm_manager(C); if (wm->drags.first) { return OPERATOR_PASS_THROUGH; } ARegion *region = CTX_wm_region(C); SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); float view_mval[2]; UI_view2d_region_to_view( ®ion->v2d, event->mval[0], event->mval[1], &view_mval[0], &view_mval[1]); TreeElement *hovered_te = outliner_find_item_at_y( space_outliner, &space_outliner->tree, view_mval[1]); TreeElement *icon_te = nullptr; bool is_over_icon = false; if (hovered_te) { icon_te = outliner_find_item_at_x_in_row( space_outliner, hovered_te, view_mval[0], nullptr, &is_over_icon); } bool changed = false; if (!hovered_te || !is_over_icon || !(hovered_te->store_elem->flag & TSE_HIGHLIGHTED) || !(icon_te->store_elem->flag & TSE_HIGHLIGHTED_ICON)) { /* Clear highlights when nothing is hovered or when a new item is hovered. */ changed = outliner_flag_set(*space_outliner, TSE_HIGHLIGHTED_ANY | TSE_DRAG_ANY, false); if (hovered_te) { hovered_te->store_elem->flag |= TSE_HIGHLIGHTED; changed = true; } if (is_over_icon) { icon_te->store_elem->flag |= TSE_HIGHLIGHTED_ICON; changed = true; } } if (changed) { ED_region_tag_redraw_no_rebuild(region); } return OPERATOR_PASS_THROUGH; } void OUTLINER_OT_highlight_update(wmOperatorType *ot) { ot->name = "Update Highlight"; ot->idname = "OUTLINER_OT_highlight_update"; ot->description = "Update the item highlight based on the current mouse position"; ot->invoke = outliner_highlight_update; ot->poll = ED_operator_outliner_active; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Toggle Open/Closed Operator * \{ */ void outliner_item_openclose(TreeElement *te, bool open, bool toggle_all) { /* Only allow opening elements with children. */ if (!(te->flag & TE_PRETEND_HAS_CHILDREN) && BLI_listbase_is_empty(&te->subtree)) { return; } /* Don't allow collapsing the scene collection. */ TreeStoreElem *tselem = TREESTORE(te); if (tselem->type == TSE_VIEW_COLLECTION_BASE) { return; } if (open) { tselem->flag &= ~TSE_CLOSED; } else { tselem->flag |= TSE_CLOSED; } if (toggle_all) { outliner_flag_set(te->subtree, TSE_CLOSED, !open); } } struct OpenCloseData { TreeStoreElem *prev_tselem; bool open; int x_location; }; static int outliner_item_openclose_modal(bContext *C, wmOperator *op, const wmEvent *event) { ARegion *region = CTX_wm_region(C); SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); float view_mval[2]; UI_view2d_region_to_view( ®ion->v2d, event->mval[0], event->mval[1], &view_mval[0], &view_mval[1]); if (event->type == MOUSEMOVE) { TreeElement *te = outliner_find_item_at_y(space_outliner, &space_outliner->tree, view_mval[1]); OpenCloseData *data = (OpenCloseData *)op->customdata; /* Only openclose if mouse is not over the previously toggled element */ if (te && TREESTORE(te) != data->prev_tselem) { /* Only toggle openclose on the same level as the first clicked element */ if (te->xs == data->x_location) { outliner_item_openclose(te, data->open, false); outliner_tag_redraw_avoid_rebuild_on_open_change(space_outliner, region); } } if (te) { data->prev_tselem = TREESTORE(te); } else { data->prev_tselem = nullptr; } } else if (event->val == KM_RELEASE) { MEM_freeN(op->customdata); return OPERATOR_FINISHED; } return OPERATOR_RUNNING_MODAL; } static int outliner_item_openclose_invoke(bContext *C, wmOperator *op, const wmEvent *event) { ARegion *region = CTX_wm_region(C); SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); const bool toggle_all = RNA_boolean_get(op->ptr, "all"); float view_mval[2]; int mval[2]; WM_event_drag_start_mval(event, region, mval); UI_view2d_region_to_view(®ion->v2d, mval[0], mval[1], &view_mval[0], &view_mval[1]); TreeElement *te = outliner_find_item_at_y(space_outliner, &space_outliner->tree, view_mval[1]); if (te && outliner_item_is_co_within_close_toggle(te, view_mval[0])) { TreeStoreElem *tselem = TREESTORE(te); const bool open = (tselem->flag & TSE_CLOSED) || (toggle_all && outliner_flag_is_any_test(&te->subtree, TSE_CLOSED, 1)); outliner_item_openclose(te, open, toggle_all); outliner_tag_redraw_avoid_rebuild_on_open_change(space_outliner, region); /* Only toggle once for single click toggling */ if ((event->type == LEFTMOUSE) && (event->val != KM_CLICK_DRAG)) { return OPERATOR_FINISHED; } /* Store last expanded tselem and x coordinate of disclosure triangle */ OpenCloseData *toggle_data = MEM_cnew("open_close_data"); toggle_data->prev_tselem = tselem; toggle_data->open = open; toggle_data->x_location = te->xs; /* Store the first clicked on element */ op->customdata = toggle_data; WM_event_add_modal_handler(C, op); return OPERATOR_RUNNING_MODAL; } return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH; } void OUTLINER_OT_item_openclose(wmOperatorType *ot) { ot->name = "Open/Close"; ot->idname = "OUTLINER_OT_item_openclose"; ot->description = "Toggle whether item under cursor is enabled or closed"; ot->invoke = outliner_item_openclose_invoke; ot->modal = outliner_item_openclose_modal; ot->poll = ED_operator_outliner_active; RNA_def_boolean(ot->srna, "all", false, "All", "Close or open all items"); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Rename Operator * \{ */ static void do_item_rename(ARegion *region, TreeElement *te, TreeStoreElem *tselem, ReportList *reports) { bool add_textbut = false; /* can't rename rna datablocks entries or listbases */ if (ELEM(tselem->type, TSE_RNA_STRUCT, TSE_RNA_PROPERTY, TSE_RNA_ARRAY_ELEM, TSE_ID_BASE, TSE_SCENE_OBJECTS_BASE)) { /* do nothing */ } else if (ELEM(tselem->type, TSE_ANIM_DATA, TSE_NLA, TSE_DEFGROUP_BASE, TSE_CONSTRAINT_BASE, TSE_MODIFIER_BASE, TSE_DRIVER_BASE, TSE_POSE_BASE, TSE_POSEGRP_BASE, TSE_R_LAYER_BASE, TSE_SCENE_COLLECTION_BASE, TSE_VIEW_COLLECTION_BASE, TSE_LIBRARY_OVERRIDE_BASE)) { BKE_report(reports, RPT_WARNING, "Cannot edit builtin name"); } else if (ELEM(tselem->type, TSE_SEQUENCE, TSE_SEQ_STRIP, TSE_SEQUENCE_DUP)) { BKE_report(reports, RPT_WARNING, "Cannot edit sequence name"); } else if (ID_IS_LINKED(tselem->id)) { BKE_report(reports, RPT_WARNING, "Cannot edit external library data"); } else if (ID_IS_OVERRIDE_LIBRARY(tselem->id)) { BKE_report(reports, RPT_WARNING, "Cannot edit name of an override data-block"); } else if (outliner_is_collection_tree_element(te)) { Collection *collection = outliner_collection_from_tree_element(te); if (collection->flag & COLLECTION_IS_MASTER) { BKE_report(reports, RPT_WARNING, "Cannot edit name of master collection"); } else { add_textbut = true; } } else if (te->idcode == ID_LI) { if (reinterpret_cast(tselem->id)->parent) { BKE_report(reports, RPT_WARNING, "Cannot edit the path of an indirectly linked library"); } else { BKE_report( reports, RPT_WARNING, "Library path is not editable from here anymore, please use Relocate operation instead"); } } else { add_textbut = true; } if (add_textbut) { tselem->flag |= TSE_TEXTBUT; ED_region_tag_redraw(region); } } void item_rename_fn(bContext *C, ReportList *reports, Scene * /*scene*/, TreeElement *te, TreeStoreElem * /*tsep*/, TreeStoreElem *tselem, void * /*user_data*/) { ARegion *region = CTX_wm_region(C); do_item_rename(region, te, tselem, reports); } static TreeElement *outliner_item_rename_find_active(const SpaceOutliner *space_outliner, ReportList *reports) { TreeElement *active_element = outliner_find_element_with_flag(&space_outliner->tree, TSE_ACTIVE); if (!active_element) { BKE_report(reports, RPT_WARNING, "No active item to rename"); return nullptr; } return active_element; } static TreeElement *outliner_item_rename_find_hovered(const SpaceOutliner *space_outliner, ARegion *region, const wmEvent *event) { float fmval[2]; UI_view2d_region_to_view(®ion->v2d, event->mval[0], event->mval[1], &fmval[0], &fmval[1]); TreeElement *hovered = outliner_find_item_at_y(space_outliner, &space_outliner->tree, fmval[1]); if (hovered && outliner_item_is_co_over_name(hovered, fmval[0])) { return hovered; } return nullptr; } static int outliner_item_rename(bContext *C, wmOperator *op, const wmEvent *event) { ARegion *region = CTX_wm_region(C); View2D *v2d = ®ion->v2d; SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); const bool use_active = RNA_boolean_get(op->ptr, "use_active"); TreeElement *te = use_active ? outliner_item_rename_find_active(space_outliner, op->reports) : outliner_item_rename_find_hovered(space_outliner, region, event); if (!te) { return OPERATOR_CANCELLED; } /* Force element into view. */ outliner_show_active(space_outliner, region, te, TREESTORE(te)->id); int size_y = BLI_rcti_size_y(&v2d->mask) + 1; int ytop = (te->ys + (size_y / 2)); int delta_y = ytop - v2d->cur.ymax; outliner_scroll_view(space_outliner, region, delta_y); do_item_rename(region, te, TREESTORE(te), op->reports); return OPERATOR_FINISHED; } void OUTLINER_OT_item_rename(wmOperatorType *ot) { ot->name = "Rename"; ot->idname = "OUTLINER_OT_item_rename"; ot->description = "Rename the active element"; ot->invoke = outliner_item_rename; ot->poll = ED_operator_outliner_active; /* Flags. */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; RNA_def_boolean(ot->srna, "use_active", false, "Use Active", "Rename the active item, rather than the one the mouse is over"); } /** \} */ /* -------------------------------------------------------------------- */ /** \name ID Delete Operator * \{ */ static void id_delete_tag(bContext *C, ReportList *reports, TreeElement *te, TreeStoreElem *tselem) { Main *bmain = CTX_data_main(C); ID *id = tselem->id; BLI_assert(id != nullptr); BLI_assert(((tselem->type == TSE_SOME_ID) && (te->idcode != 0)) || (tselem->type == TSE_LAYER_COLLECTION)); UNUSED_VARS_NDEBUG(te); if (ID_IS_OVERRIDE_LIBRARY(id)) { if (!ID_IS_OVERRIDE_LIBRARY_REAL(id) || (id->override_library->flag & IDOVERRIDE_LIBRARY_FLAG_NO_HIERARCHY) == 0) { BKE_reportf(reports, RPT_WARNING, "Cannot delete library override id '%s', it is part of an override hierarchy", id->name); return; } } if (te->idcode == ID_LI && ((Library *)id)->parent != nullptr) { BKE_reportf(reports, RPT_WARNING, "Cannot delete indirectly linked library '%s'", id->name); return; } if (id->tag & LIB_TAG_INDIRECT) { BKE_reportf(reports, RPT_WARNING, "Cannot delete indirectly linked id '%s'", id->name); return; } if (ID_REAL_USERS(id) <= 1 && BKE_library_ID_is_indirectly_used(bmain, id)) { BKE_reportf(reports, RPT_WARNING, "Cannot delete id '%s', indirectly used data-blocks need at least one user", id->name); return; } if (te->idcode == ID_WS) { BKE_workspace_id_tag_all_visible(bmain, LIB_TAG_PRE_EXISTING); if (id->tag & LIB_TAG_PRE_EXISTING) { BKE_reportf( reports, RPT_WARNING, "Cannot delete currently visible workspace id '%s'", id->name); BKE_main_id_tag_idcode(bmain, ID_WS, LIB_TAG_PRE_EXISTING, false); return; } BKE_main_id_tag_idcode(bmain, ID_WS, LIB_TAG_PRE_EXISTING, false); } id->tag |= LIB_TAG_DOIT; WM_event_add_notifier(C, NC_WINDOW, nullptr); } void id_delete_tag_fn(bContext *C, ReportList *reports, Scene * /*scene*/, TreeElement *te, TreeStoreElem * /*tsep*/, TreeStoreElem *tselem, void * /*user_data*/) { id_delete_tag(C, reports, te, tselem); } static int outliner_id_delete_tag(bContext *C, ReportList *reports, TreeElement *te, const float mval[2]) { int id_tagged_num = 0; if (mval[1] > te->ys && mval[1] < te->ys + UI_UNIT_Y) { TreeStoreElem *tselem = TREESTORE(te); if (te->idcode != 0 && tselem->id) { if (te->idcode == ID_LI && ((Library *)tselem->id)->parent) { BKE_reportf(reports, RPT_ERROR_INVALID_INPUT, "Cannot delete indirectly linked library '%s'", ((Library *)tselem->id)->filepath_abs); } else { id_delete_tag(C, reports, te, tselem); id_tagged_num++; } } } else { LISTBASE_FOREACH (TreeElement *, te_sub, &te->subtree) { if ((id_tagged_num += outliner_id_delete_tag(C, reports, te_sub, mval)) != 0) { break; } } } return id_tagged_num; } static int outliner_id_delete_invoke(bContext *C, wmOperator *op, const wmEvent *event) { Main *bmain = CTX_data_main(C); ARegion *region = CTX_wm_region(C); SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); float fmval[2]; BLI_assert(region && space_outliner); UI_view2d_region_to_view(®ion->v2d, event->mval[0], event->mval[1], &fmval[0], &fmval[1]); int id_tagged_num = 0; BKE_main_id_tag_all(bmain, LIB_TAG_DOIT, false); LISTBASE_FOREACH (TreeElement *, te, &space_outliner->tree) { if ((id_tagged_num += outliner_id_delete_tag(C, op->reports, te, fmval)) != 0) { break; } } if (id_tagged_num == 0) { BKE_main_id_tag_all(bmain, LIB_TAG_DOIT, false); return OPERATOR_CANCELLED; } BKE_id_multi_tagged_delete(bmain); BKE_main_id_tag_all(bmain, LIB_TAG_DOIT, false); return OPERATOR_FINISHED; } void OUTLINER_OT_id_delete(wmOperatorType *ot) { ot->name = "Delete Data-Block"; ot->idname = "OUTLINER_OT_id_delete"; ot->description = "Delete the ID under cursor"; ot->invoke = outliner_id_delete_invoke; ot->poll = ED_operator_outliner_active; /* Flags. */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; } /** \} */ /* -------------------------------------------------------------------- */ /** \name ID Remap Operator * \{ */ static int outliner_id_remap_exec(bContext *C, wmOperator *op) { Main *bmain = CTX_data_main(C); SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); const short id_type = short(RNA_enum_get(op->ptr, "id_type")); ID *old_id = static_cast( BLI_findlink(which_libbase(CTX_data_main(C), id_type), RNA_enum_get(op->ptr, "old_id"))); ID *new_id = static_cast( BLI_findlink(which_libbase(CTX_data_main(C), id_type), RNA_enum_get(op->ptr, "new_id"))); /* check for invalid states */ if (space_outliner == nullptr) { return OPERATOR_CANCELLED; } if (!(old_id && new_id && (old_id != new_id) && (GS(old_id->name) == GS(new_id->name)))) { BKE_reportf(op->reports, RPT_ERROR_INVALID_INPUT, "Invalid old/new ID pair ('%s' / '%s')", old_id ? old_id->name : "Invalid ID", new_id ? new_id->name : "Invalid ID"); return OPERATOR_CANCELLED; } if (ID_IS_LINKED(old_id)) { BKE_reportf(op->reports, RPT_WARNING, "Old ID '%s' is linked from a library, indirect usages of this data-block will " "not be remapped", old_id->name); } BKE_libblock_remap( bmain, old_id, new_id, ID_REMAP_SKIP_INDIRECT_USAGE | ID_REMAP_SKIP_NEVER_NULL_USAGE); BKE_main_lib_objects_recalc_all(bmain); /* recreate dependency graph to include new objects */ DEG_relations_tag_update(bmain); /* Free gpu materials, some materials depend on existing objects, * such as lights so freeing correctly refreshes. */ GPU_materials_free(bmain); WM_event_add_notifier(C, NC_WINDOW, nullptr); return OPERATOR_FINISHED; } static bool outliner_id_remap_find_tree_element(bContext *C, wmOperator *op, ListBase *tree, const float y) { LISTBASE_FOREACH (TreeElement *, te, tree) { if (y > te->ys && y < te->ys + UI_UNIT_Y) { TreeStoreElem *tselem = TREESTORE(te); if ((tselem->type == TSE_SOME_ID) && tselem->id) { RNA_enum_set(op->ptr, "id_type", GS(tselem->id->name)); RNA_enum_set_identifier(C, op->ptr, "new_id", tselem->id->name + 2); RNA_enum_set_identifier(C, op->ptr, "old_id", tselem->id->name + 2); return true; } } if (outliner_id_remap_find_tree_element(C, op, &te->subtree, y)) { return true; } } return false; } static int outliner_id_remap_invoke(bContext *C, wmOperator *op, const wmEvent *event) { SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); ARegion *region = CTX_wm_region(C); float fmval[2]; if (!RNA_property_is_set(op->ptr, RNA_struct_find_property(op->ptr, "id_type"))) { UI_view2d_region_to_view(®ion->v2d, event->mval[0], event->mval[1], &fmval[0], &fmval[1]); outliner_id_remap_find_tree_element(C, op, &space_outliner->tree, fmval[1]); } return WM_operator_props_dialog_popup(C, op, 400); } static const EnumPropertyItem *outliner_id_itemf(bContext *C, PointerRNA *ptr, PropertyRNA * /*prop*/, bool *r_free) { if (C == nullptr) { return DummyRNA_NULL_items; } EnumPropertyItem item_tmp = {0}, *item = nullptr; int totitem = 0; int i = 0; short id_type = short(RNA_enum_get(ptr, "id_type")); ID *id = static_cast(which_libbase(CTX_data_main(C), id_type)->first); for (; id; id = static_cast(id->next)) { item_tmp.identifier = item_tmp.name = id->name + 2; item_tmp.value = i++; RNA_enum_item_add(&item, &totitem, &item_tmp); } RNA_enum_item_end(&item, &totitem); *r_free = true; return item; } void OUTLINER_OT_id_remap(wmOperatorType *ot) { PropertyRNA *prop; /* identifiers */ ot->name = "Outliner ID Data Remap"; ot->idname = "OUTLINER_OT_id_remap"; /* callbacks */ ot->invoke = outliner_id_remap_invoke; ot->exec = outliner_id_remap_exec; ot->poll = ED_operator_outliner_active; /* Flags. */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; prop = RNA_def_enum(ot->srna, "id_type", rna_enum_id_type_items, ID_OB, "ID Type", ""); RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_ID_ID); /* Changing ID type wont make sense, would return early with "Invalid old/new ID pair" anyways. */ RNA_def_property_flag(prop, PROP_HIDDEN); prop = RNA_def_enum(ot->srna, "old_id", DummyRNA_NULL_items, 0, "Old ID", "Old ID to replace"); RNA_def_property_enum_funcs_runtime(prop, nullptr, nullptr, outliner_id_itemf); RNA_def_property_flag(prop, (PropertyFlag)(PROP_ENUM_NO_TRANSLATE | PROP_HIDDEN)); ot->prop = RNA_def_enum(ot->srna, "new_id", DummyRNA_NULL_items, 0, "New ID", "New ID to remap all selected IDs' users to"); RNA_def_property_enum_funcs_runtime(ot->prop, nullptr, nullptr, outliner_id_itemf); RNA_def_property_flag(ot->prop, PROP_ENUM_NO_TRANSLATE); } void id_remap_fn(bContext *C, ReportList * /*reports*/, Scene * /*scene*/, TreeElement * /*te*/, TreeStoreElem * /*tsep*/, TreeStoreElem *tselem, void * /*user_data*/) { wmOperatorType *ot = WM_operatortype_find("OUTLINER_OT_id_remap", false); PointerRNA op_props; BLI_assert(tselem->id != nullptr); WM_operator_properties_create_ptr(&op_props, ot); RNA_enum_set(&op_props, "id_type", GS(tselem->id->name)); RNA_enum_set_identifier(C, &op_props, "old_id", tselem->id->name + 2); WM_operator_name_call_ptr(C, ot, WM_OP_INVOKE_DEFAULT, &op_props, nullptr); WM_operator_properties_free(&op_props); } /** \} */ /* -------------------------------------------------------------------- */ /** \name ID Copy Operator * \{ */ static int outliner_id_copy_tag(SpaceOutliner *space_outliner, ListBase *tree) { int num_ids = 0; LISTBASE_FOREACH (TreeElement *, te, tree) { TreeStoreElem *tselem = TREESTORE(te); /* if item is selected and is an ID, tag it as needing to be copied. */ if (tselem->flag & TSE_SELECTED && ELEM(tselem->type, TSE_SOME_ID, TSE_LAYER_COLLECTION)) { ID *id = tselem->id; if (!(id->tag & LIB_TAG_DOIT)) { BKE_copybuffer_copy_tag_ID(tselem->id); num_ids++; } } /* go over sub-tree */ num_ids += outliner_id_copy_tag(space_outliner, &te->subtree); } return num_ids; } static int outliner_id_copy_exec(bContext *C, wmOperator *op) { Main *bmain = CTX_data_main(C); SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); char str[FILE_MAX]; BKE_copybuffer_copy_begin(bmain); const int num_ids = outliner_id_copy_tag(space_outliner, &space_outliner->tree); if (num_ids == 0) { BKE_report(op->reports, RPT_INFO, "No selected data-blocks to copy"); return OPERATOR_CANCELLED; } BLI_path_join(str, sizeof(str), BKE_tempdir_base(), "copybuffer.blend"); BKE_copybuffer_copy_end(bmain, str, op->reports); BKE_reportf(op->reports, RPT_INFO, "Copied %d selected data-block(s)", num_ids); return OPERATOR_FINISHED; } void OUTLINER_OT_id_copy(wmOperatorType *ot) { /* identifiers */ ot->name = "Outliner ID Data Copy"; ot->idname = "OUTLINER_OT_id_copy"; ot->description = "Selected data-blocks are copied to the clipboard"; /* callbacks */ ot->exec = outliner_id_copy_exec; ot->poll = ED_operator_outliner_active; /* Flags, don't need any undo here (this operator does not change anything in Blender data). */ ot->flag = 0; } /** \} */ /* -------------------------------------------------------------------- */ /** \name ID Paste Operator * \{ */ static int outliner_id_paste_exec(bContext *C, wmOperator *op) { char str[FILE_MAX]; const short flag = FILE_AUTOSELECT | FILE_ACTIVE_COLLECTION; BLI_path_join(str, sizeof(str), BKE_tempdir_base(), "copybuffer.blend"); const int num_pasted = BKE_copybuffer_paste(C, str, flag, op->reports, 0); if (num_pasted == 0) { BKE_report(op->reports, RPT_INFO, "No data to paste"); return OPERATOR_CANCELLED; } WM_event_add_notifier(C, NC_WINDOW, nullptr); BKE_reportf(op->reports, RPT_INFO, "%d data-block(s) pasted", num_pasted); return OPERATOR_FINISHED; } void OUTLINER_OT_id_paste(wmOperatorType *ot) { /* identifiers */ ot->name = "Outliner ID Data Paste"; ot->idname = "OUTLINER_OT_id_paste"; ot->description = "Data-blocks from the clipboard are pasted"; /* callbacks */ ot->exec = outliner_id_paste_exec; ot->poll = ED_operator_outliner_active; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Library Relocate Operator * \{ */ static int lib_relocate( bContext *C, TreeElement *te, TreeStoreElem *tselem, wmOperatorType *ot, const bool reload) { PointerRNA op_props; int ret = 0; BLI_assert(te->idcode == ID_LI && tselem->id != nullptr); UNUSED_VARS_NDEBUG(te); WM_operator_properties_create_ptr(&op_props, ot); RNA_string_set(&op_props, "library", tselem->id->name + 2); if (reload) { Library *lib = (Library *)tselem->id; char dir[FILE_MAXDIR], filename[FILE_MAX]; BLI_split_dirfile(lib->filepath_abs, dir, filename, sizeof(dir), sizeof(filename)); printf("%s, %s\n", tselem->id->name, lib->filepath_abs); /* We assume if both paths in lib are not the same then `lib->filepath` was relative. */ RNA_boolean_set( &op_props, "relative_path", BLI_path_cmp(lib->filepath_abs, lib->filepath) != 0); RNA_string_set(&op_props, "directory", dir); RNA_string_set(&op_props, "filename", filename); ret = WM_operator_name_call_ptr(C, ot, WM_OP_EXEC_DEFAULT, &op_props, nullptr); } else { ret = WM_operator_name_call_ptr(C, ot, WM_OP_INVOKE_DEFAULT, &op_props, nullptr); } WM_operator_properties_free(&op_props); return ret; } static int outliner_lib_relocate_invoke_do( bContext *C, ReportList *reports, TreeElement *te, const float mval[2], const bool reload) { if (mval[1] > te->ys && mval[1] < te->ys + UI_UNIT_Y) { TreeStoreElem *tselem = TREESTORE(te); if (te->idcode == ID_LI && tselem->id) { if (((Library *)tselem->id)->parent && !reload) { BKE_reportf(reports, RPT_ERROR_INVALID_INPUT, "Cannot relocate indirectly linked library '%s'", ((Library *)tselem->id)->filepath_abs); return OPERATOR_CANCELLED; } wmOperatorType *ot = WM_operatortype_find(reload ? "WM_OT_lib_reload" : "WM_OT_lib_relocate", false); return lib_relocate(C, te, tselem, ot, reload); } } else { LISTBASE_FOREACH (TreeElement *, te_sub, &te->subtree) { int ret; if ((ret = outliner_lib_relocate_invoke_do(C, reports, te_sub, mval, reload))) { return ret; } } } return 0; } static int outliner_lib_relocate_invoke(bContext *C, wmOperator *op, const wmEvent *event) { ARegion *region = CTX_wm_region(C); SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); float fmval[2]; BLI_assert(region && space_outliner); UI_view2d_region_to_view(®ion->v2d, event->mval[0], event->mval[1], &fmval[0], &fmval[1]); LISTBASE_FOREACH (TreeElement *, te, &space_outliner->tree) { int ret; if ((ret = outliner_lib_relocate_invoke_do(C, op->reports, te, fmval, false))) { return ret; } } return OPERATOR_CANCELLED; } void OUTLINER_OT_lib_relocate(wmOperatorType *ot) { ot->name = "Relocate Library"; ot->idname = "OUTLINER_OT_lib_relocate"; ot->description = "Relocate the library under cursor"; ot->invoke = outliner_lib_relocate_invoke; ot->poll = ED_operator_outliner_active; /* Flags. */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; } void lib_relocate_fn(bContext *C, ReportList * /*reports*/, Scene * /*scene*/, TreeElement *te, TreeStoreElem * /*tsep*/, TreeStoreElem *tselem, void * /*user_data*/) { /* XXX: This does not work with several items * (it is only called once in the end, due to the 'deferred' * file-browser invocation through event system...). */ wmOperatorType *ot = WM_operatortype_find("WM_OT_lib_relocate", false); lib_relocate(C, te, tselem, ot, false); } static int outliner_lib_reload_invoke(bContext *C, wmOperator *op, const wmEvent *event) { ARegion *region = CTX_wm_region(C); SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); float fmval[2]; BLI_assert(region && space_outliner); UI_view2d_region_to_view(®ion->v2d, event->mval[0], event->mval[1], &fmval[0], &fmval[1]); LISTBASE_FOREACH (TreeElement *, te, &space_outliner->tree) { int ret; if ((ret = outliner_lib_relocate_invoke_do(C, op->reports, te, fmval, true))) { return ret; } } return OPERATOR_CANCELLED; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Library Reload Operator * \{ */ void OUTLINER_OT_lib_reload(wmOperatorType *ot) { ot->name = "Reload Library"; ot->idname = "OUTLINER_OT_lib_reload"; ot->description = "Reload the library under cursor"; ot->invoke = outliner_lib_reload_invoke; ot->poll = ED_operator_outliner_active; /* Flags. */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; } void lib_reload_fn(bContext *C, ReportList * /*reports*/, Scene * /*scene*/, TreeElement *te, TreeStoreElem * /*tsep*/, TreeStoreElem *tselem, void * /*user_data*/) { wmOperatorType *ot = WM_operatortype_find("WM_OT_lib_reload", false); lib_relocate(C, te, tselem, ot, true); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Apply Settings Utilities * \{ */ static int outliner_count_levels(ListBase *lb, const int curlevel) { int level = curlevel; LISTBASE_FOREACH (TreeElement *, te, lb) { int lev = outliner_count_levels(&te->subtree, curlevel + 1); if (lev > level) { level = lev; } } return level; } int outliner_flag_is_any_test(ListBase *lb, short flag, const int curlevel) { LISTBASE_FOREACH (TreeElement *, te, lb) { TreeStoreElem *tselem = TREESTORE(te); if (tselem->flag & flag) { return curlevel; } int level = outliner_flag_is_any_test(&te->subtree, flag, curlevel + 1); if (level) { return level; } } return 0; } bool outliner_flag_set(const SpaceOutliner &space_outliner, const short flag, const short set) { return outliner_flag_set(space_outliner.tree, flag, set); } bool outliner_flag_set(const ListBase &lb, const short flag, const short set) { bool changed = false; tree_iterator::all(lb, [&](TreeElement *te) { TreeStoreElem *tselem = TREESTORE(te); bool has_flag = (tselem->flag & flag); if (set == 0) { if (has_flag) { tselem->flag &= ~flag; changed = true; } } else if (!has_flag) { tselem->flag |= flag; changed = true; } }); return changed; } bool outliner_flag_flip(const SpaceOutliner &space_outliner, const short flag) { return outliner_flag_flip(space_outliner.tree, flag); } bool outliner_flag_flip(const ListBase &lb, const short flag) { bool changed = false; tree_iterator::all(lb, [&](TreeElement *te) { TreeStoreElem *tselem = TREESTORE(te); tselem->flag ^= flag; }); return changed; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Toggle Expanded (Outliner) Operator * \{ */ static int outliner_toggle_expanded_exec(bContext *C, wmOperator * /*op*/) { SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); ARegion *region = CTX_wm_region(C); if (outliner_flag_is_any_test(&space_outliner->tree, TSE_CLOSED, 1)) { outliner_flag_set(*space_outliner, TSE_CLOSED, 0); } else { outliner_flag_set(*space_outliner, TSE_CLOSED, 1); } ED_region_tag_redraw(region); return OPERATOR_FINISHED; } void OUTLINER_OT_expanded_toggle(wmOperatorType *ot) { /* identifiers */ ot->name = "Expand/Collapse All"; ot->idname = "OUTLINER_OT_expanded_toggle"; ot->description = "Expand/Collapse all items"; /* callbacks */ ot->exec = outliner_toggle_expanded_exec; ot->poll = ED_operator_outliner_active; /* no undo or registry, UI option */ } /** \} */ /* -------------------------------------------------------------------- */ /** \name Toggle Selected (Outliner) Operator * \{ */ static int outliner_select_all_exec(bContext *C, wmOperator *op) { SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); ARegion *region = CTX_wm_region(C); Scene *scene = CTX_data_scene(C); int action = RNA_enum_get(op->ptr, "action"); if (action == SEL_TOGGLE) { action = outliner_flag_is_any_test(&space_outliner->tree, TSE_SELECTED, 1) ? SEL_DESELECT : SEL_SELECT; } switch (action) { case SEL_SELECT: outliner_flag_set(*space_outliner, TSE_SELECTED, 1); break; case SEL_DESELECT: outliner_flag_set(*space_outliner, TSE_SELECTED, 0); break; case SEL_INVERT: outliner_flag_flip(*space_outliner, TSE_SELECTED); break; } ED_outliner_select_sync_from_outliner(C, space_outliner); DEG_id_tag_update(&scene->id, ID_RECALC_SELECT); WM_event_add_notifier(C, NC_SCENE | ND_OB_SELECT, scene); ED_region_tag_redraw_no_rebuild(region); return OPERATOR_FINISHED; } void OUTLINER_OT_select_all(wmOperatorType *ot) { /* identifiers */ ot->name = "Toggle Selected"; ot->idname = "OUTLINER_OT_select_all"; ot->description = "Toggle the Outliner selection of items"; /* callbacks */ ot->exec = outliner_select_all_exec; ot->poll = ED_operator_outliner_active; /* no undo or registry */ /* rna */ WM_operator_properties_select_all(ot); } /** \} */ /* -------------------------------------------------------------------- */ /** \name View Show Active (Outliner) Operator * \{ */ void outliner_set_coordinates(const ARegion *region, const SpaceOutliner *space_outliner) { int starty = int(region->v2d.tot.ymax) - UI_UNIT_Y; tree_iterator::all_open(*space_outliner, [&](TreeElement *te) { /* store coord and continue, we need coordinates for elements outside view too */ te->xs = 0; te->ys = float(starty); starty -= UI_UNIT_Y; }); } /* return 1 when levels were opened */ static int outliner_open_back(TreeElement *te) { TreeStoreElem *tselem; int retval = 0; for (te = te->parent; te; te = te->parent) { tselem = TREESTORE(te); if (tselem->flag & TSE_CLOSED) { tselem->flag &= ~TSE_CLOSED; retval = 1; } } return retval; } /* Return element representing the active base or bone in the outliner, or NULL if none exists */ static TreeElement *outliner_show_active_get_element(bContext *C, SpaceOutliner *space_outliner, const Scene *scene, ViewLayer *view_layer) { TreeElement *te; BKE_view_layer_synced_ensure(scene, view_layer); Object *obact = BKE_view_layer_active_object_get(view_layer); if (!obact) { return nullptr; } te = outliner_find_id(space_outliner, &space_outliner->tree, &obact->id); if (te != nullptr && obact->type == OB_ARMATURE) { /* traverse down the bone hierarchy in case of armature */ TreeElement *te_obact = te; if (obact->mode & OB_MODE_POSE) { Object *obpose = BKE_object_pose_armature_get(obact); bPoseChannel *pchan = BKE_pose_channel_active(obpose, false); if (pchan) { te = outliner_find_posechannel(&te_obact->subtree, pchan); } } else if (obact->mode & OB_MODE_EDIT) { EditBone *ebone = CTX_data_active_bone(C); if (ebone) { te = outliner_find_editbone(&te_obact->subtree, ebone); } } } return te; } static void outliner_show_active(SpaceOutliner *space_outliner, ARegion *region, TreeElement *te, ID *id) { /* open up tree to active object/bone */ if (TREESTORE(te)->id == id) { if (outliner_open_back(te)) { outliner_set_coordinates(region, space_outliner); } return; } LISTBASE_FOREACH (TreeElement *, ten, &te->subtree) { outliner_show_active(space_outliner, region, ten, id); } } static int outliner_show_active_exec(bContext *C, wmOperator * /*op*/) { SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); const Scene *scene = CTX_data_scene(C); ViewLayer *view_layer = CTX_data_view_layer(C); ARegion *region = CTX_wm_region(C); View2D *v2d = ®ion->v2d; TreeElement *active_element = outliner_show_active_get_element( C, space_outliner, scene, view_layer); if (active_element) { ID *id = TREESTORE(active_element)->id; /* Expand all elements in the outliner with matching ID */ LISTBASE_FOREACH (TreeElement *, te, &space_outliner->tree) { outliner_show_active(space_outliner, region, te, id); } /* Also open back from the active_element (only done for the first found occurrence of ID * though). */ outliner_show_active(space_outliner, region, active_element, id); /* Center view on first element found */ int size_y = BLI_rcti_size_y(&v2d->mask) + 1; int ytop = (active_element->ys + (size_y / 2)); int delta_y = ytop - v2d->cur.ymax; outliner_scroll_view(space_outliner, region, delta_y); } else { return OPERATOR_CANCELLED; } ED_region_tag_redraw_no_rebuild(region); return OPERATOR_FINISHED; } void OUTLINER_OT_show_active(wmOperatorType *ot) { /* identifiers */ ot->name = "Show Active"; ot->idname = "OUTLINER_OT_show_active"; ot->description = "Open up the tree and adjust the view so that the active object is shown centered"; /* callbacks */ ot->exec = outliner_show_active_exec; ot->poll = ED_operator_outliner_active; } /** \} */ /* -------------------------------------------------------------------- */ /** \name View Panning (Outliner) Operator * \{ */ static int outliner_scroll_page_exec(bContext *C, wmOperator *op) { SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); ARegion *region = CTX_wm_region(C); int size_y = BLI_rcti_size_y(®ion->v2d.mask) + 1; bool up = RNA_boolean_get(op->ptr, "up"); if (!up) { size_y = -size_y; } outliner_scroll_view(space_outliner, region, size_y); ED_region_tag_redraw_no_rebuild(region); return OPERATOR_FINISHED; } void OUTLINER_OT_scroll_page(wmOperatorType *ot) { PropertyRNA *prop; /* identifiers */ ot->name = "Scroll Page"; ot->idname = "OUTLINER_OT_scroll_page"; ot->description = "Scroll page up or down"; /* callbacks */ ot->exec = outliner_scroll_page_exec; ot->poll = ED_operator_outliner_active; /* properties */ prop = RNA_def_boolean(ot->srna, "up", false, "Up", "Scroll up one page"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Show One Level Operator * \{ */ /* helper function for Show/Hide one level operator */ static void outliner_openclose_level(ListBase *lb, int curlevel, int level, int open) { LISTBASE_FOREACH (TreeElement *, te, lb) { TreeStoreElem *tselem = TREESTORE(te); if (open) { if (curlevel <= level) { tselem->flag &= ~TSE_CLOSED; } } else { if (curlevel >= level) { tselem->flag |= TSE_CLOSED; } } outliner_openclose_level(&te->subtree, curlevel + 1, level, open); } } static int outliner_one_level_exec(bContext *C, wmOperator *op) { SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); ARegion *region = CTX_wm_region(C); const bool add = RNA_boolean_get(op->ptr, "open"); int level; level = outliner_flag_is_any_test(&space_outliner->tree, TSE_CLOSED, 1); if (add == 1) { if (level) { outliner_openclose_level(&space_outliner->tree, 1, level, 1); } } else { if (level == 0) { level = outliner_count_levels(&space_outliner->tree, 0); } if (level) { outliner_openclose_level(&space_outliner->tree, 1, level - 1, 0); } } ED_region_tag_redraw(region); return OPERATOR_FINISHED; } void OUTLINER_OT_show_one_level(wmOperatorType *ot) { PropertyRNA *prop; /* identifiers */ ot->name = "Show/Hide One Level"; ot->idname = "OUTLINER_OT_show_one_level"; ot->description = "Expand/collapse all entries by one level"; /* callbacks */ ot->exec = outliner_one_level_exec; ot->poll = ED_operator_outliner_active; /* no undo or registry, UI option */ /* properties */ prop = RNA_def_boolean(ot->srna, "open", true, "Open", "Expand all entries one level deep"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Show Hierarchy Operator * \{ */ /** * Helper function for #tree_element_shwo_hierarchy() - * recursively checks whether subtrees have any objects. */ static int subtree_has_objects(ListBase *lb) { LISTBASE_FOREACH (TreeElement *, te, lb) { TreeStoreElem *tselem = TREESTORE(te); if ((tselem->type == TSE_SOME_ID) && (te->idcode == ID_OB)) { return 1; } if (subtree_has_objects(&te->subtree)) { return 1; } } return 0; } /* Helper function for Show Hierarchy operator */ static void tree_element_show_hierarchy(Scene *scene, SpaceOutliner *space_outliner) { /* open all object elems, close others */ tree_iterator::all_open(*space_outliner, [&](TreeElement *te) { TreeStoreElem *tselem = TREESTORE(te); if (ELEM(tselem->type, TSE_SOME_ID, TSE_SCENE_OBJECTS_BASE, TSE_VIEW_COLLECTION_BASE, TSE_LAYER_COLLECTION)) { if (te->idcode == ID_SCE) { if (tselem->id != (ID *)scene) { tselem->flag |= TSE_CLOSED; } else { tselem->flag &= ~TSE_CLOSED; } } else if (te->idcode == ID_OB) { if (subtree_has_objects(&te->subtree)) { tselem->flag &= ~TSE_CLOSED; } else { tselem->flag |= TSE_CLOSED; } } } else { tselem->flag |= TSE_CLOSED; } }); } /* show entire object level hierarchy */ static int outliner_show_hierarchy_exec(bContext *C, wmOperator * /*op*/) { SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); ARegion *region = CTX_wm_region(C); Scene *scene = CTX_data_scene(C); /* recursively open/close levels */ tree_element_show_hierarchy(scene, space_outliner); ED_region_tag_redraw(region); return OPERATOR_FINISHED; } void OUTLINER_OT_show_hierarchy(wmOperatorType *ot) { /* identifiers */ ot->name = "Show Hierarchy"; ot->idname = "OUTLINER_OT_show_hierarchy"; ot->description = "Open all object entries and close all others"; /* callbacks */ ot->exec = outliner_show_hierarchy_exec; ot->poll = ED_operator_outliner_active; /* TODO: shouldn't be allowed in RNA views... */ /* no undo or registry, UI option */ } /** \} */ /* -------------------------------------------------------------------- */ /** \name Animation Internal Utilities * \{ */ /** * Specialized poll callback for these operators to work in data-blocks view only. */ static bool ed_operator_outliner_datablocks_active(bContext *C) { ScrArea *area = CTX_wm_area(C); if ((area) && (area->spacetype == SPACE_OUTLINER)) { SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); return (space_outliner->outlinevis == SO_DATA_API); } return false; } /* Helper func to extract an RNA path from selected tree element * NOTE: the caller must zero-out all values of the pointers that it passes here first, as * this function does not do that yet */ static void tree_element_to_path(TreeElement *te, TreeStoreElem *tselem, ID **id, char **path, int *array_index, short *flag, short * /*groupmode*/) { ListBase hierarchy = {nullptr, nullptr}; char *newpath = nullptr; /* optimize tricks: * - Don't do anything if the selected item is a 'struct', but arrays are allowed */ if (tselem->type == TSE_RNA_STRUCT) { return; } /* Overview of Algorithm: * 1. Go up the chain of parents until we find the 'root', taking note of the * levels encountered in reverse-order (i.e. items are added to the start of the list * for more convenient looping later) * 2. Walk down the chain, adding from the first ID encountered * (which will become the 'ID' for the KeyingSet Path), and build a * path as we step through the chain */ /* step 1: flatten out hierarchy of parents into a flat chain */ for (TreeElement *tem = te->parent; tem; tem = tem->parent) { LinkData *ld = MEM_cnew("LinkData for tree_element_to_path()"); ld->data = tem; BLI_addhead(&hierarchy, ld); } /* step 2: step down hierarchy building the path * (NOTE: addhead in previous loop was needed so that we can loop like this) */ LISTBASE_FOREACH (LinkData *, ld, &hierarchy) { /* get data */ TreeElement *tem = (TreeElement *)ld->data; TreeElementRNACommon *tem_rna = tree_element_cast(tem); PointerRNA ptr = tem_rna->getPointerRNA(); /* check if we're looking for first ID, or appending to path */ if (*id) { /* just 'append' property to path * - to prevent memory leaks, we must write to newpath not path, * then free old path + swap them. */ if (TreeElementRNAProperty *tem_rna_prop = tree_element_cast(tem)) { PropertyRNA *prop = tem_rna_prop->getPropertyRNA(); if (RNA_property_type(prop) == PROP_POINTER) { /* for pointer we just append property name */ newpath = RNA_path_append(*path, &ptr, prop, 0, nullptr); } else if (RNA_property_type(prop) == PROP_COLLECTION) { char buf[128], *name; TreeElement *temnext = (TreeElement *)(ld->next->data); PointerRNA nextptr = tree_element_cast(temnext)->getPointerRNA(); name = RNA_struct_name_get_alloc(&nextptr, buf, sizeof(buf), nullptr); if (name) { /* if possible, use name as a key in the path */ newpath = RNA_path_append(*path, nullptr, prop, 0, name); if (name != buf) { MEM_freeN(name); } } else { /* otherwise use index */ int index = 0; LISTBASE_FOREACH (TreeElement *, temsub, &tem->subtree) { if (temsub == temnext) { break; } index++; } newpath = RNA_path_append(*path, nullptr, prop, index, nullptr); } ld = ld->next; } } if (newpath) { if (*path) { MEM_freeN(*path); } *path = newpath; newpath = nullptr; } } else { /* no ID, so check if entry is RNA-struct, * and if that RNA-struct is an ID datablock to extract info from. */ if (tree_element_cast(tem)) { /* ptr->data not ptr->owner_id seems to be the one we want, * since ptr->data is sometimes the owner of this ID? */ if (RNA_struct_is_ID(ptr.type)) { *id = static_cast(ptr.data); /* clear path */ if (*path) { MEM_freeN(*path); path = nullptr; } } } } } /* step 3: if we've got an ID, add the current item to the path */ if (*id) { /* add the active property to the path */ PropertyRNA *prop = tree_element_cast(te)->getPropertyRNA(); /* array checks */ if (tselem->type == TSE_RNA_ARRAY_ELEM) { /* item is part of an array, so must set the array_index */ *array_index = te->index; } else if (RNA_property_array_check(prop)) { /* entire array was selected, so keyframe all */ *flag |= KSP_FLAG_WHOLE_ARRAY; } /* path */ newpath = RNA_path_append(*path, nullptr, prop, 0, nullptr); if (*path) { MEM_freeN(*path); } *path = newpath; } /* free temp data */ BLI_freelistN(&hierarchy); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Driver Internal Utilities * \{ */ /** * Driver Operations * * These operators are only available in data-browser mode for now, * as they depend on having RNA paths and/or hierarchies available. */ enum { DRIVERS_EDITMODE_ADD = 0, DRIVERS_EDITMODE_REMOVE, } /*eDrivers_EditModes*/; /* Iterate over tree, finding and working on selected items */ static void do_outliner_drivers_editop(SpaceOutliner *space_outliner, ReportList *reports, short mode) { tree_iterator::all_open(*space_outliner, [&](TreeElement *te) { TreeStoreElem *tselem = TREESTORE(te); /* if item is selected, perform operation */ if (!(tselem->flag & TSE_SELECTED)) { return; } ID *id = nullptr; char *path = nullptr; int array_index = 0; short flag = 0; short groupmode = KSP_GROUP_KSNAME; TreeElementRNACommon *te_rna = tree_element_cast(te); PointerRNA ptr = te_rna ? te_rna->getPointerRNA() : PointerRNA_NULL; PropertyRNA *prop = te_rna ? te_rna->getPropertyRNA() : nullptr; /* check if RNA-property described by this selected element is an animatable prop */ if (prop && RNA_property_animateable(&ptr, prop)) { /* get id + path + index info from the selected element */ tree_element_to_path(te, tselem, &id, &path, &array_index, &flag, &groupmode); } /* only if ID and path were set, should we perform any actions */ if (id && path) { short dflags = CREATEDRIVER_WITH_DEFAULT_DVAR; int arraylen = 1; /* array checks */ if (flag & KSP_FLAG_WHOLE_ARRAY) { /* entire array was selected, so add drivers for all */ arraylen = RNA_property_array_length(&ptr, prop); } else { arraylen = array_index; } /* we should do at least one step */ if (arraylen == array_index) { arraylen++; } /* for each array element we should affect, add driver */ for (; array_index < arraylen; array_index++) { /* action depends on mode */ switch (mode) { case DRIVERS_EDITMODE_ADD: { /* add a new driver with the information obtained (only if valid) */ ANIM_add_driver(reports, id, path, array_index, dflags, DRIVER_TYPE_PYTHON); break; } case DRIVERS_EDITMODE_REMOVE: { /* remove driver matching the information obtained (only if valid) */ ANIM_remove_driver(reports, id, path, array_index, dflags); break; } } } /* free path, since it had to be generated */ MEM_freeN(path); } }); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Driver Add Operator * \{ */ static int outliner_drivers_addsel_exec(bContext *C, wmOperator *op) { SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); /* check for invalid states */ if (space_outliner == nullptr) { return OPERATOR_CANCELLED; } /* recursively go into tree, adding selected items */ do_outliner_drivers_editop(space_outliner, op->reports, DRIVERS_EDITMODE_ADD); /* send notifiers */ WM_event_add_notifier(C, NC_ANIMATION | ND_FCURVES_ORDER, nullptr); /* XXX */ return OPERATOR_FINISHED; } void OUTLINER_OT_drivers_add_selected(wmOperatorType *ot) { /* api callbacks */ ot->idname = "OUTLINER_OT_drivers_add_selected"; ot->name = "Add Drivers for Selected"; ot->description = "Add drivers to selected items"; /* api callbacks */ ot->exec = outliner_drivers_addsel_exec; ot->poll = ed_operator_outliner_datablocks_active; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Driver Remove Operator * \{ */ static int outliner_drivers_deletesel_exec(bContext *C, wmOperator *op) { SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); /* check for invalid states */ if (space_outliner == nullptr) { return OPERATOR_CANCELLED; } /* recursively go into tree, adding selected items */ do_outliner_drivers_editop(space_outliner, op->reports, DRIVERS_EDITMODE_REMOVE); /* send notifiers */ WM_event_add_notifier(C, ND_KEYS, nullptr); /* XXX */ return OPERATOR_FINISHED; } void OUTLINER_OT_drivers_delete_selected(wmOperatorType *ot) { /* identifiers */ ot->idname = "OUTLINER_OT_drivers_delete_selected"; ot->name = "Delete Drivers for Selected"; ot->description = "Delete drivers assigned to selected items"; /* api callbacks */ ot->exec = outliner_drivers_deletesel_exec; ot->poll = ed_operator_outliner_datablocks_active; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Keying-Set Internal Utilities * \{ */ /** * Keying-Set Operations * * These operators are only available in data-browser mode for now, as * they depend on having RNA paths and/or hierarchies available. */ enum { KEYINGSET_EDITMODE_ADD = 0, KEYINGSET_EDITMODE_REMOVE, } /*eKeyingSet_EditModes*/; /* Find the 'active' KeyingSet, and add if not found (if adding is allowed). */ /* TODO: should this be an API func? */ static KeyingSet *verify_active_keyingset(Scene *scene, short add) { KeyingSet *ks = nullptr; /* sanity check */ if (scene == nullptr) { return nullptr; } /* try to find one from scene */ if (scene->active_keyingset > 0) { ks = static_cast(BLI_findlink(&scene->keyingsets, scene->active_keyingset - 1)); } /* Add if none found */ /* XXX the default settings have yet to evolve. */ if ((add) && (ks == nullptr)) { ks = BKE_keyingset_add(&scene->keyingsets, nullptr, nullptr, KEYINGSET_ABSOLUTE, 0); scene->active_keyingset = BLI_listbase_count(&scene->keyingsets); } return ks; } /* Iterate over tree, finding and working on selected items */ static void do_outliner_keyingset_editop(SpaceOutliner *space_outliner, KeyingSet *ks, const short mode) { tree_iterator::all_open(*space_outliner, [&](TreeElement *te) { TreeStoreElem *tselem = TREESTORE(te); /* if item is selected, perform operation */ if (!(tselem->flag & TSE_SELECTED)) { return; } ID *id = nullptr; char *path = nullptr; int array_index = 0; short flag = 0; short groupmode = KSP_GROUP_KSNAME; /* check if RNA-property described by this selected element is an animatable prop */ const TreeElementRNACommon *te_rna = tree_element_cast(te); PointerRNA ptr = te_rna->getPointerRNA(); if (te_rna && te_rna->getPropertyRNA() && RNA_property_animateable(&ptr, te_rna->getPropertyRNA())) { /* get id + path + index info from the selected element */ tree_element_to_path(te, tselem, &id, &path, &array_index, &flag, &groupmode); } /* only if ID and path were set, should we perform any actions */ if (id && path) { /* action depends on mode */ switch (mode) { case KEYINGSET_EDITMODE_ADD: { /* add a new path with the information obtained (only if valid) */ /* TODO: what do we do with group name? * for now, we don't supply one, and just let this use the KeyingSet name */ BKE_keyingset_add_path(ks, id, nullptr, path, array_index, flag, groupmode); ks->active_path = BLI_listbase_count(&ks->paths); break; } case KEYINGSET_EDITMODE_REMOVE: { /* find the relevant path, then remove it from the KeyingSet */ KS_Path *ksp = BKE_keyingset_find_path(ks, id, nullptr, path, array_index, groupmode); if (ksp) { /* free path's data */ BKE_keyingset_free_path(ks, ksp); ks->active_path = 0; } break; } } /* free path, since it had to be generated */ MEM_freeN(path); } }); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Keying-Set Add Operator * \{ */ static int outliner_keyingset_additems_exec(bContext *C, wmOperator *op) { SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); Scene *scene = CTX_data_scene(C); KeyingSet *ks = verify_active_keyingset(scene, 1); /* check for invalid states */ if (ks == nullptr) { BKE_report(op->reports, RPT_ERROR, "Operation requires an active keying set"); return OPERATOR_CANCELLED; } if (space_outliner == nullptr) { return OPERATOR_CANCELLED; } /* recursively go into tree, adding selected items */ do_outliner_keyingset_editop(space_outliner, ks, KEYINGSET_EDITMODE_ADD); /* send notifiers */ WM_event_add_notifier(C, NC_SCENE | ND_KEYINGSET, nullptr); return OPERATOR_FINISHED; } void OUTLINER_OT_keyingset_add_selected(wmOperatorType *ot) { /* identifiers */ ot->idname = "OUTLINER_OT_keyingset_add_selected"; ot->name = "Keying Set Add Selected"; ot->description = "Add selected items (blue-gray rows) to active Keying Set"; /* api callbacks */ ot->exec = outliner_keyingset_additems_exec; ot->poll = ed_operator_outliner_datablocks_active; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Keying-Set Remove Operator * \{ */ static int outliner_keyingset_removeitems_exec(bContext *C, wmOperator * /*op*/) { SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); Scene *scene = CTX_data_scene(C); KeyingSet *ks = verify_active_keyingset(scene, 1); /* check for invalid states */ if (space_outliner == nullptr) { return OPERATOR_CANCELLED; } /* recursively go into tree, adding selected items */ do_outliner_keyingset_editop(space_outliner, ks, KEYINGSET_EDITMODE_REMOVE); /* send notifiers */ WM_event_add_notifier(C, NC_SCENE | ND_KEYINGSET, nullptr); return OPERATOR_FINISHED; } void OUTLINER_OT_keyingset_remove_selected(wmOperatorType *ot) { /* identifiers */ ot->idname = "OUTLINER_OT_keyingset_remove_selected"; ot->name = "Keying Set Remove Selected"; ot->description = "Remove selected items (blue-gray rows) from active Keying Set"; /* api callbacks */ ot->exec = outliner_keyingset_removeitems_exec; ot->poll = ed_operator_outliner_datablocks_active; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Purge Orphan Data-Blocks Operator * \{ */ static bool ed_operator_outliner_id_orphans_active(bContext *C) { ScrArea *area = CTX_wm_area(C); if (area != nullptr && area->spacetype == SPACE_OUTLINER) { SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); return (space_outliner->outlinevis == SO_ID_ORPHANS); } return true; } static int outliner_orphans_purge_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/) { Main *bmain = CTX_data_main(C); int num_tagged[INDEX_ID_MAX] = {0}; const bool do_local_ids = RNA_boolean_get(op->ptr, "do_local_ids"); const bool do_linked_ids = RNA_boolean_get(op->ptr, "do_linked_ids"); const bool do_recursive_cleanup = RNA_boolean_get(op->ptr, "do_recursive"); /* Tag all IDs to delete. */ BKE_lib_query_unused_ids_tag( bmain, LIB_TAG_DOIT, do_local_ids, do_linked_ids, do_recursive_cleanup, num_tagged); RNA_int_set(op->ptr, "num_deleted", num_tagged[INDEX_ID_NULL]); if (num_tagged[INDEX_ID_NULL] == 0) { BKE_report(op->reports, RPT_INFO, "No orphaned data-blocks to purge"); return OPERATOR_CANCELLED; } DynStr *dyn_str = BLI_dynstr_new(); BLI_dynstr_appendf(dyn_str, "Purging %d unused data-blocks (", num_tagged[INDEX_ID_NULL]); bool is_first = true; for (int i = 0; i < INDEX_ID_MAX - 2; i++) { if (num_tagged[i] != 0) { if (!is_first) { BLI_dynstr_append(dyn_str, ", "); } else { is_first = false; } BLI_dynstr_appendf(dyn_str, "%d %s", num_tagged[i], TIP_(BKE_idtype_idcode_to_name_plural(BKE_idtype_idcode_from_index(i)))); } } BLI_dynstr_append(dyn_str, TIP_("). Click here to proceed...")); char *message = BLI_dynstr_get_cstring(dyn_str); int ret = WM_operator_confirm_message(C, op, message); MEM_freeN(message); BLI_dynstr_free(dyn_str); return ret; } static int outliner_orphans_purge_exec(bContext *C, wmOperator *op) { Main *bmain = CTX_data_main(C); ScrArea *area = CTX_wm_area(C); SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); int num_tagged[INDEX_ID_MAX] = {0}; if ((num_tagged[INDEX_ID_NULL] = RNA_int_get(op->ptr, "num_deleted")) == 0) { const bool do_local_ids = RNA_boolean_get(op->ptr, "do_local_ids"); const bool do_linked_ids = RNA_boolean_get(op->ptr, "do_linked_ids"); const bool do_recursive_cleanup = RNA_boolean_get(op->ptr, "do_recursive"); /* Tag all IDs to delete. */ BKE_lib_query_unused_ids_tag( bmain, LIB_TAG_DOIT, do_local_ids, do_linked_ids, do_recursive_cleanup, num_tagged); if (num_tagged[INDEX_ID_NULL] == 0) { BKE_report(op->reports, RPT_INFO, "No orphaned data-blocks to purge"); return OPERATOR_CANCELLED; } } BKE_id_multi_tagged_delete(bmain); BKE_reportf(op->reports, RPT_INFO, "Deleted %d data-block(s)", num_tagged[INDEX_ID_NULL]); /* XXX: tree management normally happens from draw_outliner(), but when * you're clicking to fast on Delete object from context menu in * outliner several mouse events can be handled in one cycle without * handling notifiers/redraw which leads to deleting the same object twice. * cleanup tree here to prevent such cases. */ if ((area != nullptr) && (area->spacetype == SPACE_OUTLINER)) { outliner_cleanup_tree(space_outliner); } DEG_relations_tag_update(bmain); WM_event_add_notifier(C, NC_ID | NA_REMOVED, nullptr); /* Force full redraw of the UI. */ WM_main_add_notifier(NC_WINDOW, nullptr); return OPERATOR_FINISHED; } void OUTLINER_OT_orphans_purge(wmOperatorType *ot) { /* identifiers */ ot->idname = "OUTLINER_OT_orphans_purge"; ot->name = "Purge All"; ot->description = "Clear all orphaned data-blocks without any users from the file"; /* callbacks */ ot->invoke = outliner_orphans_purge_invoke; ot->exec = outliner_orphans_purge_exec; ot->poll = ed_operator_outliner_id_orphans_active; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; /* properties */ PropertyRNA *prop = RNA_def_int(ot->srna, "num_deleted", 0, 0, INT_MAX, "", "", 0, INT_MAX); RNA_def_property_flag(prop, (PropertyFlag)(PROP_SKIP_SAVE | PROP_HIDDEN)); RNA_def_boolean(ot->srna, "do_local_ids", true, "Local Data-blocks", "Include unused local data-blocks into deletion"); RNA_def_boolean(ot->srna, "do_linked_ids", true, "Linked Data-blocks", "Include unused linked data-blocks into deletion"); RNA_def_boolean(ot->srna, "do_recursive", false, "Recursive Delete", "Recursively check for indirectly unused data-blocks, ensuring that no orphaned " "data-blocks remain after execution"); } /** \} */ } // namespace blender::ed::outliner