diff options
author | Hans Goudey <h.goudey@me.com> | 2020-06-05 17:01:20 +0300 |
---|---|---|
committer | Hans Goudey <h.goudey@me.com> | 2020-06-05 17:01:20 +0300 |
commit | 859505a3dae8b23a44c7216b9a3aa753594ae1b0 (patch) | |
tree | 0a3321507565c7c8897e5a2fc587666f3ec7ab52 /source | |
parent | 1270984e131d6a96a6cb630448eeef056c9098ff (diff) |
UI: Pan 2D Region When Dragging to Boundary
This adds a modal operator called Edge Pan, which is meant to run invisibly
while something inside a region is being dragged. This patch applies this
to dragging panels, but it can be used elsewhere too.
The speed (which is defined relative to how far the mouse is beyond the
boundary) and delay are easily adjustible. and the speed also increases
smoothly from a start value to a max to make it feel more interactive
and less robotic.
Differential Revision: https://developer.blender.org/D7465
Diffstat (limited to 'source')
-rw-r--r-- | source/blender/editors/interface/interface_panel.c | 34 | ||||
-rw-r--r-- | source/blender/editors/interface/view2d_ops.c | 185 |
2 files changed, 209 insertions, 10 deletions
diff --git a/source/blender/editors/interface/interface_panel.c b/source/blender/editors/interface/interface_panel.c index 54f60a05cfd..33f600f30a1 100644 --- a/source/blender/editors/interface/interface_panel.c +++ b/source/blender/editors/interface/interface_panel.c @@ -107,6 +107,7 @@ typedef struct uiHandlePanelData { int startx, starty; int startofsx, startofsy; int startsizex, startsizey; + float start_cur_xmin, start_cur_ymin; } uiHandlePanelData; typedef struct PanelSort { @@ -1731,21 +1732,22 @@ static void check_panel_overlap(ARegion *region, Panel *panel) /************************ panel dragging ****************************/ +#define DRAG_REGION_PAD (PNL_HEADER * 0.5) static void ui_do_drag(const bContext *C, const wmEvent *event, Panel *panel) { uiHandlePanelData *data = panel->activedata; ScrArea *area = CTX_wm_area(C); ARegion *region = CTX_wm_region(C); - short align = panel_aligned(area, region), dx = 0, dy = 0; + short align = panel_aligned(area, region); - /* first clip for window, no dragging outside */ - if (!BLI_rcti_isect_pt_v(®ion->winrct, &event->x)) { - return; - } + /* Keep the drag position in the region with a small pad to keep the panel visible. */ + int x = clamp_i(event->x, region->winrct.xmin, region->winrct.xmax + DRAG_REGION_PAD); + int y = clamp_i(event->y, region->winrct.ymin, region->winrct.ymax + DRAG_REGION_PAD); - dx = (event->x - data->startx); - dy = (event->y - data->starty); + float dx = (float)(x - data->startx); + float dy = (float)(y - data->starty); + /* Adjust for region zoom. */ dx *= (float)BLI_rctf_size_x(®ion->v2d.cur) / (float)BLI_rcti_size_x(®ion->winrct); dy *= (float)BLI_rctf_size_y(®ion->v2d.cur) / (float)BLI_rcti_size_y(®ion->winrct); @@ -1763,17 +1765,21 @@ static void ui_do_drag(const bContext *C, const wmEvent *event, Panel *panel) /* reset the panel snapping, to allow dragging away from snapped edges */ panel->snap = PNL_SNAP_NONE; - panel->ofsx = data->startofsx + dx; - panel->ofsy = data->startofsy + dy; + /* Add the movement of the view due to edge scrolling while dragging. */ + dx += ((float)region->v2d.cur.xmin - data->start_cur_xmin); + dy += ((float)region->v2d.cur.ymin - data->start_cur_ymin); + panel->ofsx = data->startofsx + round_fl_to_int(dx); + panel->ofsy = data->startofsy + round_fl_to_int(dy); check_panel_overlap(region, panel); if (align) { - uiAlignPanelStep(area, region, 0.2, true); + uiAlignPanelStep(area, region, 0.2f, true); } } ED_region_tag_redraw(region); } +#undef DRAG_REGION_PAD /******************* region level panel interaction *****************/ @@ -2967,6 +2973,12 @@ static void panel_activate_state(const bContext *C, Panel *panel, uiHandlePanelS data->animtimer = WM_event_add_timer(CTX_wm_manager(C), win, TIMER, ANIMATION_INTERVAL); } + /* Initiate edge panning during drags so we can move beyond the initial region view. */ + if (state == PANEL_STATE_DRAG) { + wmOperatorType *ot = WM_operatortype_find("VIEW2D_OT_edge_pan", true); + ui_handle_afterfunc_add_operator(ot, WM_OP_INVOKE_DEFAULT, true); + } + data->state = state; data->startx = win->eventstate->x; data->starty = win->eventstate->y; @@ -2974,6 +2986,8 @@ static void panel_activate_state(const bContext *C, Panel *panel, uiHandlePanelS data->startofsy = panel->ofsy; data->startsizex = panel->sizex; data->startsizey = panel->sizey; + data->start_cur_xmin = region->v2d.cur.xmin; + data->start_cur_ymin = region->v2d.cur.ymin; data->starttime = PIL_check_seconds_timer(); /* Remember drag drop state even when animating to the aligned position after dragging. */ diff --git a/source/blender/editors/interface/view2d_ops.c b/source/blender/editors/interface/view2d_ops.c index 5b1e5f746ef..ff35b25e488 100644 --- a/source/blender/editors/interface/view2d_ops.c +++ b/source/blender/editors/interface/view2d_ops.c @@ -100,6 +100,10 @@ typedef struct v2dViewPanData { /** for MMB in scrollers (old feature in past, but now not that useful) */ short in_scroller; + + /* View2D Edge Panning */ + double edge_pan_last_time; + double edge_pan_start_time_x, edge_pan_start_time_y; } v2dViewPanData; /* initialize panning customdata */ @@ -357,6 +361,186 @@ static void VIEW2D_OT_pan(wmOperatorType *ot) /** \} */ /* -------------------------------------------------------------------- */ +/** \name View Edge Pan Operator (modal) + * + * Scroll the region if the mouse is dragged to an edge. "Invisible" operator that always + * passes through. + * \{ */ + +/** Distance from the edge of the region within which to start panning. */ +#define EDGE_PAN_REGION_PAD (U.widget_unit) +/** Speed factor in pixels per second per pixel of distance from edge pan zone beginning. */ +#define EDGE_PAN_SPEED_PER_PIXEL (25.0f * (float)U.dpi_fac) +/** Delay before drag panning in seconds. */ +#define EDGE_PAN_DELAY 1.0f + +/* set up modal operator and relevant settings */ +static int view_edge_pan_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event)) +{ + /* Set up customdata. */ + if (!view_pan_init(C, op)) { + return OPERATOR_PASS_THROUGH; + } + + v2dViewPanData *vpd = op->customdata; + + vpd->edge_pan_start_time_x = 0.0; + vpd->edge_pan_start_time_y = 0.0; + vpd->edge_pan_last_time = PIL_check_seconds_timer(); + + WM_event_add_modal_handler(C, op); + + return (OPERATOR_RUNNING_MODAL | OPERATOR_PASS_THROUGH); +} + +/** + * Reset the edge pan timers if the mouse isn't in the scroll zone and + * start the timers when the mouse enters a scroll zone. + */ +static void edge_pan_manage_delay_timers(v2dViewPanData *vpd, + int pan_dir_x, + int pan_dir_y, + const double current_time) +{ + if (pan_dir_x == 0) { + vpd->edge_pan_start_time_x = 0.0; + } + else if (vpd->edge_pan_start_time_x == 0.0) { + vpd->edge_pan_start_time_x = current_time; + } + if (pan_dir_y == 0) { + vpd->edge_pan_start_time_y = 0.0; + } + else if (vpd->edge_pan_start_time_y == 0.0) { + vpd->edge_pan_start_time_y = current_time; + } +} + +/** + * Used to calculate a "fade in" factor for edge panning to make the interaction feel smooth + * and more purposeful. + * + * \note Assumes a domain_min of 0.0f. + */ +static float smootherstep(const float domain_max, float x) +{ + x = clamp_f(x / domain_max, 0.0, 1.0); + return x * x * x * (x * (x * 6.0 - 15.0) + 10.0); +} + +static float edge_pan_speed(v2dViewPanData *vpd, + int event_loc, + bool x_dir, + const double current_time) +{ + ARegion *region = vpd->region; + + /* Find the distance from the start of the drag zone. */ + int min = x_dir ? region->winrct.xmin : region->winrct.ymin + EDGE_PAN_REGION_PAD; + int max = x_dir ? region->winrct.xmax : region->winrct.ymax - EDGE_PAN_REGION_PAD; + int distance = 0.0; + if (event_loc > max) { + distance = event_loc - max; + } + else if (event_loc < min) { + distance = min - event_loc; + } + else { + BLI_assert(!"Calculating speed outside of pan zones"); + return 0.0f; + } + + /* Apply a fade in to the speed based on a start time delay. */ + double start_time = x_dir ? vpd->edge_pan_start_time_x : vpd->edge_pan_start_time_y; + float delay_factor = smootherstep(EDGE_PAN_DELAY, (float)(current_time - start_time)); + + return distance * EDGE_PAN_SPEED_PER_PIXEL * delay_factor; +} + +static int view_edge_pan_modal(bContext *C, wmOperator *op, const wmEvent *event) +{ + v2dViewPanData *vpd = op->customdata; + ARegion *region = vpd->region; + + if (event->val == KM_RELEASE || event->type == EVT_ESCKEY) { + view_pan_exit(op); + return (OPERATOR_FINISHED | OPERATOR_PASS_THROUGH); + } + /* Only mousemove events matter here, ignore others. */ + if (event->type != MOUSEMOVE) { + return OPERATOR_PASS_THROUGH; + } + + /* This operator is supposed to run together with some drag action. + * On successful handling, always pass events on to other handlers. */ + const int success_retval = OPERATOR_PASS_THROUGH; + + /* Find whether the mouse is beyond X and Y edges. */ + int pan_dir_x = 0; + int pan_dir_y = 0; + if (event->x > region->winrct.xmax - EDGE_PAN_REGION_PAD) { + pan_dir_x = 1; + } + else if (event->x < region->winrct.xmin + EDGE_PAN_REGION_PAD) { + pan_dir_x = -1; + } + if (event->y > region->winrct.ymax - EDGE_PAN_REGION_PAD) { + pan_dir_y = 1; + } + else if (event->y < region->winrct.ymin + EDGE_PAN_REGION_PAD) { + pan_dir_y = -1; + } + + const double current_time = PIL_check_seconds_timer(); + edge_pan_manage_delay_timers(vpd, pan_dir_x, pan_dir_y, current_time); + + /* Calculate the delta since the last time the operator was called. */ + float dtime = (float)(current_time - vpd->edge_pan_last_time); + float dx = 0.0f, dy = 0.0f; + if (pan_dir_x != 0) { + float speed = edge_pan_speed(vpd, event->x, true, current_time); + dx = dtime * speed * (float)pan_dir_x; + } + if (pan_dir_y != 0) { + float speed = edge_pan_speed(vpd, event->y, false, current_time); + dy = dtime * speed * (float)pan_dir_y; + } + vpd->edge_pan_last_time = current_time; + + /* Pan, clamping inside the regions's total bounds. */ + view_pan_apply_ex(C, vpd, dx, dy); + + return success_retval; +} + +static void view_edge_pan_cancel(bContext *UNUSED(C), wmOperator *op) +{ + view_pan_exit(op); +} + +static void VIEW2D_OT_edge_pan(wmOperatorType *ot) +{ + /* identifiers */ + ot->name = "View Edge Pan"; + ot->description = "Pan the view when the mouse is held at an edge"; + ot->idname = "VIEW2D_OT_edge_pan"; + + /* api callbacks */ + ot->invoke = view_edge_pan_invoke; + ot->modal = view_edge_pan_modal; + ot->cancel = view_edge_pan_cancel; + + /* operator is modal */ + ot->flag = OPTYPE_INTERNAL; +} + +#undef EDGE_PAN_REGION_PAD +#undef EDGE_PAN_SPEED_PER_PIXEL +#undef EDGE_PAN_DELAY + +/** \} */ + +/* -------------------------------------------------------------------- */ /** \name View Pan Operator (single step) * \{ */ @@ -2246,6 +2430,7 @@ static void VIEW2D_OT_reset(wmOperatorType *ot) void ED_operatortypes_view2d(void) { WM_operatortype_append(VIEW2D_OT_pan); + WM_operatortype_append(VIEW2D_OT_edge_pan); WM_operatortype_append(VIEW2D_OT_scroll_left); WM_operatortype_append(VIEW2D_OT_scroll_right); |