diff options
Diffstat (limited to 'source/blender/editors/space_outliner')
5 files changed, 511 insertions, 15 deletions
diff --git a/source/blender/editors/space_outliner/outliner_dragdrop.c b/source/blender/editors/space_outliner/outliner_dragdrop.c index 46a5f90f6c2..58f6f82c80d 100644 --- a/source/blender/editors/space_outliner/outliner_dragdrop.c +++ b/source/blender/editors/space_outliner/outliner_dragdrop.c @@ -26,7 +26,9 @@ #include "MEM_guardedalloc.h" #include "DNA_collection_types.h" +#include "DNA_constraint_types.h" #include "DNA_material_types.h" +#include "DNA_modifier_types.h" #include "DNA_object_types.h" #include "DNA_space_types.h" @@ -36,6 +38,7 @@ #include "BLT_translation.h" #include "BKE_collection.h" +#include "BKE_constraint.h" #include "BKE_context.h" #include "BKE_layer.h" #include "BKE_lib_id.h" @@ -44,6 +47,7 @@ #include "BKE_object.h" #include "BKE_report.h" #include "BKE_scene.h" +#include "BKE_shader_fx.h" #include "DEG_depsgraph.h" #include "DEG_depsgraph_build.h" @@ -65,6 +69,8 @@ #include "outliner_intern.h" +static Collection *collection_parent_from_ID(ID *id); + /* ******************** Drop Target Find *********************** */ static TreeElement *outliner_dropzone_element(TreeElement *te, @@ -146,7 +152,8 @@ static TreeElement *outliner_drop_insert_find(bContext *C, const float margin = UI_UNIT_Y * (1.0f / 4); if (view_mval[1] < (te_hovered->ys + margin)) { - if (TSELEM_OPEN(TREESTORE(te_hovered), space_outliner)) { + if (TSELEM_OPEN(TREESTORE(te_hovered), space_outliner) && + !BLI_listbase_is_empty(&te_hovered->subtree)) { /* inserting after a open item means we insert into it, but as first child */ if (BLI_listbase_is_empty(&te_hovered->subtree)) { *r_insert_type = TE_INSERT_INTO; @@ -183,20 +190,37 @@ static TreeElement *outliner_drop_insert_find(bContext *C, return NULL; } -static Collection *outliner_collection_from_tree_element_and_parents(TreeElement *te, - TreeElement **r_te) +typedef bool (*CheckTypeFn)(TreeElement *te); + +static TreeElement *outliner_data_from_tree_element_and_parents(CheckTypeFn check_type, + TreeElement *te) { while (te != NULL) { - Collection *collection = outliner_collection_from_tree_element(te); - if (collection) { - *r_te = te; - return collection; + if (check_type(te)) { + return te; } te = te->parent; } return NULL; } +static bool is_collection_element(TreeElement *te) +{ + return outliner_is_collection_tree_element(te); +} + +static bool is_object_element(TreeElement *te) +{ + TreeStoreElem *tselem = TREESTORE(te); + return tselem->type == 0 && te->idcode == ID_OB; +} + +static bool is_pchan_element(TreeElement *te) +{ + TreeStoreElem *tselem = TREESTORE(te); + return tselem->type == TSE_POSE_CHANNEL; +} + static TreeElement *outliner_drop_insert_collection_find(bContext *C, const wmEvent *event, TreeElementInsertType *r_insert_type) @@ -206,11 +230,12 @@ static TreeElement *outliner_drop_insert_collection_find(bContext *C, return NULL; } - TreeElement *collection_te; - Collection *collection = outliner_collection_from_tree_element_and_parents(te, &collection_te); - if (!collection) { + TreeElement *collection_te = outliner_data_from_tree_element_and_parents(is_collection_element, + te); + if (!collection_te) { return NULL; } + Collection *collection = outliner_collection_from_tree_element(collection_te); if (collection_te != te) { *r_insert_type = TE_INSERT_INTO; @@ -224,6 +249,30 @@ static TreeElement *outliner_drop_insert_collection_find(bContext *C, return collection_te; } +static int outliner_get_insert_index(TreeElement *drag_te, + TreeElement *drop_te, + TreeElementInsertType insert_type, + ListBase *listbase) +{ + /* Find the element to insert after. NULL is the start of the list. */ + if (drag_te->index < drop_te->index) { + if (insert_type == TE_INSERT_BEFORE) { + drop_te = drop_te->prev; + } + } + else { + if (insert_type == TE_INSERT_AFTER) { + drop_te = drop_te->next; + } + } + + if (drop_te == NULL) { + return 0; + } + + return BLI_findindex(listbase, drop_te->directdata); +} + /* ******************** Parent Drop Operator *********************** */ static bool parent_drop_allowed(TreeElement *te, Object *potential_child) @@ -616,6 +665,413 @@ void OUTLINER_OT_material_drop(wmOperatorType *ot) ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO | OPTYPE_INTERNAL; } +/* ******************** Data Stack Drop Operator *********************** */ + +/* A generic operator to allow drag and drop for modifiers, constraints, + * and shader effects which all share the same UI stack layout. + * + * The following operations are allowed: + * - Reordering within an object. + * - Copying a single modifier/constraint/effect to another object. + * - Copying (linking) an object's modifiers/constraints/effects to another. */ + +typedef enum eDataStackDropAction { + DATA_STACK_DROP_REORDER, + DATA_STACK_DROP_COPY, + DATA_STACK_DROP_LINK, +} eDataStackDropAction; + +typedef struct StackDropData { + Object *ob_parent; + bPoseChannel *pchan_parent; + TreeStoreElem *drag_tselem; + void *drag_directdata; + int drag_index; + + eDataStackDropAction drop_action; + TreeElement *drop_te; + TreeElementInsertType insert_type; +} StackDropData; + +static void datastack_drop_data_init(wmDrag *drag, + Object *ob, + bPoseChannel *pchan, + TreeElement *te, + TreeStoreElem *tselem, + void *directdata) +{ + StackDropData *drop_data = MEM_callocN(sizeof(*drop_data), "datastack drop data"); + + drop_data->ob_parent = ob; + drop_data->pchan_parent = pchan; + drop_data->drag_tselem = tselem; + drop_data->drag_directdata = directdata; + drop_data->drag_index = te->index; + + drag->poin = drop_data; + drag->flags |= WM_DRAG_FREE_DATA; +} + +static bool datastack_drop_init(bContext *C, const wmEvent *event, StackDropData *drop_data) +{ + if (!ELEM(drop_data->drag_tselem->type, + TSE_MODIFIER, + TSE_MODIFIER_BASE, + TSE_CONSTRAINT, + TSE_CONSTRAINT_BASE, + TSE_GPENCIL_EFFECT, + TSE_GPENCIL_EFFECT_BASE)) { + return false; + } + + TreeElement *te_target = outliner_drop_insert_find(C, event, &drop_data->insert_type); + if (!te_target) { + return false; + } + TreeStoreElem *tselem_target = TREESTORE(te_target); + + if (drop_data->drag_tselem == tselem_target) { + return false; + } + + Object *ob = NULL; + TreeElement *object_te = outliner_data_from_tree_element_and_parents(is_object_element, + te_target); + if (object_te) { + ob = (Object *)TREESTORE(object_te)->id; + } + + bPoseChannel *pchan = NULL; + TreeElement *pchan_te = outliner_data_from_tree_element_and_parents(is_pchan_element, te_target); + if (pchan_te) { + pchan = (bPoseChannel *)pchan_te->directdata; + } + if (pchan) { + ob = NULL; + } + + if (ob && ID_IS_LINKED(&ob->id)) { + return false; + } + + /* Drag a base for linking. */ + if (ELEM(drop_data->drag_tselem->type, + TSE_MODIFIER_BASE, + TSE_CONSTRAINT_BASE, + TSE_GPENCIL_EFFECT_BASE)) { + drop_data->insert_type = TE_INSERT_INTO; + drop_data->drop_action = DATA_STACK_DROP_LINK; + + if (pchan && pchan != drop_data->pchan_parent) { + drop_data->drop_te = pchan_te; + tselem_target = TREESTORE(pchan_te); + } + else if (ob && ob != drop_data->ob_parent) { + drop_data->drop_te = object_te; + tselem_target = TREESTORE(object_te); + } + else { + return false; + } + } + else if (ob || pchan) { + /* Drag a single item. */ + if (pchan && pchan != drop_data->pchan_parent) { + drop_data->insert_type = TE_INSERT_INTO; + drop_data->drop_action = DATA_STACK_DROP_COPY; + drop_data->drop_te = pchan_te; + tselem_target = TREESTORE(pchan_te); + } + else if (ob && ob != drop_data->ob_parent) { + drop_data->insert_type = TE_INSERT_INTO; + drop_data->drop_action = DATA_STACK_DROP_COPY; + drop_data->drop_te = object_te; + tselem_target = TREESTORE(object_te); + } + else if (tselem_target->type == drop_data->drag_tselem->type) { + if (drop_data->insert_type == TE_INSERT_INTO) { + return false; + } + drop_data->drop_action = DATA_STACK_DROP_REORDER; + drop_data->drop_te = te_target; + } + else { + return false; + } + } + else { + return false; + } + + return true; +} + +/* Ensure that grease pencil and object data remain separate. */ +static bool datastack_drop_are_types_valid(StackDropData *drop_data) +{ + TreeStoreElem *tselem = TREESTORE(drop_data->drop_te); + Object *ob_parent = drop_data->ob_parent; + Object *ob_dst = (Object *)tselem->id; + + /* Don't allow data to be moved between objects and bones. */ + if (tselem->type == TSE_CONSTRAINT) { + } + else if ((drop_data->pchan_parent && tselem->type != TSE_POSE_CHANNEL) || + (!drop_data->pchan_parent && tselem->type == TSE_POSE_CHANNEL)) { + return false; + } + + switch (drop_data->drag_tselem->type) { + case TSE_MODIFIER_BASE: + case TSE_MODIFIER: + if (ob_parent->type == OB_GPENCIL) { + return ob_dst->type == OB_GPENCIL; + } + else if (ob_parent->type != OB_GPENCIL) { + return ob_dst->type != OB_GPENCIL; + } + break; + case TSE_CONSTRAINT_BASE: + case TSE_CONSTRAINT: + + break; + case TSE_GPENCIL_EFFECT_BASE: + case TSE_GPENCIL_EFFECT: + return ob_parent->type == OB_GPENCIL && ob_dst->type == OB_GPENCIL; + break; + } + + return true; +} + +static bool datastack_drop_poll(bContext *C, + wmDrag *drag, + const wmEvent *event, + const char **r_tooltip) +{ + SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); + ARegion *region = CTX_wm_region(C); + bool changed = outliner_flag_set(&space_outliner->tree, TSE_HIGHLIGHTED | TSE_DRAG_ANY, false); + + StackDropData *drop_data = drag->poin; + if (!drop_data) { + return false; + } + + if (!datastack_drop_init(C, event, drop_data)) { + return false; + } + + if (!datastack_drop_are_types_valid(drop_data)) { + return false; + } + + TreeStoreElem *tselem_target = TREESTORE(drop_data->drop_te); + switch (drop_data->insert_type) { + case TE_INSERT_BEFORE: + tselem_target->flag |= TSE_DRAG_BEFORE; + break; + case TE_INSERT_AFTER: + tselem_target->flag |= TSE_DRAG_AFTER; + break; + case TE_INSERT_INTO: + tselem_target->flag |= TSE_DRAG_INTO; + break; + } + + switch (drop_data->drop_action) { + case DATA_STACK_DROP_REORDER: + *r_tooltip = TIP_("Reorder"); + break; + case DATA_STACK_DROP_COPY: + if (drop_data->pchan_parent) { + *r_tooltip = TIP_("Copy to bone"); + } + else { + *r_tooltip = TIP_("Copy to object"); + } + break; + case DATA_STACK_DROP_LINK: + if (drop_data->pchan_parent) { + *r_tooltip = TIP_("Link all to bone"); + } + else { + *r_tooltip = TIP_("Link all to object"); + } + break; + } + + if (changed) { + ED_region_tag_redraw_no_rebuild(region); + } + + return true; +} + +static void datastack_drop_link(bContext *C, StackDropData *drop_data) +{ + Main *bmain = CTX_data_main(C); + TreeStoreElem *tselem = TREESTORE(drop_data->drop_te); + Object *ob_dst = (Object *)tselem->id; + + switch (drop_data->drag_tselem->type) { + case TSE_MODIFIER_BASE: + ED_object_modifier_link(C, ob_dst, drop_data->ob_parent); + break; + case TSE_CONSTRAINT_BASE: { + ListBase *src; + + if (drop_data->pchan_parent) { + src = &drop_data->pchan_parent->constraints; + } + else { + src = &drop_data->ob_parent->constraints; + } + + ListBase *dst; + if (tselem->type == TSE_POSE_CHANNEL) { + bPoseChannel *pchan = (bPoseChannel *)drop_data->drop_te->directdata; + dst = &pchan->constraints; + } + else { + dst = &ob_dst->constraints; + } + + ED_object_constraint_link(bmain, ob_dst, dst, src); + break; + } + case TSE_GPENCIL_EFFECT_BASE: + if (ob_dst->type != OB_GPENCIL) { + return; + } + + ED_object_shaderfx_link(ob_dst, drop_data->ob_parent); + break; + } +} + +static void datastack_drop_copy(bContext *C, StackDropData *drop_data) +{ + Main *bmain = CTX_data_main(C); + + TreeStoreElem *tselem = TREESTORE(drop_data->drop_te); + Object *ob_dst = (Object *)tselem->id; + + switch (drop_data->drag_tselem->type) { + case TSE_MODIFIER: + if (drop_data->ob_parent->type == OB_GPENCIL && ob_dst->type == OB_GPENCIL) { + ED_object_gpencil_modifier_copy_to_object(ob_dst, drop_data->drag_directdata); + } + else if (drop_data->ob_parent->type != OB_GPENCIL && ob_dst->type != OB_GPENCIL) { + ED_object_modifier_copy_to_object( + ob_dst, drop_data->ob_parent, drop_data->drag_directdata); + } + break; + case TSE_CONSTRAINT: + if (tselem->type == TSE_POSE_CHANNEL) { + ED_object_constraint_copy_for_pose( + bmain, ob_dst, drop_data->drop_te->directdata, drop_data->drag_directdata); + } + else { + ED_object_constraint_copy_for_object(bmain, ob_dst, drop_data->drag_directdata); + } + break; + case TSE_GPENCIL_EFFECT: { + if (ob_dst->type != OB_GPENCIL) { + return; + } + + ED_object_shaderfx_copy(ob_dst, drop_data->drag_directdata); + break; + } + } +} + +static void datastack_drop_reorder(bContext *C, ReportList *reports, StackDropData *drop_data) +{ + SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); + + TreeElement *drag_te = outliner_find_tree_element(&space_outliner->tree, drop_data->drag_tselem); + if (!drag_te) { + return; + } + + TreeElement *drop_te = drop_data->drop_te; + TreeElementInsertType insert_type = drop_data->insert_type; + + Object *ob = drop_data->ob_parent; + + int index = 0; + switch (drop_data->drag_tselem->type) { + case TSE_MODIFIER: + if (ob->type == OB_GPENCIL) { + index = outliner_get_insert_index( + drag_te, drop_te, insert_type, &ob->greasepencil_modifiers); + ED_object_gpencil_modifier_move_to_index(reports, ob, drop_data->drag_directdata, index); + } + else if (ob->type != OB_GPENCIL) { + index = outliner_get_insert_index(drag_te, drop_te, insert_type, &ob->modifiers); + ED_object_modifier_move_to_index(reports, ob, drop_data->drag_directdata, index); + } + break; + case TSE_CONSTRAINT: + if (drop_data->pchan_parent) { + index = outliner_get_insert_index( + drag_te, drop_te, insert_type, &drop_data->pchan_parent->constraints); + } + else { + index = outliner_get_insert_index(drag_te, drop_te, insert_type, &ob->constraints); + } + ED_object_constraint_move_to_index(ob, drop_data->drag_directdata, index); + + break; + case TSE_GPENCIL_EFFECT: + index = outliner_get_insert_index(drag_te, drop_te, insert_type, &ob->shader_fx); + ED_object_shaderfx_move_to_index(reports, ob, drop_data->drag_directdata, index); + } +} + +static int datastack_drop_invoke(bContext *C, wmOperator *op, const wmEvent *event) +{ + if (event->custom != EVT_DATA_DRAGDROP) { + return OPERATOR_CANCELLED; + } + + ListBase *lb = event->customdata; + wmDrag *drag = lb->first; + StackDropData *drop_data = drag->poin; + + switch (drop_data->drop_action) { + case DATA_STACK_DROP_LINK: + datastack_drop_link(C, drop_data); + break; + case DATA_STACK_DROP_COPY: + datastack_drop_copy(C, drop_data); + break; + case DATA_STACK_DROP_REORDER: + datastack_drop_reorder(C, op->reports, drop_data); + break; + } + + return OPERATOR_FINISHED; +} + +void OUTLINER_OT_datastack_drop(wmOperatorType *ot) +{ + /* identifiers */ + ot->name = "Data Stack Drop"; + ot->description = "Copy or reorder modifiers, constraints, and effects"; + ot->idname = "OUTLINER_OT_datastack_drop"; + + /* api callbacks */ + ot->invoke = datastack_drop_invoke; + + ot->poll = ED_operator_outliner_active; + + /* flags */ + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO | OPTYPE_INTERNAL; +} + /* ******************** Collection Drop Operator *********************** */ typedef struct CollectionDrop { @@ -882,7 +1338,8 @@ static int outliner_item_drag_drop_invoke(bContext *C, return (OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH); } - TreeElementIcon data = tree_element_get_icon(TREESTORE(te), te); + TreeStoreElem *tselem = TREESTORE(te); + TreeElementIcon data = tree_element_get_icon(tselem, te); if (!data.drag_id) { return (OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH); } @@ -910,13 +1367,25 @@ static int outliner_item_drag_drop_invoke(bContext *C, wmDrag *drag = WM_event_start_drag(C, data.icon, WM_DRAG_ID, NULL, 0.0, WM_DRAG_NOP); - if (ELEM(GS(data.drag_id->name), ID_OB, ID_GR)) { + if (ELEM(tselem->type, + TSE_MODIFIER, + TSE_MODIFIER_BASE, + TSE_CONSTRAINT, + TSE_CONSTRAINT_BASE, + TSE_GPENCIL_EFFECT, + TSE_GPENCIL_EFFECT_BASE)) { + + TreeElement *te_bone = NULL; + bPoseChannel *pchan = outliner_find_parent_bone(te, &te_bone); + datastack_drop_data_init(drag, (Object *)tselem->id, pchan, te, tselem, te->directdata); + } + else if (ELEM(GS(data.drag_id->name), ID_OB, ID_GR)) { /* For collections and objects we cheat and drag all selected. */ /* Only drag element under mouse if it was not selected before. */ - if ((TREESTORE(te)->flag & TSE_SELECTED) == 0) { + if ((tselem->flag & TSE_SELECTED) == 0) { outliner_flag_set(&space_outliner->tree, TSE_SELECTED, 0); - TREESTORE(te)->flag |= TSE_SELECTED; + tselem->flag |= TSE_SELECTED; } /* Gather all selected elements. */ @@ -995,7 +1464,7 @@ static int outliner_item_drag_drop_invoke(bContext *C, WM_drag_add_ID(drag, data.drag_id, data.drag_parent); } - ED_outliner_select_sync_from_all_tag(C); + ED_outliner_select_sync_from_outliner(C, space_outliner); return (OPERATOR_FINISHED | OPERATOR_PASS_THROUGH); } @@ -1027,5 +1496,6 @@ void outliner_dropboxes(void) WM_dropbox_add(lb, "OUTLINER_OT_parent_clear", parent_clear_poll, NULL); WM_dropbox_add(lb, "OUTLINER_OT_scene_drop", scene_drop_poll, NULL); WM_dropbox_add(lb, "OUTLINER_OT_material_drop", material_drop_poll, NULL); + WM_dropbox_add(lb, "OUTLINER_OT_datastack_drop", datastack_drop_poll, NULL); WM_dropbox_add(lb, "OUTLINER_OT_collection_drop", collection_drop_poll, NULL); } diff --git a/source/blender/editors/space_outliner/outliner_draw.c b/source/blender/editors/space_outliner/outliner_draw.c index 3449b73ed24..0ab98a6ab66 100644 --- a/source/blender/editors/space_outliner/outliner_draw.c +++ b/source/blender/editors/space_outliner/outliner_draw.c @@ -2024,9 +2024,11 @@ TreeElementIcon tree_element_get_icon(TreeStoreElem *tselem, TreeElement *te) break; case TSE_CONSTRAINT_BASE: data.icon = ICON_CONSTRAINT; + data.drag_id = tselem->id; break; case TSE_CONSTRAINT: { bConstraint *con = te->directdata; + data.drag_id = tselem->id; switch ((eBConstraint_Types)con->type) { case CONSTRAINT_TYPE_CAMERASOLVER: data.icon = ICON_CON_CAMERASOLVER; @@ -2121,6 +2123,7 @@ TreeElementIcon tree_element_get_icon(TreeStoreElem *tselem, TreeElement *te) } case TSE_MODIFIER_BASE: data.icon = ICON_MODIFIER_DATA; + data.drag_id = tselem->id; break; case TSE_LINKED_OB: data.icon = ICON_OBJECT_DATA; @@ -2130,6 +2133,8 @@ TreeElementIcon tree_element_get_icon(TreeStoreElem *tselem, TreeElement *te) break; case TSE_MODIFIER: { Object *ob = (Object *)tselem->id; + data.drag_id = tselem->id; + if (ob->type != OB_GPENCIL) { ModifierData *md = BLI_findlink(&ob->modifiers, tselem->nr); switch ((ModifierType)md->type) { diff --git a/source/blender/editors/space_outliner/outliner_intern.h b/source/blender/editors/space_outliner/outliner_intern.h index 485f72781e3..9ef3d8eed7a 100644 --- a/source/blender/editors/space_outliner/outliner_intern.h +++ b/source/blender/editors/space_outliner/outliner_intern.h @@ -276,6 +276,8 @@ eOLDrawState tree_element_active(struct bContext *C, const eOLSetState set, const bool handle_all_types); +struct bPoseChannel *outliner_find_parent_bone(TreeElement *te, TreeElement **r_bone_te); + void outliner_item_select(struct bContext *C, struct SpaceOutliner *space_outliner, struct TreeElement *te, @@ -387,6 +389,7 @@ void OUTLINER_OT_parent_drop(struct wmOperatorType *ot); void OUTLINER_OT_parent_clear(struct wmOperatorType *ot); void OUTLINER_OT_scene_drop(struct wmOperatorType *ot); void OUTLINER_OT_material_drop(struct wmOperatorType *ot); +void OUTLINER_OT_datastack_drop(struct wmOperatorType *ot); void OUTLINER_OT_collection_drop(struct wmOperatorType *ot); /* ...................................................... */ diff --git a/source/blender/editors/space_outliner/outliner_ops.c b/source/blender/editors/space_outliner/outliner_ops.c index 22541a0ab1f..acda5ae82f0 100644 --- a/source/blender/editors/space_outliner/outliner_ops.c +++ b/source/blender/editors/space_outliner/outliner_ops.c @@ -88,6 +88,7 @@ void outliner_operatortypes(void) WM_operatortype_append(OUTLINER_OT_parent_clear); WM_operatortype_append(OUTLINER_OT_scene_drop); WM_operatortype_append(OUTLINER_OT_material_drop); + WM_operatortype_append(OUTLINER_OT_datastack_drop); WM_operatortype_append(OUTLINER_OT_collection_drop); /* collections */ diff --git a/source/blender/editors/space_outliner/outliner_select.c b/source/blender/editors/space_outliner/outliner_select.c index 61228e24ed9..602bb351c0f 100644 --- a/source/blender/editors/space_outliner/outliner_select.c +++ b/source/blender/editors/space_outliner/outliner_select.c @@ -1032,6 +1032,23 @@ eOLDrawState tree_element_type_active(bContext *C, return OL_DRAWSEL_NONE; } +bPoseChannel *outliner_find_parent_bone(TreeElement *te, TreeElement **r_bone_te) +{ + TreeStoreElem *tselem; + + te = te->parent; + while (te) { + tselem = TREESTORE(te); + if (tselem->type == TSE_POSE_CHANNEL) { + *r_bone_te = te; + return (bPoseChannel *)te->directdata; + } + te = te->parent; + } + + return NULL; +} + /* ================================================ */ /** |