/* * ***** BEGIN GPL LICENSE BLOCK ***** * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * The Original Code is Copyright (C) 2007 Blender Foundation. * All rights reserved. * * Contributor(s): Blender Foundation * * ***** END GPL LICENSE BLOCK ***** */ /** \file blender/windowmanager/intern/wm_gesture_ops.c * \ingroup wm * * Default operator callbacks for use with gestures (border/circle/lasso/straightline). * Operators themselves are defined elsewhere. * * - Keymaps are in ``wm_operators.c``. * - Property definitions are in ``wm_operator_props.c``. */ #include "MEM_guardedalloc.h" #include "DNA_windowmanager_types.h" #include "BLI_math.h" #include "BKE_context.h" #include "WM_types.h" #include "WM_api.h" #include "wm.h" #include "wm_event_types.h" #include "wm_event_system.h" #include "wm_subwindow.h" #include "ED_screen.h" #include "RNA_access.h" #include "RNA_define.h" /* -------------------------------------------------------------------- */ /** \name Internal Gesture Utilities * * Border gesture has two types: * -# #WM_GESTURE_CROSS_RECT: starts a cross, on mouse click it changes to border. * -# #WM_GESTURE_RECT: starts immediate as a border, on mouse click or release it ends. * * It stores 4 values (xmin, xmax, ymin, ymax) and event it ended with (event_type). * * \{ */ static void gesture_modal_end(bContext *C, wmOperator *op) { wmGesture *gesture = op->customdata; WM_gesture_end(C, gesture); /* frees gesture itself, and unregisters from window */ op->customdata = NULL; ED_area_tag_redraw(CTX_wm_area(C)); if (RNA_struct_find_property(op->ptr, "cursor")) { WM_cursor_modal_restore(CTX_wm_window(C)); } } static void gesture_modal_state_to_operator(wmOperator *op, int modal_state) { PropertyRNA *prop; switch (modal_state) { case GESTURE_MODAL_SELECT: case GESTURE_MODAL_DESELECT: if ((prop = RNA_struct_find_property(op->ptr, "deselect"))) { RNA_property_boolean_set(op->ptr, prop, (modal_state == GESTURE_MODAL_DESELECT)); } break; case GESTURE_MODAL_IN: case GESTURE_MODAL_OUT: if ((prop = RNA_struct_find_property(op->ptr, "zoom_out"))) { RNA_property_boolean_set(op->ptr, prop, (modal_state == GESTURE_MODAL_OUT)); } break; } } static int gesture_modal_state_from_operator(wmOperator *op) { PropertyRNA *prop; if ((prop = RNA_struct_find_property(op->ptr, "deselect"))) { if (RNA_property_is_set(op->ptr, prop)) { return RNA_property_boolean_get(op->ptr, prop) ? GESTURE_MODAL_DESELECT : GESTURE_MODAL_SELECT; } } if ((prop = RNA_struct_find_property(op->ptr, "zoom_out"))) { if (RNA_property_is_set(op->ptr, prop)) { return RNA_property_boolean_get(op->ptr, prop) ? GESTURE_MODAL_OUT : GESTURE_MODAL_IN; } } return GESTURE_MODAL_NOP; } /** \} */ /* -------------------------------------------------------------------- */ /** \name Border Gesture * * Border gesture has two types: * -# #WM_GESTURE_CROSS_RECT: starts a cross, on mouse click it changes to border. * -# #WM_GESTURE_RECT: starts immediate as a border, on mouse click or release it ends. * * It stores 4 values (xmin, xmax, ymin, ymax) and event it ended with (event_type). * * \{ */ static bool gesture_border_apply_rect(wmOperator *op) { wmGesture *gesture = op->customdata; rcti *rect = gesture->customdata; if (rect->xmin == rect->xmax || rect->ymin == rect->ymax) return 0; /* operator arguments and storage. */ RNA_int_set(op->ptr, "xmin", min_ii(rect->xmin, rect->xmax)); RNA_int_set(op->ptr, "ymin", min_ii(rect->ymin, rect->ymax)); RNA_int_set(op->ptr, "xmax", max_ii(rect->xmin, rect->xmax)); RNA_int_set(op->ptr, "ymax", max_ii(rect->ymin, rect->ymax)); return 1; } static bool gesture_border_apply(bContext *C, wmOperator *op) { wmGesture *gesture = op->customdata; int retval; if (!gesture_border_apply_rect(op)) { return 0; } gesture_modal_state_to_operator(op, gesture->modal_state); retval = op->type->exec(C, op); OPERATOR_RETVAL_CHECK(retval); return 1; } int WM_gesture_border_invoke(bContext *C, wmOperator *op, const wmEvent *event) { int modal_state = gesture_modal_state_from_operator(op); if (ISTWEAK(event->type) || (modal_state != GESTURE_MODAL_NOP)) { op->customdata = WM_gesture_new(C, event, WM_GESTURE_RECT); } else { op->customdata = WM_gesture_new(C, event, WM_GESTURE_CROSS_RECT); } /* Starting with the mode starts immediately, like having 'wait_for_input' disabled (some tools use this). */ if (modal_state == GESTURE_MODAL_NOP) { wmGesture *gesture = op->customdata; gesture->wait_for_input = true; } else { wmGesture *gesture = op->customdata; gesture->modal_state = modal_state; } /* add modal handler */ WM_event_add_modal_handler(C, op); wm_gesture_tag_redraw(C); return OPERATOR_RUNNING_MODAL; } int WM_gesture_border_modal(bContext *C, wmOperator *op, const wmEvent *event) { wmGesture *gesture = op->customdata; rcti *rect = gesture->customdata; int sx, sy; if (event->type == MOUSEMOVE) { wm_subwindow_origin_get(CTX_wm_window(C), gesture->swinid, &sx, &sy); if (gesture->type == WM_GESTURE_CROSS_RECT && gesture->is_active == false) { rect->xmin = rect->xmax = event->x - sx; rect->ymin = rect->ymax = event->y - sy; } else { rect->xmax = event->x - sx; rect->ymax = event->y - sy; } gesture_border_apply_rect(op); wm_gesture_tag_redraw(C); } else if (event->type == EVT_MODAL_MAP) { switch (event->val) { case GESTURE_MODAL_BEGIN: if (gesture->type == WM_GESTURE_CROSS_RECT && gesture->is_active == false) { gesture->is_active = true; wm_gesture_tag_redraw(C); } break; case GESTURE_MODAL_SELECT: case GESTURE_MODAL_DESELECT: case GESTURE_MODAL_IN: case GESTURE_MODAL_OUT: if (gesture->wait_for_input) { gesture->modal_state = event->val; } if (gesture_border_apply(C, op)) { gesture_modal_end(C, op); return OPERATOR_FINISHED; } gesture_modal_end(C, op); return OPERATOR_CANCELLED; case GESTURE_MODAL_CANCEL: gesture_modal_end(C, op); return OPERATOR_CANCELLED; } } #ifdef WITH_INPUT_NDOF else if (event->type == NDOF_MOTION) { return OPERATOR_PASS_THROUGH; } #endif #if 0 /* Allow view navigation??? */ else { return OPERATOR_PASS_THROUGH; } #endif return OPERATOR_RUNNING_MODAL; } void WM_gesture_border_cancel(bContext *C, wmOperator *op) { gesture_modal_end(C, op); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Circle Gesture * * Currently only used for selection or modal paint stuff, * calls ``exec`` while hold mouse, exits on release (with no difference between cancel and confirm). * * \{ */ static void gesture_circle_apply(bContext *C, wmOperator *op); int WM_gesture_circle_invoke(bContext *C, wmOperator *op, const wmEvent *event) { int modal_state = gesture_modal_state_from_operator(op); op->customdata = WM_gesture_new(C, event, WM_GESTURE_CIRCLE); wmGesture *gesture = op->customdata; rcti *rect = gesture->customdata; /* Default or previously stored value. */ rect->xmax = RNA_int_get(op->ptr, "radius"); /* Starting with the mode starts immediately, like having 'wait_for_input' disabled (some tools use this). */ if (modal_state == GESTURE_MODAL_NOP) { gesture->wait_for_input = true; } else { gesture->is_active = true; gesture->modal_state = modal_state; gesture_circle_apply(C, op); } /* add modal handler */ WM_event_add_modal_handler(C, op); wm_gesture_tag_redraw(C); return OPERATOR_RUNNING_MODAL; } static void gesture_circle_apply(bContext *C, wmOperator *op) { wmGesture *gesture = op->customdata; rcti *rect = gesture->customdata; if (gesture->modal_state == GESTURE_MODAL_NOP) { return; } /* operator arguments and storage. */ RNA_int_set(op->ptr, "x", rect->xmin); RNA_int_set(op->ptr, "y", rect->ymin); RNA_int_set(op->ptr, "radius", rect->xmax); gesture_modal_state_to_operator(op, gesture->modal_state); if (op->type->exec) { int retval; retval = op->type->exec(C, op); OPERATOR_RETVAL_CHECK(retval); } } int WM_gesture_circle_modal(bContext *C, wmOperator *op, const wmEvent *event) { wmGesture *gesture = op->customdata; rcti *rect = gesture->customdata; int sx, sy; if (event->type == MOUSEMOVE) { wm_subwindow_origin_get(CTX_wm_window(C), gesture->swinid, &sx, &sy); rect->xmin = event->x - sx; rect->ymin = event->y - sy; wm_gesture_tag_redraw(C); if (gesture->is_active) { gesture_circle_apply(C, op); } } else if (event->type == EVT_MODAL_MAP) { bool is_circle_size = false; bool is_finished = false; float fac; switch (event->val) { case GESTURE_MODAL_CIRCLE_SIZE: fac = 0.3f * (event->y - event->prevy); if (fac > 0) rect->xmax += ceil(fac); else rect->xmax += floor(fac); if (rect->xmax < 1) rect->xmax = 1; is_circle_size = true; break; case GESTURE_MODAL_CIRCLE_ADD: rect->xmax += 2 + rect->xmax / 10; is_circle_size = true; break; case GESTURE_MODAL_CIRCLE_SUB: rect->xmax -= 2 + rect->xmax / 10; if (rect->xmax < 1) rect->xmax = 1; is_circle_size = true; break; case GESTURE_MODAL_SELECT: case GESTURE_MODAL_DESELECT: case GESTURE_MODAL_NOP: { if (gesture->wait_for_input) { gesture->modal_state = event->val; } if (event->val == GESTURE_MODAL_NOP) { /* Single action, click-drag & release to exit. */ if (gesture->wait_for_input == false) { is_finished = true; } } else { /* apply first click */ gesture_circle_apply(C, op); gesture->is_active = true; wm_gesture_tag_redraw(C); } break; } case GESTURE_MODAL_CANCEL: case GESTURE_MODAL_CONFIRM: is_finished = true; } if (is_finished) { gesture_modal_end(C, op); return OPERATOR_FINISHED; /* use finish or we don't get an undo */ } if (is_circle_size) { wm_gesture_tag_redraw(C); /* So next use remembers last seen size, even if we didn't apply it. */ RNA_int_set(op->ptr, "radius", rect->xmax); } } #ifdef WITH_INPUT_NDOF else if (event->type == NDOF_MOTION) { return OPERATOR_PASS_THROUGH; } #endif #if 0 /* Allow view navigation??? */ /* note, this gives issues: * 1) other modal ops run on top (border select), * 2) middlemouse is used now 3) tablet/trackpad? */ else { return OPERATOR_PASS_THROUGH; } #endif return OPERATOR_RUNNING_MODAL; } void WM_gesture_circle_cancel(bContext *C, wmOperator *op) { gesture_modal_end(C, op); } #if 0 /* template to copy from */ void WM_OT_circle_gesture(wmOperatorType *ot) { ot->name = "Circle Gesture"; ot->idname = "WM_OT_circle_gesture"; ot->description = "Enter rotate mode with a circular gesture"; ot->invoke = WM_gesture_circle_invoke; ot->modal = WM_gesture_circle_modal; ot->poll = WM_operator_winactive; /* properties */ WM_operator_properties_gesture_circle(ot); } #endif /** \} */ /* -------------------------------------------------------------------- */ /** \name Tweak Gesture * \{ */ static void gesture_tweak_modal(bContext *C, const wmEvent *event) { wmWindow *window = CTX_wm_window(C); wmGesture *gesture = window->tweak; rcti *rect = gesture->customdata; int sx, sy, val; switch (event->type) { case MOUSEMOVE: case INBETWEEN_MOUSEMOVE: wm_subwindow_origin_get(window, gesture->swinid, &sx, &sy); rect->xmax = event->x - sx; rect->ymax = event->y - sy; if ((val = wm_gesture_evaluate(gesture))) { wmEvent tevent; wm_event_init_from_window(window, &tevent); /* We want to get coord from start of drag, not from point where it becomes a tweak event, see T40549 */ tevent.x = rect->xmin + sx; tevent.y = rect->ymin + sy; if (gesture->event_type == LEFTMOUSE) tevent.type = EVT_TWEAK_L; else if (gesture->event_type == RIGHTMOUSE) tevent.type = EVT_TWEAK_R; else tevent.type = EVT_TWEAK_M; tevent.val = val; /* mouse coords! */ /* important we add immediately after this event, so future mouse releases * (which may be in the queue already), are handled in order, see T44740 */ wm_event_add_ex(window, &tevent, event); WM_gesture_end(C, gesture); /* frees gesture itself, and unregisters from window */ } break; case LEFTMOUSE: case RIGHTMOUSE: case MIDDLEMOUSE: if (gesture->event_type == event->type) { WM_gesture_end(C, gesture); /* when tweak fails we should give the other keymap entries a chance */ /* XXX, assigning to readonly, BAD JUJU! */ ((wmEvent *)event)->val = KM_RELEASE; } break; default: if (!ISTIMER(event->type) && event->type != EVENT_NONE) { WM_gesture_end(C, gesture); } break; } } /* standard tweak, called after window handlers passed on event */ void wm_tweakevent_test(bContext *C, const wmEvent *event, int action) { wmWindow *win = CTX_wm_window(C); if (win->tweak == NULL) { if (CTX_wm_region(C)) { if (event->val == KM_PRESS) { if (ELEM(event->type, LEFTMOUSE, MIDDLEMOUSE, RIGHTMOUSE)) { win->tweak = WM_gesture_new(C, event, WM_GESTURE_TWEAK); } } } } else { /* no tweaks if event was handled */ if ((action & WM_HANDLER_BREAK)) { WM_gesture_end(C, win->tweak); } else gesture_tweak_modal(C, event); } } /** \} */ /* -------------------------------------------------------------------- */ /** \name Lasso Gesture * \{ */ int WM_gesture_lasso_invoke(bContext *C, wmOperator *op, const wmEvent *event) { PropertyRNA *prop; op->customdata = WM_gesture_new(C, event, WM_GESTURE_LASSO); /* add modal handler */ WM_event_add_modal_handler(C, op); wm_gesture_tag_redraw(C); if ((prop = RNA_struct_find_property(op->ptr, "cursor"))) { WM_cursor_modal_set(CTX_wm_window(C), RNA_property_int_get(op->ptr, prop)); } return OPERATOR_RUNNING_MODAL; } int WM_gesture_lines_invoke(bContext *C, wmOperator *op, const wmEvent *event) { PropertyRNA *prop; op->customdata = WM_gesture_new(C, event, WM_GESTURE_LINES); /* add modal handler */ WM_event_add_modal_handler(C, op); wm_gesture_tag_redraw(C); if ((prop = RNA_struct_find_property(op->ptr, "cursor"))) { WM_cursor_modal_set(CTX_wm_window(C), RNA_property_int_get(op->ptr, prop)); } return OPERATOR_RUNNING_MODAL; } static void gesture_lasso_apply(bContext *C, wmOperator *op) { wmGesture *gesture = op->customdata; PointerRNA itemptr; float loc[2]; int i; const short *lasso = gesture->customdata; /* operator storage as path. */ RNA_collection_clear(op->ptr, "path"); for (i = 0; i < gesture->points; i++, lasso += 2) { loc[0] = lasso[0]; loc[1] = lasso[1]; RNA_collection_add(op->ptr, "path", &itemptr); RNA_float_set_array(&itemptr, "loc", loc); } gesture_modal_end(C, op); if (op->type->exec) { int retval = op->type->exec(C, op); OPERATOR_RETVAL_CHECK(retval); } } int WM_gesture_lasso_modal(bContext *C, wmOperator *op, const wmEvent *event) { wmGesture *gesture = op->customdata; int sx, sy; switch (event->type) { case MOUSEMOVE: case INBETWEEN_MOUSEMOVE: wm_gesture_tag_redraw(C); wm_subwindow_origin_get(CTX_wm_window(C), gesture->swinid, &sx, &sy); if (gesture->points == gesture->points_alloc) { gesture->points_alloc *= 2; gesture->customdata = MEM_reallocN(gesture->customdata, sizeof(short[2]) * gesture->points_alloc); } { int x, y; short *lasso = gesture->customdata; lasso += (2 * gesture->points - 2); x = (event->x - sx - lasso[0]); y = (event->y - sy - lasso[1]); /* make a simple distance check to get a smoother lasso * add only when at least 2 pixels between this and previous location */ if ((x * x + y * y) > 4) { lasso += 2; lasso[0] = event->x - sx; lasso[1] = event->y - sy; gesture->points++; } } break; case LEFTMOUSE: case MIDDLEMOUSE: case RIGHTMOUSE: if (event->val == KM_RELEASE) { /* key release */ gesture_lasso_apply(C, op); return OPERATOR_FINISHED; } break; case ESCKEY: gesture_modal_end(C, op); return OPERATOR_CANCELLED; } return OPERATOR_RUNNING_MODAL; } int WM_gesture_lines_modal(bContext *C, wmOperator *op, const wmEvent *event) { return WM_gesture_lasso_modal(C, op, event); } void WM_gesture_lasso_cancel(bContext *C, wmOperator *op) { gesture_modal_end(C, op); } void WM_gesture_lines_cancel(bContext *C, wmOperator *op) { gesture_modal_end(C, op); } /** * helper function, we may want to add options for conversion to view space * * caller must free. */ const int (*WM_gesture_lasso_path_to_array(bContext *UNUSED(C), wmOperator *op, int *mcords_tot))[2] { PropertyRNA *prop = RNA_struct_find_property(op->ptr, "path"); int (*mcords)[2] = NULL; BLI_assert(prop != NULL); if (prop) { const int len = RNA_property_collection_length(op->ptr, prop); if (len) { int i = 0; mcords = MEM_mallocN(sizeof(int) * 2 * len, __func__); RNA_PROP_BEGIN (op->ptr, itemptr, prop) { float loc[2]; RNA_float_get_array(&itemptr, "loc", loc); mcords[i][0] = (int)loc[0]; mcords[i][1] = (int)loc[1]; i++; } RNA_PROP_END; } *mcords_tot = len; } else { *mcords_tot = 0; } /* cast for 'const' */ return (const int (*)[2])mcords; } #if 0 /* template to copy from */ static int gesture_lasso_exec(bContext *C, wmOperator *op) { RNA_BEGIN (op->ptr, itemptr, "path") { float loc[2]; RNA_float_get_array(&itemptr, "loc", loc); printf("Location: %f %f\n", loc[0], loc[1]); } RNA_END; return OPERATOR_FINISHED; } void WM_OT_lasso_gesture(wmOperatorType *ot) { PropertyRNA *prop; ot->name = "Lasso Gesture"; ot->idname = "WM_OT_lasso_gesture"; ot->description = "Select objects within the lasso as you move the pointer"; ot->invoke = WM_gesture_lasso_invoke; ot->modal = WM_gesture_lasso_modal; ot->exec = gesture_lasso_exec; ot->poll = WM_operator_winactive; prop = RNA_def_property(ot->srna, "path", PROP_COLLECTION, PROP_NONE); RNA_def_property_struct_runtime(prop, &RNA_OperatorMousePath); } #endif /** \} */ /* -------------------------------------------------------------------- */ /** \name Straight Line Gesture * \{ */ static bool gesture_straightline_apply(bContext *C, wmOperator *op) { wmGesture *gesture = op->customdata; rcti *rect = gesture->customdata; if (rect->xmin == rect->xmax && rect->ymin == rect->ymax) return 0; /* operator arguments and storage. */ RNA_int_set(op->ptr, "xstart", rect->xmin); RNA_int_set(op->ptr, "ystart", rect->ymin); RNA_int_set(op->ptr, "xend", rect->xmax); RNA_int_set(op->ptr, "yend", rect->ymax); if (op->type->exec) { int retval = op->type->exec(C, op); OPERATOR_RETVAL_CHECK(retval); } return 1; } int WM_gesture_straightline_invoke(bContext *C, wmOperator *op, const wmEvent *event) { PropertyRNA *prop; op->customdata = WM_gesture_new(C, event, WM_GESTURE_STRAIGHTLINE); if (ISTWEAK(event->type)) { wmGesture *gesture = op->customdata; gesture->is_active = true; } /* add modal handler */ WM_event_add_modal_handler(C, op); wm_gesture_tag_redraw(C); if ((prop = RNA_struct_find_property(op->ptr, "cursor"))) { WM_cursor_modal_set(CTX_wm_window(C), RNA_property_int_get(op->ptr, prop)); } return OPERATOR_RUNNING_MODAL; } int WM_gesture_straightline_modal(bContext *C, wmOperator *op, const wmEvent *event) { wmGesture *gesture = op->customdata; rcti *rect = gesture->customdata; int sx, sy; if (event->type == MOUSEMOVE) { wm_subwindow_origin_get(CTX_wm_window(C), gesture->swinid, &sx, &sy); if (gesture->is_active == false) { rect->xmin = rect->xmax = event->x - sx; rect->ymin = rect->ymax = event->y - sy; } else { rect->xmax = event->x - sx; rect->ymax = event->y - sy; gesture_straightline_apply(C, op); } wm_gesture_tag_redraw(C); } else if (event->type == EVT_MODAL_MAP) { switch (event->val) { case GESTURE_MODAL_BEGIN: if (gesture->is_active == false) { gesture->is_active = true; wm_gesture_tag_redraw(C); } break; case GESTURE_MODAL_SELECT: if (gesture_straightline_apply(C, op)) { gesture_modal_end(C, op); return OPERATOR_FINISHED; } gesture_modal_end(C, op); return OPERATOR_CANCELLED; case GESTURE_MODAL_CANCEL: gesture_modal_end(C, op); return OPERATOR_CANCELLED; } } return OPERATOR_RUNNING_MODAL; } void WM_gesture_straightline_cancel(bContext *C, wmOperator *op) { gesture_modal_end(C, op); } #if 0 /* template to copy from */ void WM_OT_straightline_gesture(wmOperatorType *ot) { PropertyRNA *prop; ot->name = "Straight Line Gesture"; ot->idname = "WM_OT_straightline_gesture"; ot->description = "Draw a straight line as you move the pointer"; ot->invoke = WM_gesture_straightline_invoke; ot->modal = WM_gesture_straightline_modal; ot->exec = gesture_straightline_exec; ot->poll = WM_operator_winactive; WM_operator_properties_gesture_straightline(ot, 0); } #endif /** \} */