Fix T82758: Convert Proxy to Override: Local constraints aren't saved.
[blender.git] / source / blender / editors / interface / view2d_ops.c
1 /*
2  * This program is free software; you can redistribute it and/or
3  * modify it under the terms of the GNU General Public License
4  * as published by the Free Software Foundation; either version 2
5  * of the License, or (at your option) any later version.
6  *
7  * This program is distributed in the hope that it will be useful,
8  * but WITHOUT ANY WARRANTY; without even the implied warranty of
9  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10  * GNU General Public License for more details.
11  *
12  * You should have received a copy of the GNU General Public License
13  * along with this program; if not, write to the Free Software Foundation,
14  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
15  *
16  * The Original Code is Copyright (C) 2008 Blender Foundation.
17  * All rights reserved.
18  */
19
20 /** \file
21  * \ingroup edinterface
22  */
23
24 #include <math.h>
25
26 #include "MEM_guardedalloc.h"
27
28 #include "DNA_userdef_types.h"
29 #include "DNA_windowmanager_types.h"
30
31 #include "BLI_blenlib.h"
32 #include "BLI_math_base.h"
33 #include "BLI_math_vector.h"
34 #include "BLI_utildefines.h"
35
36 #include "BKE_context.h"
37
38 #include "RNA_access.h"
39 #include "RNA_define.h"
40
41 #include "WM_api.h"
42 #include "WM_types.h"
43
44 #include "ED_screen.h"
45
46 #include "UI_interface.h"
47 #include "UI_view2d.h"
48
49 #include "PIL_time.h" /* USER_ZOOM_CONT */
50
51 /* -------------------------------------------------------------------- */
52 /** \name Internal Utilities
53  * \{ */
54
55 static bool view2d_poll(bContext *C)
56 {
57   ARegion *region = CTX_wm_region(C);
58
59   return (region != NULL) && (region->v2d.flag & V2D_IS_INIT);
60 }
61
62 /** \} */
63
64 /* -------------------------------------------------------------------- */
65 /** \name View Pan Shared Utilities
66  * \{ */
67
68 /**
69  * This group of operators come in several forms:
70  * -# Modal 'dragging' with MMB - where movement of mouse dictates amount to pan view by
71  * -# Scroll-wheel 'steps' - rolling mouse-wheel by one step moves view by predefined amount
72  *
73  * In order to make sure this works, each operator must define the following RNA-Operator Props:
74  * - `deltax, deltay` - define how much to move view by (relative to zoom-correction factor)
75  */
76
77 /**
78  * Temporary custom-data for operator.
79  */
80 typedef struct v2dViewPanData {
81   /** screen where view pan was initiated */
82   bScreen *screen;
83   /** area where view pan was initiated */
84   ScrArea *area;
85   /** region where view pan was initiated */
86   ARegion *region;
87   /** view2d we're operating in */
88   View2D *v2d;
89
90   /** amount to move view relative to zoom */
91   float facx, facy;
92
93   /* options for version 1 */
94   /** mouse x/y values in window when operator was initiated */
95   int startx, starty;
96   /** previous x/y values of mouse in window */
97   int lastx, lasty;
98   /** event starting pan, for modal exit */
99   int invoke_event;
100
101   /** for MMB in scrollers (old feature in past, but now not that useful) */
102   short in_scroller;
103
104   /* View2D Edge Panning */
105   double edge_pan_last_time;
106   double edge_pan_start_time_x, edge_pan_start_time_y;
107 } v2dViewPanData;
108
109 static bool view_pan_poll(bContext *C)
110 {
111   ARegion *region = CTX_wm_region(C);
112   View2D *v2d;
113
114   /* check if there's a region in context to work with */
115   if (region == NULL) {
116     return false;
117   }
118
119   v2d = &region->v2d;
120
121   /* check that 2d-view can pan */
122   if ((v2d->keepofs & V2D_LOCKOFS_X) && (v2d->keepofs & V2D_LOCKOFS_Y)) {
123     return false;
124   }
125
126   /* view can pan */
127   return true;
128 }
129
130 /* initialize panning customdata */
131 static void view_pan_init(bContext *C, wmOperator *op)
132 {
133   /* Should've been checked before. */
134   BLI_assert(view_pan_poll(C));
135
136   /* set custom-data for operator */
137   v2dViewPanData *vpd = MEM_callocN(sizeof(v2dViewPanData), "v2dViewPanData");
138   op->customdata = vpd;
139
140   /* set pointers to owners */
141   vpd->screen = CTX_wm_screen(C);
142   vpd->area = CTX_wm_area(C);
143   vpd->region = CTX_wm_region(C);
144   vpd->v2d = &vpd->region->v2d;
145
146   /* calculate translation factor - based on size of view */
147   float winx = (float)(BLI_rcti_size_x(&vpd->region->winrct) + 1);
148   float winy = (float)(BLI_rcti_size_y(&vpd->region->winrct) + 1);
149   vpd->facx = (BLI_rctf_size_x(&vpd->v2d->cur)) / winx;
150   vpd->facy = (BLI_rctf_size_y(&vpd->v2d->cur)) / winy;
151 }
152
153 /* apply transform to view (i.e. adjust 'cur' rect) */
154 static void view_pan_apply_ex(bContext *C, v2dViewPanData *vpd, float dx, float dy)
155 {
156   View2D *v2d = vpd->v2d;
157
158   /* calculate amount to move view by */
159   dx *= vpd->facx;
160   dy *= vpd->facy;
161
162   /* only move view on an axis if change is allowed */
163   if ((v2d->keepofs & V2D_LOCKOFS_X) == 0) {
164     v2d->cur.xmin += dx;
165     v2d->cur.xmax += dx;
166   }
167   if ((v2d->keepofs & V2D_LOCKOFS_Y) == 0) {
168     v2d->cur.ymin += dy;
169     v2d->cur.ymax += dy;
170   }
171
172   /* Inform v2d about changes after this operation. */
173   UI_view2d_curRect_changed(C, v2d);
174
175   /* don't rebuild full tree in outliner, since we're just changing our view */
176   ED_region_tag_redraw_no_rebuild(vpd->region);
177
178   /* request updates to be done... */
179   WM_event_add_mousemove(CTX_wm_window(C));
180
181   UI_view2d_sync(vpd->screen, vpd->area, v2d, V2D_LOCK_COPY);
182 }
183
184 static void view_pan_apply(bContext *C, wmOperator *op)
185 {
186   v2dViewPanData *vpd = op->customdata;
187
188   view_pan_apply_ex(C, vpd, RNA_int_get(op->ptr, "deltax"), RNA_int_get(op->ptr, "deltay"));
189 }
190
191 /* cleanup temp customdata  */
192 static void view_pan_exit(wmOperator *op)
193 {
194   if (op->customdata) {
195     MEM_freeN(op->customdata);
196     op->customdata = NULL;
197   }
198 }
199
200 /** \} */
201
202 /* -------------------------------------------------------------------- */
203 /** \name View Pan Operator (modal drag-pan)
204  * \{ */
205
206 /* for 'redo' only, with no user input */
207 static int view_pan_exec(bContext *C, wmOperator *op)
208 {
209   view_pan_init(C, op);
210   view_pan_apply(C, op);
211   view_pan_exit(op);
212   return OPERATOR_FINISHED;
213 }
214
215 /* set up modal operator and relevant settings */
216 static int view_pan_invoke(bContext *C, wmOperator *op, const wmEvent *event)
217 {
218   wmWindow *window = CTX_wm_window(C);
219   v2dViewPanData *vpd;
220   View2D *v2d;
221
222   /* set up customdata */
223   view_pan_init(C, op);
224
225   vpd = op->customdata;
226   v2d = vpd->v2d;
227
228   /* set initial settings */
229   vpd->startx = vpd->lastx = event->x;
230   vpd->starty = vpd->lasty = event->y;
231   vpd->invoke_event = event->type;
232
233   if (event->type == MOUSEPAN) {
234     RNA_int_set(op->ptr, "deltax", event->prevx - event->x);
235     RNA_int_set(op->ptr, "deltay", event->prevy - event->y);
236
237     view_pan_apply(C, op);
238     view_pan_exit(op);
239     return OPERATOR_FINISHED;
240   }
241
242   RNA_int_set(op->ptr, "deltax", 0);
243   RNA_int_set(op->ptr, "deltay", 0);
244
245   if (v2d->keepofs & V2D_LOCKOFS_X) {
246     WM_cursor_modal_set(window, WM_CURSOR_NS_SCROLL);
247   }
248   else if (v2d->keepofs & V2D_LOCKOFS_Y) {
249     WM_cursor_modal_set(window, WM_CURSOR_EW_SCROLL);
250   }
251   else {
252     WM_cursor_modal_set(window, WM_CURSOR_NSEW_SCROLL);
253   }
254
255   /* add temp handler */
256   WM_event_add_modal_handler(C, op);
257
258   return OPERATOR_RUNNING_MODAL;
259 }
260
261 /* handle user input - calculations of mouse-movement
262  * need to be done here, not in the apply callback! */
263 static int view_pan_modal(bContext *C, wmOperator *op, const wmEvent *event)
264 {
265   v2dViewPanData *vpd = op->customdata;
266
267   /* execute the events */
268   switch (event->type) {
269     case MOUSEMOVE: {
270       /* calculate new delta transform, then store mouse-coordinates for next-time */
271       RNA_int_set(op->ptr, "deltax", (vpd->lastx - event->x));
272       RNA_int_set(op->ptr, "deltay", (vpd->lasty - event->y));
273
274       vpd->lastx = event->x;
275       vpd->lasty = event->y;
276
277       view_pan_apply(C, op);
278       break;
279     }
280     /* XXX - Mode switching isn't implemented. See comments in 36818.
281      * switch to zoom */
282 #if 0
283     case LEFTMOUSE:
284       if (event->val == KM_PRESS) {
285         /* calculate overall delta mouse-movement for redo */
286         RNA_int_set(op->ptr, "deltax", (vpd->startx - vpd->lastx));
287         RNA_int_set(op->ptr, "deltay", (vpd->starty - vpd->lasty));
288
289         view_pan_exit(op);
290         WM_cursor_modal_restore(CTX_wm_window(C));
291         WM_operator_name_call(C, "VIEW2D_OT_zoom", WM_OP_INVOKE_DEFAULT, NULL);
292         return OPERATOR_FINISHED;
293       }
294 #endif
295     default:
296       if (ELEM(event->type, vpd->invoke_event, EVT_ESCKEY)) {
297         if (event->val == KM_RELEASE) {
298           /* calculate overall delta mouse-movement for redo */
299           RNA_int_set(op->ptr, "deltax", (vpd->startx - vpd->lastx));
300           RNA_int_set(op->ptr, "deltay", (vpd->starty - vpd->lasty));
301
302           view_pan_exit(op);
303           WM_cursor_modal_restore(CTX_wm_window(C));
304
305           return OPERATOR_FINISHED;
306         }
307       }
308       break;
309   }
310
311   return OPERATOR_RUNNING_MODAL;
312 }
313
314 static void view_pan_cancel(bContext *UNUSED(C), wmOperator *op)
315 {
316   view_pan_exit(op);
317 }
318
319 static void VIEW2D_OT_pan(wmOperatorType *ot)
320 {
321   /* identifiers */
322   ot->name = "Pan View";
323   ot->description = "Pan the view";
324   ot->idname = "VIEW2D_OT_pan";
325
326   /* api callbacks */
327   ot->exec = view_pan_exec;
328   ot->invoke = view_pan_invoke;
329   ot->modal = view_pan_modal;
330   ot->cancel = view_pan_cancel;
331   ot->poll = view_pan_poll;
332
333   /* operator is modal */
334   ot->flag = OPTYPE_BLOCKING | OPTYPE_GRAB_CURSOR_XY;
335
336   /* rna - must keep these in sync with the other operators */
337   RNA_def_int(ot->srna, "deltax", 0, INT_MIN, INT_MAX, "Delta X", "", INT_MIN, INT_MAX);
338   RNA_def_int(ot->srna, "deltay", 0, INT_MIN, INT_MAX, "Delta Y", "", INT_MIN, INT_MAX);
339 }
340
341 /** \} */
342
343 /* -------------------------------------------------------------------- */
344 /** \name View Edge Pan Operator (modal)
345  *
346  * Scroll the region if the mouse is dragged to an edge. "Invisible" operator that always
347  * passes through.
348  * \{ */
349
350 /** Distance from the edge of the region within which to start panning. */
351 #define EDGE_PAN_REGION_PAD (U.widget_unit)
352 /** Speed factor in pixels per second per pixel of distance from edge pan zone beginning. */
353 #define EDGE_PAN_SPEED_PER_PIXEL (25.0f * (float)U.dpi_fac)
354 /** Delay before drag panning in seconds. */
355 #define EDGE_PAN_DELAY 1.0f
356
357 /* set up modal operator and relevant settings */
358 static int view_edge_pan_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event))
359 {
360   /* Set up customdata. */
361   view_pan_init(C, op);
362
363   v2dViewPanData *vpd = op->customdata;
364
365   vpd->edge_pan_start_time_x = 0.0;
366   vpd->edge_pan_start_time_y = 0.0;
367   vpd->edge_pan_last_time = PIL_check_seconds_timer();
368
369   WM_event_add_modal_handler(C, op);
370
371   return (OPERATOR_RUNNING_MODAL | OPERATOR_PASS_THROUGH);
372 }
373
374 /**
375  * Reset the edge pan timers if the mouse isn't in the scroll zone and
376  * start the timers when the mouse enters a scroll zone.
377  */
378 static void edge_pan_manage_delay_timers(v2dViewPanData *vpd,
379                                          int pan_dir_x,
380                                          int pan_dir_y,
381                                          const double current_time)
382 {
383   if (pan_dir_x == 0) {
384     vpd->edge_pan_start_time_x = 0.0;
385   }
386   else if (vpd->edge_pan_start_time_x == 0.0) {
387     vpd->edge_pan_start_time_x = current_time;
388   }
389   if (pan_dir_y == 0) {
390     vpd->edge_pan_start_time_y = 0.0;
391   }
392   else if (vpd->edge_pan_start_time_y == 0.0) {
393     vpd->edge_pan_start_time_y = current_time;
394   }
395 }
396
397 /**
398  * Used to calculate a "fade in" factor for edge panning to make the interaction feel smooth
399  * and more purposeful.
400  *
401  * \note Assumes a domain_min of 0.0f.
402  */
403 static float smootherstep(const float domain_max, float x)
404 {
405   x = clamp_f(x / domain_max, 0.0, 1.0);
406   return x * x * x * (x * (x * 6.0 - 15.0) + 10.0);
407 }
408
409 static float edge_pan_speed(v2dViewPanData *vpd,
410                             int event_loc,
411                             bool x_dir,
412                             const double current_time)
413 {
414   ARegion *region = vpd->region;
415
416   /* Find the distance from the start of the drag zone. */
417   const int min = (x_dir ? region->winrct.xmin : region->winrct.ymin) + EDGE_PAN_REGION_PAD;
418   const int max = (x_dir ? region->winrct.xmax : region->winrct.ymax) - EDGE_PAN_REGION_PAD;
419   int distance = 0.0;
420   if (event_loc > max) {
421     distance = event_loc - max;
422   }
423   else if (event_loc < min) {
424     distance = min - event_loc;
425   }
426   else {
427     BLI_assert(!"Calculating speed outside of pan zones");
428     return 0.0f;
429   }
430
431   /* Apply a fade in to the speed based on a start time delay. */
432   const double start_time = x_dir ? vpd->edge_pan_start_time_x : vpd->edge_pan_start_time_y;
433   const float delay_factor = smootherstep(EDGE_PAN_DELAY, (float)(current_time - start_time));
434
435   return distance * EDGE_PAN_SPEED_PER_PIXEL * delay_factor;
436 }
437
438 static int view_edge_pan_modal(bContext *C, wmOperator *op, const wmEvent *event)
439 {
440   v2dViewPanData *vpd = op->customdata;
441   ARegion *region = vpd->region;
442
443   if (event->val == KM_RELEASE || event->type == EVT_ESCKEY) {
444     view_pan_exit(op);
445     return (OPERATOR_FINISHED | OPERATOR_PASS_THROUGH);
446   }
447   /* Only mousemove events matter here, ignore others. */
448   if (event->type != MOUSEMOVE) {
449     return OPERATOR_PASS_THROUGH;
450   }
451
452   /* This operator is supposed to run together with some drag action.
453    * On successful handling, always pass events on to other handlers. */
454   const int success_retval = OPERATOR_PASS_THROUGH;
455
456   const int outside_padding = RNA_int_get(op->ptr, "outside_padding") * UI_UNIT_X;
457   rcti padding_rect;
458   if (outside_padding != 0) {
459     padding_rect = region->winrct;
460     BLI_rcti_pad(&padding_rect, outside_padding, outside_padding);
461   }
462
463   int pan_dir_x = 0;
464   int pan_dir_y = 0;
465   if ((outside_padding == 0) || BLI_rcti_isect_pt(&padding_rect, event->x, event->y)) {
466     /* Find whether the mouse is beyond X and Y edges. */
467     if (event->x > region->winrct.xmax - EDGE_PAN_REGION_PAD) {
468       pan_dir_x = 1;
469     }
470     else if (event->x < region->winrct.xmin + EDGE_PAN_REGION_PAD) {
471       pan_dir_x = -1;
472     }
473     if (event->y > region->winrct.ymax - EDGE_PAN_REGION_PAD) {
474       pan_dir_y = 1;
475     }
476     else if (event->y < region->winrct.ymin + EDGE_PAN_REGION_PAD) {
477       pan_dir_y = -1;
478     }
479   }
480
481   const double current_time = PIL_check_seconds_timer();
482   edge_pan_manage_delay_timers(vpd, pan_dir_x, pan_dir_y, current_time);
483
484   /* Calculate the delta since the last time the operator was called. */
485   const float dtime = (float)(current_time - vpd->edge_pan_last_time);
486   float dx = 0.0f, dy = 0.0f;
487   if (pan_dir_x != 0) {
488     const float speed = edge_pan_speed(vpd, event->x, true, current_time);
489     dx = dtime * speed * (float)pan_dir_x;
490   }
491   if (pan_dir_y != 0) {
492     const float speed = edge_pan_speed(vpd, event->y, false, current_time);
493     dy = dtime * speed * (float)pan_dir_y;
494   }
495   vpd->edge_pan_last_time = current_time;
496
497   /* Pan, clamping inside the regions's total bounds. */
498   view_pan_apply_ex(C, vpd, dx, dy);
499
500   return success_retval;
501 }
502
503 static void view_edge_pan_cancel(bContext *UNUSED(C), wmOperator *op)
504 {
505   view_pan_exit(op);
506 }
507
508 static void VIEW2D_OT_edge_pan(wmOperatorType *ot)
509 {
510   /* identifiers */
511   ot->name = "View Edge Pan";
512   ot->description = "Pan the view when the mouse is held at an edge";
513   ot->idname = "VIEW2D_OT_edge_pan";
514
515   /* api callbacks */
516   ot->invoke = view_edge_pan_invoke;
517   ot->modal = view_edge_pan_modal;
518   ot->cancel = view_edge_pan_cancel;
519   ot->poll = view_pan_poll;
520
521   /* operator is modal */
522   ot->flag = OPTYPE_INTERNAL;
523   RNA_def_int(ot->srna,
524               "outside_padding",
525               0,
526               0,
527               100,
528               "Outside Padding",
529               "Padding around the region in UI units within which panning is activated (0 to "
530               "disable boundary)",
531               0,
532               100);
533 }
534
535 #undef EDGE_PAN_REGION_PAD
536 #undef EDGE_PAN_SPEED_PER_PIXEL
537 #undef EDGE_PAN_DELAY
538
539 /** \} */
540
541 /* -------------------------------------------------------------------- */
542 /** \name View Pan Operator (single step)
543  * \{ */
544
545 /* this operator only needs this single callback, where it calls the view_pan_*() methods */
546 static int view_scrollright_exec(bContext *C, wmOperator *op)
547 {
548   v2dViewPanData *vpd;
549
550   /* initialize default settings (and validate if ok to run) */
551   view_pan_init(C, op);
552
553   /* also, check if can pan in horizontal axis */
554   vpd = op->customdata;
555   if (vpd->v2d->keepofs & V2D_LOCKOFS_X) {
556     view_pan_exit(op);
557     return OPERATOR_PASS_THROUGH;
558   }
559
560   /* set RNA-Props - only movement in positive x-direction */
561   RNA_int_set(op->ptr, "deltax", 40);
562   RNA_int_set(op->ptr, "deltay", 0);
563
564   /* apply movement, then we're done */
565   view_pan_apply(C, op);
566   view_pan_exit(op);
567
568   return OPERATOR_FINISHED;
569 }
570
571 static void VIEW2D_OT_scroll_right(wmOperatorType *ot)
572 {
573   /* identifiers */
574   ot->name = "Scroll Right";
575   ot->description = "Scroll the view right";
576   ot->idname = "VIEW2D_OT_scroll_right";
577
578   /* api callbacks */
579   ot->exec = view_scrollright_exec;
580   ot->poll = view_pan_poll;
581
582   /* rna - must keep these in sync with the other operators */
583   RNA_def_int(ot->srna, "deltax", 0, INT_MIN, INT_MAX, "Delta X", "", INT_MIN, INT_MAX);
584   RNA_def_int(ot->srna, "deltay", 0, INT_MIN, INT_MAX, "Delta Y", "", INT_MIN, INT_MAX);
585 }
586
587 /* this operator only needs this single callback, where it calls the view_pan_*() methods */
588 static int view_scrollleft_exec(bContext *C, wmOperator *op)
589 {
590   v2dViewPanData *vpd;
591
592   /* initialize default settings (and validate if ok to run) */
593   view_pan_init(C, op);
594
595   /* also, check if can pan in horizontal axis */
596   vpd = op->customdata;
597   if (vpd->v2d->keepofs & V2D_LOCKOFS_X) {
598     view_pan_exit(op);
599     return OPERATOR_PASS_THROUGH;
600   }
601
602   /* set RNA-Props - only movement in negative x-direction */
603   RNA_int_set(op->ptr, "deltax", -40);
604   RNA_int_set(op->ptr, "deltay", 0);
605
606   /* apply movement, then we're done */
607   view_pan_apply(C, op);
608   view_pan_exit(op);
609
610   return OPERATOR_FINISHED;
611 }
612
613 static void VIEW2D_OT_scroll_left(wmOperatorType *ot)
614 {
615   /* identifiers */
616   ot->name = "Scroll Left";
617   ot->description = "Scroll the view left";
618   ot->idname = "VIEW2D_OT_scroll_left";
619
620   /* api callbacks */
621   ot->exec = view_scrollleft_exec;
622   ot->poll = view_pan_poll;
623
624   /* rna - must keep these in sync with the other operators */
625   RNA_def_int(ot->srna, "deltax", 0, INT_MIN, INT_MAX, "Delta X", "", INT_MIN, INT_MAX);
626   RNA_def_int(ot->srna, "deltay", 0, INT_MIN, INT_MAX, "Delta Y", "", INT_MIN, INT_MAX);
627 }
628
629 /* this operator only needs this single callback, where it calls the view_pan_*() methods */
630 static int view_scrolldown_exec(bContext *C, wmOperator *op)
631 {
632   v2dViewPanData *vpd;
633
634   /* initialize default settings (and validate if ok to run) */
635   view_pan_init(C, op);
636
637   /* also, check if can pan in vertical axis */
638   vpd = op->customdata;
639   if (vpd->v2d->keepofs & V2D_LOCKOFS_Y) {
640     view_pan_exit(op);
641     return OPERATOR_PASS_THROUGH;
642   }
643
644   /* set RNA-Props */
645   RNA_int_set(op->ptr, "deltax", 0);
646   RNA_int_set(op->ptr, "deltay", -40);
647
648   PropertyRNA *prop = RNA_struct_find_property(op->ptr, "page");
649   if (RNA_property_is_set(op->ptr, prop) && RNA_property_boolean_get(op->ptr, prop)) {
650     ARegion *region = CTX_wm_region(C);
651     RNA_int_set(op->ptr, "deltay", region->v2d.mask.ymin - region->v2d.mask.ymax);
652   }
653
654   /* apply movement, then we're done */
655   view_pan_apply(C, op);
656   view_pan_exit(op);
657
658   return OPERATOR_FINISHED;
659 }
660
661 static void VIEW2D_OT_scroll_down(wmOperatorType *ot)
662 {
663   /* identifiers */
664   ot->name = "Scroll Down";
665   ot->description = "Scroll the view down";
666   ot->idname = "VIEW2D_OT_scroll_down";
667
668   /* api callbacks */
669   ot->exec = view_scrolldown_exec;
670   ot->poll = view_pan_poll;
671
672   /* rna - must keep these in sync with the other operators */
673   RNA_def_int(ot->srna, "deltax", 0, INT_MIN, INT_MAX, "Delta X", "", INT_MIN, INT_MAX);
674   RNA_def_int(ot->srna, "deltay", 0, INT_MIN, INT_MAX, "Delta Y", "", INT_MIN, INT_MAX);
675   RNA_def_boolean(ot->srna, "page", 0, "Page", "Scroll down one page");
676 }
677
678 /* this operator only needs this single callback, where it calls the view_pan_*() methods */
679 static int view_scrollup_exec(bContext *C, wmOperator *op)
680 {
681   v2dViewPanData *vpd;
682
683   /* initialize default settings (and validate if ok to run) */
684   view_pan_init(C, op);
685
686   /* also, check if can pan in vertical axis */
687   vpd = op->customdata;
688   if (vpd->v2d->keepofs & V2D_LOCKOFS_Y) {
689     view_pan_exit(op);
690     return OPERATOR_PASS_THROUGH;
691   }
692
693   /* set RNA-Props */
694   RNA_int_set(op->ptr, "deltax", 0);
695   RNA_int_set(op->ptr, "deltay", 40);
696
697   PropertyRNA *prop = RNA_struct_find_property(op->ptr, "page");
698   if (RNA_property_is_set(op->ptr, prop) && RNA_property_boolean_get(op->ptr, prop)) {
699     ARegion *region = CTX_wm_region(C);
700     RNA_int_set(op->ptr, "deltay", BLI_rcti_size_y(&region->v2d.mask));
701   }
702
703   /* apply movement, then we're done */
704   view_pan_apply(C, op);
705   view_pan_exit(op);
706
707   return OPERATOR_FINISHED;
708 }
709
710 static void VIEW2D_OT_scroll_up(wmOperatorType *ot)
711 {
712   /* identifiers */
713   ot->name = "Scroll Up";
714   ot->description = "Scroll the view up";
715   ot->idname = "VIEW2D_OT_scroll_up";
716
717   /* api callbacks */
718   ot->exec = view_scrollup_exec;
719   ot->poll = view_pan_poll;
720
721   /* rna - must keep these in sync with the other operators */
722   RNA_def_int(ot->srna, "deltax", 0, INT_MIN, INT_MAX, "Delta X", "", INT_MIN, INT_MAX);
723   RNA_def_int(ot->srna, "deltay", 0, INT_MIN, INT_MAX, "Delta Y", "", INT_MIN, INT_MAX);
724   RNA_def_boolean(ot->srna, "page", 0, "Page", "Scroll up one page");
725 }
726
727 /** \} */
728
729 /* -------------------------------------------------------------------- */
730 /** \name View Zoom Shared Utilities
731  * \{ */
732
733 /**
734  * This group of operators come in several forms:
735  * -# Scroll-wheel 'steps' - rolling mouse-wheel by one step zooms view by predefined amount.
736  * -# Scroll-wheel 'steps' + alt + ctrl/shift - zooms view on one axis only (ctrl=x, shift=y).
737  *    XXX this could be implemented...
738  * -# Pad +/- Keys - pressing each key moves the zooms the view by a predefined amount.
739  *
740  * In order to make sure this works, each operator must define the following RNA-Operator Props:
741  *
742  * - zoomfacx, zoomfacy - These two zoom factors allow for non-uniform scaling.
743  *   It is safe to scale by 0, as these factors are used to determine.
744  *   amount to enlarge 'cur' by.
745  */
746
747 /**
748  * Temporary custom-data for operator.
749  */
750 typedef struct v2dViewZoomData {
751   View2D *v2d; /* view2d we're operating in */
752   ARegion *region;
753
754   /* needed for continuous zoom */
755   wmTimer *timer;
756   double timer_lastdraw;
757
758   int lastx, lasty;   /* previous x/y values of mouse in window */
759   int invoke_event;   /* event type that invoked, for modal exits */
760   float dx, dy;       /* running tally of previous delta values (for obtaining final zoom) */
761   float mx_2d, my_2d; /* initial mouse location in v2d coords */
762   bool zoom_to_mouse_pos;
763 } v2dViewZoomData;
764
765 /**
766  * Clamp by convention rather then locking flags,
767  * for ndof and +/- keys
768  */
769 static void view_zoom_axis_lock_defaults(bContext *C, bool r_do_zoom_xy[2])
770 {
771   ScrArea *area = CTX_wm_area(C);
772
773   r_do_zoom_xy[0] = true;
774   r_do_zoom_xy[1] = true;
775
776   /* default not to zoom the sequencer vertically */
777   if (area && area->spacetype == SPACE_SEQ) {
778     ARegion *region = CTX_wm_region(C);
779
780     if (region && region->regiontype == RGN_TYPE_WINDOW) {
781       r_do_zoom_xy[1] = false;
782     }
783   }
784 }
785
786 /* check if step-zoom can be applied */
787 static bool view_zoom_poll(bContext *C)
788 {
789   ARegion *region = CTX_wm_region(C);
790   View2D *v2d;
791
792   /* check if there's a region in context to work with */
793   if (region == NULL) {
794     return false;
795   }
796
797   /* Do not show that in 3DView context. */
798   if (CTX_wm_region_view3d(C)) {
799     return false;
800   }
801
802   v2d = &region->v2d;
803
804   /* check that 2d-view is zoomable */
805   if ((v2d->keepzoom & V2D_LOCKZOOM_X) && (v2d->keepzoom & V2D_LOCKZOOM_Y)) {
806     return false;
807   }
808
809   /* view is zoomable */
810   return true;
811 }
812
813 /* initialize panning customdata */
814 static void view_zoomdrag_init(bContext *C, wmOperator *op)
815 {
816   /* Should've been checked before. */
817   BLI_assert(view_zoom_poll(C));
818
819   /* set custom-data for operator */
820   v2dViewZoomData *vzd = MEM_callocN(sizeof(v2dViewZoomData), "v2dViewZoomData");
821   op->customdata = vzd;
822
823   /* set pointers to owners */
824   vzd->region = CTX_wm_region(C);
825   vzd->v2d = &vzd->region->v2d;
826   /* False by default. Interactive callbacks (ie invoke()) can set it to true. */
827   vzd->zoom_to_mouse_pos = false;
828 }
829
830 /* apply transform to view (i.e. adjust 'cur' rect) */
831 static void view_zoomstep_apply_ex(bContext *C,
832                                    v2dViewZoomData *vzd,
833                                    const float facx,
834                                    const float facy)
835 {
836   ARegion *region = CTX_wm_region(C);
837   View2D *v2d = &region->v2d;
838   const rctf cur_old = v2d->cur;
839   float dx, dy;
840   const int snap_test = ED_region_snap_size_test(region);
841
842   /* calculate amount to move view by, ensuring symmetry so the
843    * old zoom level is restored after zooming back the same amount
844    */
845   if (facx >= 0.0f) {
846     dx = BLI_rctf_size_x(&v2d->cur) * facx;
847     dy = BLI_rctf_size_y(&v2d->cur) * facy;
848   }
849   else {
850     dx = (BLI_rctf_size_x(&v2d->cur) / (1.0f + 2.0f * facx)) * facx;
851     dy = (BLI_rctf_size_y(&v2d->cur) / (1.0f + 2.0f * facy)) * facy;
852   }
853
854   /* only resize view on an axis if change is allowed */
855   if ((v2d->keepzoom & V2D_LOCKZOOM_X) == 0) {
856     if (v2d->keepofs & V2D_LOCKOFS_X) {
857       v2d->cur.xmax -= 2 * dx;
858     }
859     else if (v2d->keepofs & V2D_KEEPOFS_X) {
860       if (v2d->align & V2D_ALIGN_NO_POS_X) {
861         v2d->cur.xmin += 2 * dx;
862       }
863       else {
864         v2d->cur.xmax -= 2 * dx;
865       }
866     }
867     else {
868
869       v2d->cur.xmin += dx;
870       v2d->cur.xmax -= dx;
871
872       if (vzd->zoom_to_mouse_pos) {
873         /* get zoom fac the same way as in
874          * ui_view2d_curRect_validate_resize - better keep in sync! */
875         const float zoomx = (float)(BLI_rcti_size_x(&v2d->mask) + 1) / BLI_rctf_size_x(&v2d->cur);
876
877         /* only move view to mouse if zoom fac is inside minzoom/maxzoom */
878         if (((v2d->keepzoom & V2D_LIMITZOOM) == 0) ||
879             IN_RANGE_INCL(zoomx, v2d->minzoom, v2d->maxzoom)) {
880           const float mval_fac = (vzd->mx_2d - cur_old.xmin) / BLI_rctf_size_x(&cur_old);
881           const float mval_faci = 1.0f - mval_fac;
882           const float ofs = (mval_fac * dx) - (mval_faci * dx);
883
884           v2d->cur.xmin += ofs;
885           v2d->cur.xmax += ofs;
886         }
887       }
888     }
889   }
890   if ((v2d->keepzoom & V2D_LOCKZOOM_Y) == 0) {
891     if (v2d->keepofs & V2D_LOCKOFS_Y) {
892       v2d->cur.ymax -= 2 * dy;
893     }
894     else if (v2d->keepofs & V2D_KEEPOFS_Y) {
895       if (v2d->align & V2D_ALIGN_NO_POS_Y) {
896         v2d->cur.ymin += 2 * dy;
897       }
898       else {
899         v2d->cur.ymax -= 2 * dy;
900       }
901     }
902     else {
903
904       v2d->cur.ymin += dy;
905       v2d->cur.ymax -= dy;
906
907       if (vzd->zoom_to_mouse_pos) {
908         /* get zoom fac the same way as in
909          * ui_view2d_curRect_validate_resize - better keep in sync! */
910         const float zoomy = (float)(BLI_rcti_size_y(&v2d->mask) + 1) / BLI_rctf_size_y(&v2d->cur);
911
912         /* only move view to mouse if zoom fac is inside minzoom/maxzoom */
913         if (((v2d->keepzoom & V2D_LIMITZOOM) == 0) ||
914             IN_RANGE_INCL(zoomy, v2d->minzoom, v2d->maxzoom)) {
915           const float mval_fac = (vzd->my_2d - cur_old.ymin) / BLI_rctf_size_y(&cur_old);
916           const float mval_faci = 1.0f - mval_fac;
917           const float ofs = (mval_fac * dy) - (mval_faci * dy);
918
919           v2d->cur.ymin += ofs;
920           v2d->cur.ymax += ofs;
921         }
922       }
923     }
924   }
925
926   /* Inform v2d about changes after this operation. */
927   UI_view2d_curRect_changed(C, v2d);
928
929   if (ED_region_snap_size_apply(region, snap_test)) {
930     ScrArea *area = CTX_wm_area(C);
931     ED_area_tag_redraw(area);
932     WM_event_add_notifier(C, NC_SCREEN | NA_EDITED, NULL);
933   }
934
935   /* request updates to be done... */
936   ED_region_tag_redraw_no_rebuild(vzd->region);
937   UI_view2d_sync(CTX_wm_screen(C), CTX_wm_area(C), v2d, V2D_LOCK_COPY);
938 }
939
940 static void view_zoomstep_apply(bContext *C, wmOperator *op)
941 {
942   v2dViewZoomData *vzd = op->customdata;
943   view_zoomstep_apply_ex(
944       C, vzd, RNA_float_get(op->ptr, "zoomfacx"), RNA_float_get(op->ptr, "zoomfacy"));
945 }
946
947 /** \} */
948
949 /* -------------------------------------------------------------------- */
950 /** \name View Zoom Operator (single step)
951  * \{ */
952
953 /* cleanup temp customdata  */
954 static void view_zoomstep_exit(wmOperator *op)
955 {
956   UI_view2d_zoom_cache_reset();
957
958   if (op->customdata) {
959     MEM_freeN(op->customdata);
960     op->customdata = NULL;
961   }
962 }
963
964 /* this operator only needs this single callback, where it calls the view_zoom_*() methods */
965 static int view_zoomin_exec(bContext *C, wmOperator *op)
966 {
967   if (op->customdata == NULL) { /* Might have been setup in _invoke() already. */
968     view_zoomdrag_init(C, op);
969   }
970
971   bool do_zoom_xy[2];
972   view_zoom_axis_lock_defaults(C, do_zoom_xy);
973
974   /* set RNA-Props - zooming in by uniform factor */
975   RNA_float_set(op->ptr, "zoomfacx", do_zoom_xy[0] ? 0.0375f : 0.0f);
976   RNA_float_set(op->ptr, "zoomfacy", do_zoom_xy[1] ? 0.0375f : 0.0f);
977
978   /* apply movement, then we're done */
979   view_zoomstep_apply(C, op);
980
981   view_zoomstep_exit(op);
982
983   return OPERATOR_FINISHED;
984 }
985
986 static int view_zoomin_invoke(bContext *C, wmOperator *op, const wmEvent *event)
987 {
988   view_zoomdrag_init(C, op);
989
990   v2dViewZoomData *vzd = op->customdata;
991
992   if (U.uiflag & USER_ZOOM_TO_MOUSEPOS) {
993     ARegion *region = CTX_wm_region(C);
994
995     /* store initial mouse position (in view space) */
996     UI_view2d_region_to_view(
997         &region->v2d, event->mval[0], event->mval[1], &vzd->mx_2d, &vzd->my_2d);
998     vzd->zoom_to_mouse_pos = true;
999   }
1000
1001   return view_zoomin_exec(C, op);
1002 }
1003
1004 static void VIEW2D_OT_zoom_in(wmOperatorType *ot)
1005 {
1006   PropertyRNA *prop;
1007
1008   /* identifiers */
1009   ot->name = "Zoom In";
1010   ot->description = "Zoom in the view";
1011   ot->idname = "VIEW2D_OT_zoom_in";
1012
1013   /* api callbacks */
1014   ot->invoke = view_zoomin_invoke;
1015   ot->exec = view_zoomin_exec;
1016   ot->poll = view_zoom_poll;
1017
1018   /* rna - must keep these in sync with the other operators */
1019   prop = RNA_def_float(
1020       ot->srna, "zoomfacx", 0, -FLT_MAX, FLT_MAX, "Zoom Factor X", "", -FLT_MAX, FLT_MAX);
1021   RNA_def_property_flag(prop, PROP_HIDDEN);
1022   prop = RNA_def_float(
1023       ot->srna, "zoomfacy", 0, -FLT_MAX, FLT_MAX, "Zoom Factor Y", "", -FLT_MAX, FLT_MAX);
1024   RNA_def_property_flag(prop, PROP_HIDDEN);
1025 }
1026
1027 /* this operator only needs this single callback, where it calls the view_zoom_*() methods */
1028 static int view_zoomout_exec(bContext *C, wmOperator *op)
1029 {
1030   bool do_zoom_xy[2];
1031
1032   if (op->customdata == NULL) { /* Might have been setup in _invoke() already. */
1033     view_zoomdrag_init(C, op);
1034   }
1035
1036   view_zoom_axis_lock_defaults(C, do_zoom_xy);
1037
1038   /* set RNA-Props - zooming in by uniform factor */
1039   RNA_float_set(op->ptr, "zoomfacx", do_zoom_xy[0] ? -0.0375f : 0.0f);
1040   RNA_float_set(op->ptr, "zoomfacy", do_zoom_xy[1] ? -0.0375f : 0.0f);
1041
1042   /* apply movement, then we're done */
1043   view_zoomstep_apply(C, op);
1044
1045   view_zoomstep_exit(op);
1046
1047   return OPERATOR_FINISHED;
1048 }
1049
1050 static int view_zoomout_invoke(bContext *C, wmOperator *op, const wmEvent *event)
1051 {
1052   view_zoomdrag_init(C, op);
1053
1054   v2dViewZoomData *vzd = op->customdata;
1055
1056   if (U.uiflag & USER_ZOOM_TO_MOUSEPOS) {
1057     ARegion *region = CTX_wm_region(C);
1058
1059     /* store initial mouse position (in view space) */
1060     UI_view2d_region_to_view(
1061         &region->v2d, event->mval[0], event->mval[1], &vzd->mx_2d, &vzd->my_2d);
1062     vzd->zoom_to_mouse_pos = true;
1063   }
1064
1065   return view_zoomout_exec(C, op);
1066 }
1067
1068 static void VIEW2D_OT_zoom_out(wmOperatorType *ot)
1069 {
1070   PropertyRNA *prop;
1071
1072   /* identifiers */
1073   ot->name = "Zoom Out";
1074   ot->description = "Zoom out the view";
1075   ot->idname = "VIEW2D_OT_zoom_out";
1076
1077   /* api callbacks */
1078   ot->invoke = view_zoomout_invoke;
1079   ot->exec = view_zoomout_exec;
1080
1081   ot->poll = view_zoom_poll;
1082
1083   /* rna - must keep these in sync with the other operators */
1084   prop = RNA_def_float(
1085       ot->srna, "zoomfacx", 0, -FLT_MAX, FLT_MAX, "Zoom Factor X", "", -FLT_MAX, FLT_MAX);
1086   RNA_def_property_flag(prop, PROP_HIDDEN);
1087   prop = RNA_def_float(
1088       ot->srna, "zoomfacy", 0, -FLT_MAX, FLT_MAX, "Zoom Factor Y", "", -FLT_MAX, FLT_MAX);
1089   RNA_def_property_flag(prop, PROP_HIDDEN);
1090 }
1091
1092 /** \} */
1093
1094 /* -------------------------------------------------------------------- */
1095 /** \name View Zoom Operator (modal drag-zoom)
1096  * \{ */
1097
1098 /**
1099  * MMB Drag - allows non-uniform scaling by dragging mouse
1100  *
1101  * In order to make sure this works, each operator must define the following RNA-Operator Props:
1102  * - `deltax, deltay` - amounts to add to each side of the 'cur' rect
1103  */
1104
1105 /* apply transform to view (i.e. adjust 'cur' rect) */
1106 static void view_zoomdrag_apply(bContext *C, wmOperator *op)
1107 {
1108   v2dViewZoomData *vzd = op->customdata;
1109   View2D *v2d = vzd->v2d;
1110   float dx, dy;
1111   const int snap_test = ED_region_snap_size_test(vzd->region);
1112
1113   const bool use_cursor_init = RNA_boolean_get(op->ptr, "use_cursor_init");
1114   const bool zoom_to_pos = use_cursor_init && vzd->zoom_to_mouse_pos;
1115
1116   /* get amount to move view by */
1117   dx = RNA_float_get(op->ptr, "deltax") / U.dpi_fac;
1118   dy = RNA_float_get(op->ptr, "deltay") / U.dpi_fac;
1119
1120   if (U.uiflag & USER_ZOOM_INVERT) {
1121     dx *= -1;
1122     dy *= -1;
1123   }
1124
1125   /* Check if the 'timer' is initialized, as zooming with the trackpad
1126    * never uses the "Continuous" zoom method, and the 'timer' is not initialized. */
1127   if ((U.viewzoom == USER_ZOOM_CONT) && vzd->timer) { /* XXX store this setting as RNA prop? */
1128     const double time = PIL_check_seconds_timer();
1129     const float time_step = (float)(time - vzd->timer_lastdraw);
1130
1131     dx *= time_step * 0.5f;
1132     dy *= time_step * 0.5f;
1133
1134     vzd->timer_lastdraw = time;
1135   }
1136
1137   /* only move view on an axis if change is allowed */
1138   if ((v2d->keepzoom & V2D_LOCKZOOM_X) == 0) {
1139     if (v2d->keepofs & V2D_LOCKOFS_X) {
1140       v2d->cur.xmax -= 2 * dx;
1141     }
1142     else {
1143       if (zoom_to_pos) {
1144         const float mval_fac = (vzd->mx_2d - v2d->cur.xmin) / BLI_rctf_size_x(&v2d->cur);
1145         const float mval_faci = 1.0f - mval_fac;
1146         const float ofs = (mval_fac * dx) - (mval_faci * dx);
1147
1148         v2d->cur.xmin += ofs + dx;
1149         v2d->cur.xmax += ofs - dx;
1150       }
1151       else {
1152         v2d->cur.xmin += dx;
1153         v2d->cur.xmax -= dx;
1154       }
1155     }
1156   }
1157   if ((v2d->keepzoom & V2D_LOCKZOOM_Y) == 0) {
1158     if (v2d->keepofs & V2D_LOCKOFS_Y) {
1159       v2d->cur.ymax -= 2 * dy;
1160     }
1161     else {
1162       if (zoom_to_pos) {
1163         const float mval_fac = (vzd->my_2d - v2d->cur.ymin) / BLI_rctf_size_y(&v2d->cur);
1164         const float mval_faci = 1.0f - mval_fac;
1165         const float ofs = (mval_fac * dy) - (mval_faci * dy);
1166
1167         v2d->cur.ymin += ofs + dy;
1168         v2d->cur.ymax += ofs - dy;
1169       }
1170       else {
1171         v2d->cur.ymin += dy;
1172         v2d->cur.ymax -= dy;
1173       }
1174     }
1175   }
1176
1177   /* Inform v2d about changes after this operation. */
1178   UI_view2d_curRect_changed(C, v2d);
1179
1180   if (ED_region_snap_size_apply(vzd->region, snap_test)) {
1181     ScrArea *area = CTX_wm_area(C);
1182     ED_area_tag_redraw(area);
1183     WM_event_add_notifier(C, NC_SCREEN | NA_EDITED, NULL);
1184   }
1185
1186   /* request updates to be done... */
1187   ED_region_tag_redraw_no_rebuild(vzd->region);
1188   UI_view2d_sync(CTX_wm_screen(C), CTX_wm_area(C), v2d, V2D_LOCK_COPY);
1189 }
1190
1191 /* cleanup temp customdata  */
1192 static void view_zoomdrag_exit(bContext *C, wmOperator *op)
1193 {
1194   UI_view2d_zoom_cache_reset();
1195
1196   if (op->customdata) {
1197     v2dViewZoomData *vzd = op->customdata;
1198
1199     if (vzd->timer) {
1200       WM_event_remove_timer(CTX_wm_manager(C), CTX_wm_window(C), vzd->timer);
1201     }
1202
1203     MEM_freeN(op->customdata);
1204     op->customdata = NULL;
1205   }
1206 }
1207
1208 static void view_zoomdrag_cancel(bContext *C, wmOperator *op)
1209 {
1210   view_zoomdrag_exit(C, op);
1211 }
1212
1213 /* for 'redo' only, with no user input */
1214 static int view_zoomdrag_exec(bContext *C, wmOperator *op)
1215 {
1216   view_zoomdrag_init(C, op);
1217   view_zoomdrag_apply(C, op);
1218   view_zoomdrag_exit(C, op);
1219   return OPERATOR_FINISHED;
1220 }
1221
1222 /* set up modal operator and relevant settings */
1223 static int view_zoomdrag_invoke(bContext *C, wmOperator *op, const wmEvent *event)
1224 {
1225   wmWindow *window = CTX_wm_window(C);
1226   v2dViewZoomData *vzd;
1227   View2D *v2d;
1228
1229   /* set up customdata */
1230   view_zoomdrag_init(C, op);
1231
1232   vzd = op->customdata;
1233   v2d = vzd->v2d;
1234
1235   if (U.uiflag & USER_ZOOM_TO_MOUSEPOS) {
1236     ARegion *region = CTX_wm_region(C);
1237
1238     /* Store initial mouse position (in view space). */
1239     UI_view2d_region_to_view(
1240         &region->v2d, event->mval[0], event->mval[1], &vzd->mx_2d, &vzd->my_2d);
1241     vzd->zoom_to_mouse_pos = true;
1242   }
1243
1244   if (ELEM(event->type, MOUSEZOOM, MOUSEPAN)) {
1245     float dx, dy, fac;
1246
1247     vzd->lastx = event->prevx;
1248     vzd->lasty = event->prevy;
1249
1250     /* As we have only 1D information (magnify value), feed both axes
1251      * with magnify information that is stored in x axis
1252      */
1253     fac = 0.01f * (event->prevx - event->x);
1254     dx = fac * BLI_rctf_size_x(&v2d->cur) / 10.0f;
1255     if (event->type == MOUSEPAN) {
1256       fac = 0.01f * (event->prevy - event->y);
1257     }
1258     dy = fac * BLI_rctf_size_y(&v2d->cur) / 10.0f;
1259
1260     /* support trackpad zoom to always zoom entirely - the v2d code uses portrait or
1261      * landscape exceptions */
1262     if (v2d->keepzoom & V2D_KEEPASPECT) {
1263       if (fabsf(dx) > fabsf(dy)) {
1264         dy = dx;
1265       }
1266       else {
1267         dx = dy;
1268       }
1269     }
1270     RNA_float_set(op->ptr, "deltax", dx);
1271     RNA_float_set(op->ptr, "deltay", dy);
1272
1273     view_zoomdrag_apply(C, op);
1274     view_zoomdrag_exit(C, op);
1275     return OPERATOR_FINISHED;
1276   }
1277
1278   /* set initial settings */
1279   vzd->lastx = event->x;
1280   vzd->lasty = event->y;
1281   RNA_float_set(op->ptr, "deltax", 0);
1282   RNA_float_set(op->ptr, "deltay", 0);
1283
1284   /* for modal exit test */
1285   vzd->invoke_event = event->type;
1286
1287   if (v2d->keepofs & V2D_LOCKOFS_X) {
1288     WM_cursor_modal_set(window, WM_CURSOR_NS_SCROLL);
1289   }
1290   else if (v2d->keepofs & V2D_LOCKOFS_Y) {
1291     WM_cursor_modal_set(window, WM_CURSOR_EW_SCROLL);
1292   }
1293   else {
1294     WM_cursor_modal_set(window, WM_CURSOR_NSEW_SCROLL);
1295   }
1296
1297   /* add temp handler */
1298   WM_event_add_modal_handler(C, op);
1299
1300   if (U.viewzoom == USER_ZOOM_CONT) {
1301     /* needs a timer to continue redrawing */
1302     vzd->timer = WM_event_add_timer(CTX_wm_manager(C), window, TIMER, 0.01f);
1303     vzd->timer_lastdraw = PIL_check_seconds_timer();
1304   }
1305
1306   return OPERATOR_RUNNING_MODAL;
1307 }
1308
1309 /* handle user input - calculations of mouse-movement need to be done here,
1310  * not in the apply callback! */
1311 static int view_zoomdrag_modal(bContext *C, wmOperator *op, const wmEvent *event)
1312 {
1313   v2dViewZoomData *vzd = op->customdata;
1314   View2D *v2d = vzd->v2d;
1315
1316   /* execute the events */
1317   if (event->type == TIMER && event->customdata == vzd->timer) {
1318     view_zoomdrag_apply(C, op);
1319   }
1320   else if (event->type == MOUSEMOVE) {
1321     float dx, dy;
1322     float zoomfac = 0.01f;
1323
1324     /* some view2d's (graph) don't have min/max zoom, or extreme ones */
1325     if (v2d->maxzoom > 0.0f) {
1326       zoomfac = clamp_f(0.001f * v2d->maxzoom, 0.001f, 0.01f);
1327     }
1328
1329     /* calculate new delta transform, based on zooming mode */
1330     if (U.viewzoom == USER_ZOOM_SCALE) {
1331       /* 'scale' zooming */
1332       float dist;
1333       float len_old[2];
1334       float len_new[2];
1335
1336       /* x-axis transform */
1337       dist = BLI_rcti_size_x(&v2d->mask) / 2.0f;
1338       len_old[0] = fabsf(vzd->lastx - vzd->region->winrct.xmin - dist);
1339       len_new[0] = fabsf(event->x - vzd->region->winrct.xmin - dist);
1340
1341       len_old[0] *= zoomfac * BLI_rctf_size_x(&v2d->cur);
1342       len_new[0] *= zoomfac * BLI_rctf_size_x(&v2d->cur);
1343
1344       /* y-axis transform */
1345       dist = BLI_rcti_size_y(&v2d->mask) / 2.0f;
1346       len_old[1] = fabsf(vzd->lasty - vzd->region->winrct.ymin - dist);
1347       len_new[1] = fabsf(event->y - vzd->region->winrct.ymin - dist);
1348
1349       len_old[1] *= zoomfac * BLI_rctf_size_y(&v2d->cur);
1350       len_new[1] *= zoomfac * BLI_rctf_size_y(&v2d->cur);
1351
1352       /* Calculate distance */
1353       if (v2d->keepzoom & V2D_KEEPASPECT) {
1354         dist = len_v2(len_new) - len_v2(len_old);
1355         dx = dy = dist;
1356       }
1357       else {
1358         dx = len_new[0] - len_old[0];
1359         dy = len_new[1] - len_old[1];
1360       }
1361     }
1362     else {
1363       /* 'continuous' or 'dolly' */
1364       float fac;
1365       /* x-axis transform */
1366       fac = zoomfac * (event->x - vzd->lastx);
1367       dx = fac * BLI_rctf_size_x(&v2d->cur);
1368
1369       /* y-axis transform */
1370       fac = zoomfac * (event->y - vzd->lasty);
1371       dy = fac * BLI_rctf_size_y(&v2d->cur);
1372
1373       /* Only respect user setting zoom axis if the view does not have any zoom restrictions
1374        * any will be scaled uniformly */
1375       if ((v2d->keepzoom & V2D_LOCKZOOM_X) == 0 && (v2d->keepzoom & V2D_LOCKZOOM_Y) == 0 &&
1376           (v2d->keepzoom & V2D_KEEPASPECT)) {
1377         if (U.uiflag & USER_ZOOM_HORIZ) {
1378           dy = 0;
1379         }
1380         else {
1381           dx = 0;
1382         }
1383       }
1384     }
1385
1386     /* support zoom to always zoom entirely - the v2d code uses portrait or
1387      * landscape exceptions */
1388     if (v2d->keepzoom & V2D_KEEPASPECT) {
1389       if (fabsf(dx) > fabsf(dy)) {
1390         dy = dx;
1391       }
1392       else {
1393         dx = dy;
1394       }
1395     }
1396
1397     /* set transform amount, and add current deltas to stored total delta (for redo) */
1398     RNA_float_set(op->ptr, "deltax", dx);
1399     RNA_float_set(op->ptr, "deltay", dy);
1400
1401     vzd->dx += dx;
1402     vzd->dy += dy;
1403
1404     /* Store mouse coordinates for next time, if not doing continuous zoom:
1405      * - Continuous zoom only depends on distance of mouse
1406      *   to starting point to determine rate of change.
1407      */
1408     if (U.viewzoom != USER_ZOOM_CONT) { /* XXX store this setting as RNA prop? */
1409       vzd->lastx = event->x;
1410       vzd->lasty = event->y;
1411     }
1412
1413     /* apply zooming */
1414     view_zoomdrag_apply(C, op);
1415   }
1416   else if (ELEM(event->type, vzd->invoke_event, EVT_ESCKEY)) {
1417     if (event->val == KM_RELEASE) {
1418
1419       /* for redo, store the overall deltas - need to respect zoom-locks here... */
1420       if ((v2d->keepzoom & V2D_LOCKZOOM_X) == 0) {
1421         RNA_float_set(op->ptr, "deltax", vzd->dx);
1422       }
1423       else {
1424         RNA_float_set(op->ptr, "deltax", 0);
1425       }
1426
1427       if ((v2d->keepzoom & V2D_LOCKZOOM_Y) == 0) {
1428         RNA_float_set(op->ptr, "deltay", vzd->dy);
1429       }
1430       else {
1431         RNA_float_set(op->ptr, "deltay", 0);
1432       }
1433
1434       /* free customdata */
1435       view_zoomdrag_exit(C, op);
1436       WM_cursor_modal_restore(CTX_wm_window(C));
1437
1438       return OPERATOR_FINISHED;
1439     }
1440   }
1441
1442   return OPERATOR_RUNNING_MODAL;
1443 }
1444
1445 static void VIEW2D_OT_zoom(wmOperatorType *ot)
1446 {
1447   PropertyRNA *prop;
1448   /* identifiers */
1449   ot->name = "Zoom 2D View";
1450   ot->description = "Zoom in/out the view";
1451   ot->idname = "VIEW2D_OT_zoom";
1452
1453   /* api callbacks */
1454   ot->exec = view_zoomdrag_exec;
1455   ot->invoke = view_zoomdrag_invoke;
1456   ot->modal = view_zoomdrag_modal;
1457   ot->cancel = view_zoomdrag_cancel;
1458
1459   ot->poll = view_zoom_poll;
1460
1461   /* operator is repeatable */
1462   ot->flag = OPTYPE_BLOCKING | OPTYPE_GRAB_CURSOR_XY;
1463
1464   /* rna - must keep these in sync with the other operators */
1465   prop = RNA_def_float(ot->srna, "deltax", 0, -FLT_MAX, FLT_MAX, "Delta X", "", -FLT_MAX, FLT_MAX);
1466   RNA_def_property_flag(prop, PROP_HIDDEN);
1467   prop = RNA_def_float(ot->srna, "deltay", 0, -FLT_MAX, FLT_MAX, "Delta Y", "", -FLT_MAX, FLT_MAX);
1468   RNA_def_property_flag(prop, PROP_HIDDEN);
1469
1470   WM_operator_properties_use_cursor_init(ot);
1471 }
1472
1473 /** \} */
1474
1475 /* -------------------------------------------------------------------- */
1476 /** \name Border Zoom Operator
1477  * \{ */
1478
1479 /**
1480  * The user defines a rect using standard box select tools, and we use this rect to
1481  * define the new zoom-level of the view in the following ways:
1482  *
1483  * -# LEFTMOUSE - zoom in to view
1484  * -# RIGHTMOUSE - zoom out of view
1485  *
1486  * Currently, these key mappings are hardcoded, but it shouldn't be too important to
1487  * have custom keymappings for this...
1488  */
1489
1490 static int view_borderzoom_exec(bContext *C, wmOperator *op)
1491 {
1492   ARegion *region = CTX_wm_region(C);
1493   View2D *v2d = &region->v2d;
1494   rctf rect;
1495   rctf cur_new = v2d->cur;
1496   const int smooth_viewtx = WM_operator_smooth_viewtx_get(op);
1497
1498   /* convert coordinates of rect to 'tot' rect coordinates */
1499   WM_operator_properties_border_to_rctf(op, &rect);
1500   UI_view2d_region_to_view_rctf(v2d, &rect, &rect);
1501
1502   /* check if zooming in/out view */
1503   const bool zoom_in = !RNA_boolean_get(op->ptr, "zoom_out");
1504
1505   if (zoom_in) {
1506     /* zoom in:
1507      * - 'cur' rect will be defined by the coordinates of the border region
1508      * - just set the 'cur' rect to have the same coordinates as the border region
1509      *   if zoom is allowed to be changed
1510      */
1511     if ((v2d->keepzoom & V2D_LOCKZOOM_X) == 0) {
1512       cur_new.xmin = rect.xmin;
1513       cur_new.xmax = rect.xmax;
1514     }
1515     if ((v2d->keepzoom & V2D_LOCKZOOM_Y) == 0) {
1516       cur_new.ymin = rect.ymin;
1517       cur_new.ymax = rect.ymax;
1518     }
1519   }
1520   else {
1521     /* zoom out:
1522      * - the current 'cur' rect coordinates are going to end up where the 'rect' ones are,
1523      *   but the 'cur' rect coordinates will need to be adjusted to take in more of the view
1524      * - calculate zoom factor, and adjust using center-point
1525      */
1526     float zoom, center, size;
1527
1528     /* TODO: is this zoom factor calculation valid?
1529      * It seems to produce same results every time... */
1530     if ((v2d->keepzoom & V2D_LOCKZOOM_X) == 0) {
1531       size = BLI_rctf_size_x(&cur_new);
1532       zoom = size / BLI_rctf_size_x(&rect);
1533       center = BLI_rctf_cent_x(&cur_new);
1534
1535       cur_new.xmin = center - (size * zoom);
1536       cur_new.xmax = center + (size * zoom);
1537     }
1538     if ((v2d->keepzoom & V2D_LOCKZOOM_Y) == 0) {
1539       size = BLI_rctf_size_y(&cur_new);
1540       zoom = size / BLI_rctf_size_y(&rect);
1541       center = BLI_rctf_cent_y(&cur_new);
1542
1543       cur_new.ymin = center - (size * zoom);
1544       cur_new.ymax = center + (size * zoom);
1545     }
1546   }
1547
1548   UI_view2d_smooth_view(C, region, &cur_new, smooth_viewtx);
1549
1550   return OPERATOR_FINISHED;
1551 }
1552
1553 static void VIEW2D_OT_zoom_border(wmOperatorType *ot)
1554 {
1555   /* identifiers */
1556   ot->name = "Zoom to Border";
1557   ot->description = "Zoom in the view to the nearest item contained in the border";
1558   ot->idname = "VIEW2D_OT_zoom_border";
1559
1560   /* api callbacks */
1561   ot->invoke = WM_gesture_box_invoke;
1562   ot->exec = view_borderzoom_exec;
1563   ot->modal = WM_gesture_box_modal;
1564   ot->cancel = WM_gesture_box_cancel;
1565
1566   ot->poll = view_zoom_poll;
1567
1568   /* rna */
1569   WM_operator_properties_gesture_box_zoom(ot);
1570 }
1571
1572 /** \} */
1573
1574 /* -------------------------------------------------------------------- */
1575 /** \name NDOF Pan/Zoom Operator
1576  * \{ */
1577
1578 #ifdef WITH_INPUT_NDOF
1579 static int view2d_ndof_invoke(bContext *C, wmOperator *op, const wmEvent *event)
1580 {
1581   if (event->type != NDOF_MOTION) {
1582     return OPERATOR_CANCELLED;
1583   }
1584
1585   const wmNDOFMotionData *ndof = event->customdata;
1586
1587   /* tune these until it feels right */
1588   const float zoom_sensitivity = 0.5f;
1589   const float speed = 10.0f; /* match view3d ortho */
1590   const bool has_translate = (ndof->tvec[0] && ndof->tvec[1]) && view_pan_poll(C);
1591   const bool has_zoom = (ndof->tvec[2] != 0.0f) && view_zoom_poll(C);
1592
1593   if (has_translate) {
1594     float pan_vec[3];
1595
1596     WM_event_ndof_pan_get(ndof, pan_vec, false);
1597
1598     pan_vec[0] *= speed;
1599     pan_vec[1] *= speed;
1600
1601     view_pan_init(C, op);
1602
1603     v2dViewPanData *vpd = op->customdata;
1604     view_pan_apply_ex(C, vpd, pan_vec[0], pan_vec[1]);
1605
1606     view_pan_exit(op);
1607   }
1608
1609   if (has_zoom) {
1610     float zoom_factor = zoom_sensitivity * ndof->dt * -ndof->tvec[2];
1611
1612     bool do_zoom_xy[2];
1613
1614     if (U.ndof_flag & NDOF_ZOOM_INVERT) {
1615       zoom_factor = -zoom_factor;
1616     }
1617
1618     view_zoom_axis_lock_defaults(C, do_zoom_xy);
1619
1620     view_zoomdrag_init(C, op);
1621
1622     v2dViewZoomData *vzd = op->customdata;
1623     view_zoomstep_apply_ex(
1624         C, vzd, do_zoom_xy[0] ? zoom_factor : 0.0f, do_zoom_xy[1] ? zoom_factor : 0.0f);
1625
1626     view_zoomstep_exit(op);
1627   }
1628
1629   return OPERATOR_FINISHED;
1630 }
1631
1632 static void VIEW2D_OT_ndof(wmOperatorType *ot)
1633 {
1634   /* identifiers */
1635   ot->name = "NDOF Pan/Zoom";
1636   ot->idname = "VIEW2D_OT_ndof";
1637   ot->description = "Use a 3D mouse device to pan/zoom the view";
1638
1639   /* api callbacks */
1640   ot->invoke = view2d_ndof_invoke;
1641   ot->poll = view2d_poll;
1642
1643   /* flags */
1644   ot->flag = OPTYPE_LOCK_BYPASS;
1645 }
1646 #endif /* WITH_INPUT_NDOF */
1647
1648 /** \} */
1649
1650 /* -------------------------------------------------------------------- */
1651 /** \name Smooth View Operator
1652  * \{ */
1653
1654 struct SmoothView2DStore {
1655   rctf orig_cur, new_cur;
1656
1657   double time_allowed;
1658 };
1659
1660 /**
1661  * function to get a factor out of a rectangle
1662  *
1663  * note: this doesn't always work as well as it might because the target size
1664  *       may not be reached because of clamping the desired rect, we _could_
1665  *       attempt to clamp the rect before working out the zoom factor but its
1666  *       not really worthwhile for the few cases this happens.
1667  */
1668 static float smooth_view_rect_to_fac(const rctf *rect_a, const rctf *rect_b)
1669 {
1670   const float size_a[2] = {BLI_rctf_size_x(rect_a), BLI_rctf_size_y(rect_a)};
1671   const float size_b[2] = {BLI_rctf_size_x(rect_b), BLI_rctf_size_y(rect_b)};
1672   const float cent_a[2] = {BLI_rctf_cent_x(rect_a), BLI_rctf_cent_y(rect_a)};
1673   const float cent_b[2] = {BLI_rctf_cent_x(rect_b), BLI_rctf_cent_y(rect_b)};
1674
1675   float fac_max = 0.0f;
1676
1677   for (int i = 0; i < 2; i++) {
1678     /* axis translation normalized to scale */
1679     float tfac = fabsf(cent_a[i] - cent_b[i]) / min_ff(size_a[i], size_b[i]);
1680     fac_max = max_ff(fac_max, tfac);
1681     if (fac_max >= 1.0f) {
1682       break;
1683     }
1684
1685     /* axis scale difference, x2 so doubling or half gives 1.0f */
1686     tfac = (1.0f - (min_ff(size_a[i], size_b[i]) / max_ff(size_a[i], size_b[i]))) * 2.0f;
1687     fac_max = max_ff(fac_max, tfac);
1688     if (fac_max >= 1.0f) {
1689       break;
1690     }
1691   }
1692   return min_ff(fac_max, 1.0f);
1693 }
1694
1695 /* will start timer if appropriate */
1696 /* the arguments are the desired situation */
1697 void UI_view2d_smooth_view(bContext *C, ARegion *region, const rctf *cur, const int smooth_viewtx)
1698 {
1699   wmWindowManager *wm = CTX_wm_manager(C);
1700   wmWindow *win = CTX_wm_window(C);
1701
1702   View2D *v2d = &region->v2d;
1703   struct SmoothView2DStore sms = {{0}};
1704   bool ok = false;
1705   float fac = 1.0f;
1706
1707   /* initialize sms */
1708   sms.new_cur = v2d->cur;
1709
1710   /* store the options we want to end with */
1711   if (cur) {
1712     sms.new_cur = *cur;
1713   }
1714
1715   if (cur) {
1716     fac = smooth_view_rect_to_fac(&v2d->cur, cur);
1717   }
1718
1719   if (smooth_viewtx && fac > FLT_EPSILON) {
1720     bool changed = false;
1721
1722     if (BLI_rctf_compare(&sms.new_cur, &v2d->cur, FLT_EPSILON) == false) {
1723       changed = true;
1724     }
1725
1726     /* The new view is different from the old one
1727      * so animate the view */
1728     if (changed) {
1729       sms.orig_cur = v2d->cur;
1730
1731       sms.time_allowed = (double)smooth_viewtx / 1000.0;
1732
1733       /* scale the time allowed the change in view */
1734       sms.time_allowed *= (double)fac;
1735
1736       /* keep track of running timer! */
1737       if (v2d->sms == NULL) {
1738         v2d->sms = MEM_mallocN(sizeof(struct SmoothView2DStore), "smoothview v2d");
1739       }
1740       *v2d->sms = sms;
1741       if (v2d->smooth_timer) {
1742         WM_event_remove_timer(wm, win, v2d->smooth_timer);
1743       }
1744       /* TIMER1 is hardcoded in keymap */
1745       /* max 30 frs/sec */
1746       v2d->smooth_timer = WM_event_add_timer(wm, win, TIMER1, 1.0 / 100.0);
1747
1748       ok = true;
1749     }
1750   }
1751
1752   /* if we get here nothing happens */
1753   if (ok == false) {
1754     v2d->cur = sms.new_cur;
1755
1756     UI_view2d_curRect_changed(C, v2d);
1757     ED_region_tag_redraw_no_rebuild(region);
1758     UI_view2d_sync(CTX_wm_screen(C), CTX_wm_area(C), v2d, V2D_LOCK_COPY);
1759   }
1760 }
1761
1762 /* only meant for timer usage */
1763 static int view2d_smoothview_invoke(bContext *C, wmOperator *UNUSED(op), const wmEvent *event)
1764 {
1765   wmWindow *win = CTX_wm_window(C);
1766   ARegion *region = CTX_wm_region(C);
1767   View2D *v2d = &region->v2d;
1768   struct SmoothView2DStore *sms = v2d->sms;
1769   float step;
1770
1771   /* escape if not our timer */
1772   if (v2d->smooth_timer == NULL || v2d->smooth_timer != event->customdata) {
1773     return OPERATOR_PASS_THROUGH;
1774   }
1775
1776   if (sms->time_allowed != 0.0) {
1777     step = (float)((v2d->smooth_timer->duration) / sms->time_allowed);
1778   }
1779   else {
1780     step = 1.0f;
1781   }
1782
1783   /* end timer */
1784   if (step >= 1.0f) {
1785     v2d->cur = sms->new_cur;
1786
1787     MEM_freeN(v2d->sms);
1788     v2d->sms = NULL;
1789
1790     WM_event_remove_timer(CTX_wm_manager(C), win, v2d->smooth_timer);
1791     v2d->smooth_timer = NULL;
1792
1793     /* Event handling won't know if a UI item has been moved under the pointer. */
1794     WM_event_add_mousemove(win);
1795   }
1796   else {
1797     /* ease in/out */
1798     step = (3.0f * step * step - 2.0f * step * step * step);
1799
1800     BLI_rctf_interp(&v2d->cur, &sms->orig_cur, &sms->new_cur, step);
1801   }
1802
1803   UI_view2d_curRect_changed(C, v2d);
1804   UI_view2d_sync(CTX_wm_screen(C), CTX_wm_area(C), v2d, V2D_LOCK_COPY);
1805   ED_region_tag_redraw_no_rebuild(region);
1806
1807   if (v2d->sms == NULL) {
1808     UI_view2d_zoom_cache_reset();
1809   }
1810
1811   return OPERATOR_FINISHED;
1812 }
1813
1814 static void VIEW2D_OT_smoothview(wmOperatorType *ot)
1815 {
1816   /* identifiers */
1817   ot->name = "Smooth View 2D";
1818   ot->idname = "VIEW2D_OT_smoothview";
1819
1820   /* api callbacks */
1821   ot->invoke = view2d_smoothview_invoke;
1822   ot->poll = view2d_poll;
1823
1824   /* flags */
1825   ot->flag = OPTYPE_INTERNAL;
1826
1827   /* rna */
1828   WM_operator_properties_gesture_box(ot);
1829 }
1830
1831 /** \} */
1832
1833 /* -------------------------------------------------------------------- */
1834 /** \name Scroll Bar Move Operator
1835  * \{ */
1836
1837 /**
1838  * Scrollers should behave in the following ways, when clicked on with LMB (and dragged):
1839  * -# 'Handles' on end of 'bubble' - when the axis that the scroller represents is zoomable,
1840  *    enlarge 'cur' rect on the relevant side.
1841  * -# 'Bubble'/'bar' - just drag, and bar should move with mouse (view pans opposite).
1842  *
1843  * In order to make sure this works, each operator must define the following RNA-Operator Props:
1844  * - `deltax, deltay` - define how much to move view by (relative to zoom-correction factor)
1845  */
1846
1847 /* customdata for scroller-invoke data */
1848 typedef struct v2dScrollerMove {
1849   /** View2D data that this operation affects */
1850   View2D *v2d;
1851   /** region that the scroller is in */
1852   ARegion *region;
1853
1854   /** scroller that mouse is in ('h' or 'v') */
1855   char scroller;
1856
1857   /* XXX find some way to provide visual feedback of this (active color?) */
1858   /** -1 is min zoomer, 0 is bar, 1 is max zoomer */
1859   short zone;
1860
1861   /** view adjustment factor, based on size of region */
1862   float fac;
1863   /** for pixel rounding (avoid visible UI jitter) */
1864   float fac_round;
1865   /** amount moved by mouse on axis of interest */
1866   float delta;
1867
1868   /** width of the scrollbar itself, used for page up/down clicks */
1869   float scrollbarwidth;
1870   /** initial location of scrollbar x/y, mouse relative */
1871   int scrollbar_orig;
1872
1873   /** previous mouse coordinates (in screen coordinates) for determining movement */
1874   int lastx, lasty;
1875 } v2dScrollerMove;
1876
1877 /**
1878  * #View2DScrollers is typedef'd in UI_view2d.h
1879  * This is a CUT DOWN VERSION of the 'real' version, which is defined in view2d.c,
1880  * as we only need focus bubble info.
1881  *
1882  * \warning: The start of this struct must not change,
1883  * so that it stays in sync with the 'real' version.
1884  * For now, we don't need to have a separate (internal) header for structs like this...
1885  */
1886 struct View2DScrollers {
1887   /* focus bubbles */
1888   int vert_min, vert_max; /* vertical scrollbar */
1889   int hor_min, hor_max;   /* horizontal scrollbar */
1890
1891   /* These values are written into, even if we don't use them. */
1892   rcti _hor, _vert;
1893 };
1894
1895 /* quick enum for vsm->zone (scroller handles) */
1896 enum {
1897   SCROLLHANDLE_MIN = -1,
1898   SCROLLHANDLE_BAR,
1899   SCROLLHANDLE_MAX,
1900   SCROLLHANDLE_MIN_OUTSIDE,
1901   SCROLLHANDLE_MAX_OUTSIDE,
1902 } /*eV2DScrollerHandle_Zone*/;
1903
1904 /**
1905  * Check if mouse is within scroller handle.
1906  *
1907  * \param mouse: relevant mouse coordinate in region space.
1908  * \param sc_min, sc_max: extents of scroller 'groove' (potential available space for scroller).
1909  * \param sh_min, sh_max: positions of scrollbar handles.
1910  */
1911 static short mouse_in_scroller_handle(int mouse, int sc_min, int sc_max, int sh_min, int sh_max)
1912 {
1913   /* firstly, check if
1914    * - 'bubble' fills entire scroller
1915    * - 'bubble' completely out of view on either side
1916    */
1917   bool in_view = true;
1918   if (sh_min <= sc_min && sc_max <= sh_max) {
1919     in_view = false;
1920   }
1921   else if (sh_max <= sc_min || sc_max <= sh_min) {
1922     in_view = false;
1923   }
1924
1925   if (!in_view) {
1926     return SCROLLHANDLE_BAR;
1927   }
1928
1929   /* check if mouse is in or past either handle */
1930   /* TODO: check if these extents are still valid or not */
1931   bool in_max = ((mouse >= (sh_max - V2D_SCROLL_HANDLE_SIZE_HOTSPOT)) &&
1932                  (mouse <= (sh_max + V2D_SCROLL_HANDLE_SIZE_HOTSPOT)));
1933   bool in_min = ((mouse <= (sh_min + V2D_SCROLL_HANDLE_SIZE_HOTSPOT)) &&
1934                  (mouse >= (sh_min - V2D_SCROLL_HANDLE_SIZE_HOTSPOT)));
1935   bool in_bar = ((mouse < (sh_max - V2D_SCROLL_HANDLE_SIZE_HOTSPOT)) &&
1936                  (mouse > (sh_min + V2D_SCROLL_HANDLE_SIZE_HOTSPOT)));
1937   const bool out_min = mouse < (sh_min - V2D_SCROLL_HANDLE_SIZE_HOTSPOT);
1938   const bool out_max = mouse > (sh_max + V2D_SCROLL_HANDLE_SIZE_HOTSPOT);
1939
1940   if (in_bar) {
1941     return SCROLLHANDLE_BAR;
1942   }
1943   if (in_max) {
1944     return SCROLLHANDLE_MAX;
1945   }
1946   if (in_min) {
1947     return SCROLLHANDLE_MIN;
1948   }
1949   if (out_min) {
1950     return SCROLLHANDLE_MIN_OUTSIDE;
1951   }
1952   if (out_max) {
1953     return SCROLLHANDLE_MAX_OUTSIDE;
1954   }
1955
1956   /* unlikely to happen, though we just cover it in case */
1957   return SCROLLHANDLE_BAR;
1958 }
1959
1960 static bool scroller_activate_poll(bContext *C)
1961 {
1962   if (!view2d_poll(C)) {
1963     return false;
1964   }
1965
1966   wmWindow *win = CTX_wm_window(C);
1967   ARegion *region = CTX_wm_region(C);
1968   View2D *v2d = &region->v2d;
1969   wmEvent *event = win->eventstate;
1970
1971   /* check if mouse in scrollbars, if they're enabled */
1972   return (UI_view2d_mouse_in_scrollers(region, v2d, event->x, event->y) != 0);
1973 }
1974
1975 /* initialize customdata for scroller manipulation operator */
1976 static void scroller_activate_init(bContext *C,
1977                                    wmOperator *op,
1978                                    const wmEvent *event,
1979                                    const char in_scroller)
1980 {
1981   v2dScrollerMove *vsm;
1982   View2DScrollers scrollers;
1983   ARegion *region = CTX_wm_region(C);
1984   View2D *v2d = &region->v2d;
1985   rctf tot_cur_union;
1986   float mask_size;
1987
1988   /* set custom-data for operator */
1989   vsm = MEM_callocN(sizeof(v2dScrollerMove), "v2dScrollerMove");
1990   op->customdata = vsm;
1991
1992   /* set general data */
1993   vsm->v2d = v2d;
1994   vsm->region = region;
1995   vsm->scroller = in_scroller;
1996
1997   /* store mouse-coordinates, and convert mouse/screen coordinates to region coordinates */
1998   vsm->lastx = event->x;
1999   vsm->lasty = event->y;
2000   /* 'zone' depends on where mouse is relative to bubble
2001    * - zooming must be allowed on this axis, otherwise, default to pan
2002    */
2003   UI_view2d_scrollers_calc(v2d, NULL, &scrollers);
2004
2005   /* Use a union of 'cur' & 'tot' in case the current view is far outside 'tot'. In this cases
2006    * moving the scroll bars has far too little effect and the view can get stuck T31476. */
2007   tot_cur_union = v2d->tot;
2008   BLI_rctf_union(&tot_cur_union, &v2d->cur);
2009
2010   if (in_scroller == 'h') {
2011     /* horizontal scroller - calculate adjustment factor first */
2012     mask_size = (float)BLI_rcti_size_x(&v2d->hor);
2013     vsm->fac = BLI_rctf_size_x(&tot_cur_union) / mask_size;
2014
2015     /* pixel rounding */
2016     vsm->fac_round = (BLI_rctf_size_x(&v2d->cur)) / (float)(BLI_rcti_size_x(&region->winrct) + 1);
2017
2018     /* get 'zone' (i.e. which part of scroller is activated) */
2019     vsm->zone = mouse_in_scroller_handle(
2020         event->mval[0], v2d->hor.xmin, v2d->hor.xmax, scrollers.hor_min, scrollers.hor_max);
2021
2022     if ((v2d->keepzoom & V2D_LOCKZOOM_X) && ELEM(vsm->zone, SCROLLHANDLE_MIN, SCROLLHANDLE_MAX)) {
2023       /* default to scroll, as handles not usable */
2024       vsm->zone = SCROLLHANDLE_BAR;
2025     }
2026
2027     vsm->scrollbarwidth = scrollers.hor_max - scrollers.hor_min;
2028     vsm->scrollbar_orig = ((scrollers.hor_max + scrollers.hor_min) / 2) + region->winrct.xmin;
2029   }
2030   else {
2031     /* vertical scroller - calculate adjustment factor first */
2032     mask_size = (float)BLI_rcti_size_y(&v2d->vert);
2033     vsm->fac = BLI_rctf_size_y(&tot_cur_union) / mask_size;
2034
2035     /* pixel rounding */
2036     vsm->fac_round = (BLI_rctf_size_y(&v2d->cur)) / (float)(BLI_rcti_size_y(&region->winrct) + 1);
2037
2038     /* get 'zone' (i.e. which part of scroller is activated) */
2039     vsm->zone = mouse_in_scroller_handle(
2040         event->mval[1], v2d->vert.ymin, v2d->vert.ymax, scrollers.vert_min, scrollers.vert_max);
2041
2042     if ((v2d->keepzoom & V2D_LOCKZOOM_Y) && ELEM(vsm->zone, SCROLLHANDLE_MIN, SCROLLHANDLE_MAX)) {
2043       /* default to scroll, as handles not usable */
2044       vsm->zone = SCROLLHANDLE_BAR;
2045     }
2046
2047     vsm->scrollbarwidth = scrollers.vert_max - scrollers.vert_min;
2048     vsm->scrollbar_orig = ((scrollers.vert_max + scrollers.vert_min) / 2) + region->winrct.ymin;
2049   }
2050
2051   ED_region_tag_redraw_no_rebuild(region);
2052 }
2053
2054 /* cleanup temp customdata  */
2055 static void scroller_activate_exit(bContext *C, wmOperator *op)
2056 {
2057   if (op->customdata) {
2058     v2dScrollerMove *vsm = op->customdata;
2059
2060     vsm->v2d->scroll_ui &= ~(V2D_SCROLL_H_ACTIVE | V2D_SCROLL_V_ACTIVE);
2061
2062     MEM_freeN(op->customdata);
2063     op->customdata = NULL;
2064
2065     ED_region_tag_redraw_no_rebuild(CTX_wm_region(C));
2066   }
2067 }
2068
2069 static void scroller_activate_cancel(bContext *C, wmOperator *op)
2070 {
2071   scroller_activate_exit(C, op);
2072 }
2073
2074 /* apply transform to view (i.e. adjust 'cur' rect) */
2075 static void scroller_activate_apply(bContext *C, wmOperator *op)
2076 {
2077   v2dScrollerMove *vsm = op->customdata;
2078   View2D *v2d = vsm->v2d;
2079   float temp;
2080
2081   /* calculate amount to move view by */
2082   temp = vsm->fac * vsm->delta;
2083
2084   /* round to pixel */
2085   temp = roundf(temp / vsm->fac_round) * vsm->fac_round;
2086
2087   /* type of movement */
2088   switch (vsm->zone) {
2089     case SCROLLHANDLE_MIN:
2090       /* only expand view on axis if zoom is allowed */
2091       if ((vsm->scroller == 'h') && !(v2d->keepzoom & V2D_LOCKZOOM_X)) {
2092         v2d->cur.xmin -= temp;
2093       }
2094       if ((vsm->scroller == 'v') && !(v2d->keepzoom & V2D_LOCKZOOM_Y)) {
2095         v2d->cur.ymin -= temp;
2096       }
2097       break;
2098
2099     case SCROLLHANDLE_MAX:
2100
2101       /* only expand view on axis if zoom is allowed */
2102       if ((vsm->scroller == 'h') && !(v2d->keepzoom & V2D_LOCKZOOM_X)) {
2103         v2d->cur.xmax += temp;
2104       }
2105       if ((vsm->scroller == 'v') && !(v2d->keepzoom & V2D_LOCKZOOM_Y)) {
2106         v2d->cur.ymax += temp;
2107       }
2108       break;
2109
2110     case SCROLLHANDLE_MIN_OUTSIDE:
2111     case SCROLLHANDLE_MAX_OUTSIDE:
2112     case SCROLLHANDLE_BAR:
2113     default:
2114       /* only move view on an axis if panning is allowed */
2115       if ((vsm->scroller == 'h') && !(v2d->keepofs & V2D_LOCKOFS_X)) {
2116         v2d->cur.xmin += temp;
2117         v2d->cur.xmax += temp;
2118       }
2119       if ((vsm->scroller == 'v') && !(v2d->keepofs & V2D_LOCKOFS_Y)) {
2120         v2d->cur.ymin += temp;
2121         v2d->cur.ymax += temp;
2122       }
2123       break;
2124   }
2125
2126   /* Inform v2d about changes after this operation. */
2127   UI_view2d_curRect_changed(C, v2d);
2128
2129   /* request updates to be done... */
2130   ED_region_tag_redraw_no_rebuild(vsm->region);
2131   UI_view2d_sync(CTX_wm_screen(C), CTX_wm_area(C), v2d, V2D_LOCK_COPY);
2132 }
2133
2134 /**
2135  * Handle user input for scrollers - calculations of mouse-movement need to be done here,
2136  * not in the apply callback!
2137  */
2138 static int scroller_activate_modal(bContext *C, wmOperator *op, const wmEvent *event)
2139 {
2140   v2dScrollerMove *vsm = op->customdata;
2141
2142   /* execute the events */
2143   switch (event->type) {
2144     case MOUSEMOVE: {
2145       /* calculate new delta transform, then store mouse-coordinates for next-time */
2146       if (ELEM(vsm->zone, SCROLLHANDLE_BAR, SCROLLHANDLE_MAX)) {
2147         /* if using bar (i.e. 'panning') or 'max' zoom widget */
2148         switch (vsm->scroller) {
2149           case 'h': /* horizontal scroller - so only horizontal movement
2150                      * ('cur' moves opposite to mouse) */
2151             vsm->delta = (float)(event->x - vsm->lastx);
2152             break;
2153           case 'v': /* vertical scroller - so only vertical movement
2154                      * ('cur' moves opposite to mouse) */
2155             vsm->delta = (float)(event->y - vsm->lasty);
2156             break;
2157         }
2158       }
2159       else if (vsm->zone == SCROLLHANDLE_MIN) {
2160         /* using 'min' zoom widget */
2161         switch (vsm->scroller) {
2162           case 'h': /* horizontal scroller - so only horizontal movement
2163                      * ('cur' moves with mouse) */
2164             vsm->delta = (float)(vsm->lastx - event->x);
2165             break;
2166           case 'v': /* vertical scroller - so only vertical movement
2167                      * ('cur' moves with to mouse) */
2168             vsm->delta = (float)(vsm->lasty - event->y);
2169             break;
2170         }
2171       }
2172
2173       /* store previous coordinates */
2174       vsm->lastx = event->x;
2175       vsm->lasty = event->y;
2176
2177       scroller_activate_apply(C, op);
2178       break;
2179     }
2180     case LEFTMOUSE:
2181     case MIDDLEMOUSE:
2182       if (event->val == KM_RELEASE) {
2183         /* single-click was in empty space outside bubble, so scroll by 1 'page' */
2184         if (ELEM(vsm->zone, SCROLLHANDLE_MIN_OUTSIDE, SCROLLHANDLE_MAX_OUTSIDE)) {
2185           if (vsm->zone == SCROLLHANDLE_MIN_OUTSIDE) {
2186             vsm->delta = -vsm->scrollbarwidth * 0.8f;
2187           }
2188           else if (vsm->zone == SCROLLHANDLE_MAX_OUTSIDE) {
2189             vsm->delta = vsm->scrollbarwidth * 0.8f;
2190           }
2191
2192           scroller_activate_apply(C, op);
2193           scroller_activate_exit(C, op);
2194           return OPERATOR_FINISHED;
2195         }
2196
2197         /* otherwise, end the drag action  */
2198         if (vsm->lastx || vsm->lasty) {
2199           scroller_activate_exit(C, op);
2200           return OPERATOR_FINISHED;
2201         }
2202       }
2203       break;
2204   }
2205
2206   return OPERATOR_RUNNING_MODAL;
2207 }
2208
2209 /* a click (or click drag in progress)
2210  * should have occurred, so check if it happened in scrollbar */
2211 static int scroller_activate_invoke(bContext *C, wmOperator *op, const wmEvent *event)
2212 {
2213   ARegion *region = CTX_wm_region(C);
2214   View2D *v2d = &region->v2d;
2215
2216   /* check if mouse in scrollbars, if they're enabled */
2217   const char in_scroller = UI_view2d_mouse_in_scrollers(region, v2d, event->x, event->y);
2218
2219   /* if in a scroller, init customdata then set modal handler which will
2220    * catch mouse-down to start doing useful stuff */
2221   if (in_scroller) {
2222     v2dScrollerMove *vsm;
2223
2224     /* initialize customdata */
2225     scroller_activate_init(C, op, event, in_scroller);
2226     vsm = (v2dScrollerMove *)op->customdata;
2227
2228     /* support for quick jump to location - gtk and qt do this on linux */
2229     if (event->type == MIDDLEMOUSE) {
2230       switch (vsm->scroller) {
2231         case 'h': /* horizontal scroller - so only horizontal movement
2232                    * ('cur' moves opposite to mouse) */
2233           vsm->delta = (float)(event->x - vsm->scrollbar_orig);
2234           break;
2235         case 'v': /* vertical scroller - so only vertical movement
2236                    * ('cur' moves opposite to mouse) */
2237           vsm->delta = (float)(event->y - vsm->scrollbar_orig);
2238           break;
2239       }
2240       scroller_activate_apply(C, op);
2241
2242       vsm->zone = SCROLLHANDLE_BAR;
2243     }
2244
2245     /* check if zoom zones are inappropriate (i.e. zoom widgets not shown), so cannot continue
2246      * NOTE: see view2d.c for latest conditions, and keep this in sync with that
2247      */
2248     if (ELEM(vsm->zone, SCROLLHANDLE_MIN, SCROLLHANDLE_MAX)) {
2249       if (((vsm->scroller == 'h') && (v2d->scroll & V2D_SCROLL_HORIZONTAL_HANDLES) == 0) ||
2250           ((vsm->scroller == 'v') && (v2d->scroll & V2D_SCROLL_VERTICAL_HANDLES) == 0)) {
2251         /* switch to bar (i.e. no scaling gets handled) */
2252         vsm->zone = SCROLLHANDLE_BAR;
2253       }
2254     }
2255
2256     /* check if zone is inappropriate (i.e. 'bar' but panning is banned), so cannot continue */
2257     if (vsm->zone == SCROLLHANDLE_BAR) {
2258       if (((vsm->scroller == 'h') && (v2d->keepofs & V2D_LOCKOFS_X)) ||
2259           ((vsm->scroller == 'v') && (v2d->keepofs & V2D_LOCKOFS_Y))) {
2260         /* free customdata initialized */
2261         scroller_activate_exit(C, op);
2262
2263         /* can't catch this event for ourselves, so let it go to someone else? */
2264         return OPERATOR_PASS_THROUGH;
2265       }
2266     }
2267
2268     /* zone is also inappropriate if scroller is not visible... */
2269     if (((vsm->scroller == 'h') && (v2d->scroll & V2D_SCROLL_HORIZONTAL_FULLR)) ||
2270         ((vsm->scroller == 'v') && (v2d->scroll & V2D_SCROLL_VERTICAL_FULLR))) {
2271       /* free customdata initialized */
2272       scroller_activate_exit(C, op);
2273
2274       /* can't catch this event for ourselves, so let it go to someone else? */
2275       /* XXX note: if handlers use mask rect to clip input, input will fail for this case */
2276       return OPERATOR_PASS_THROUGH;
2277     }
2278
2279     /* activate the scroller */
2280     if (vsm->scroller == 'h') {
2281       v2d->scroll_ui |= V2D_SCROLL_H_ACTIVE;
2282     }
2283     else {
2284       v2d->scroll_ui |= V2D_SCROLL_V_ACTIVE;
2285     }
2286
2287     /* still ok, so can add */
2288     WM_event_add_modal_handler(C, op);
2289     return OPERATOR_RUNNING_MODAL;
2290   }
2291
2292   /* not in scroller, so nothing happened...
2293    * (pass through let's something else catch event) */
2294   return OPERATOR_PASS_THROUGH;
2295 }
2296
2297 /* LMB-Drag in Scrollers - not repeatable operator! */
2298 static void VIEW2D_OT_scroller_activate(wmOperatorType *ot)
2299 {
2300   /* identifiers */
2301   ot->name = "Scroller Activate";
2302   ot->description = "Scroll view by mouse click and drag";
2303   ot->idname = "VIEW2D_OT_scroller_activate";
2304
2305   /* flags */
2306   ot->flag = OPTYPE_BLOCKING;
2307
2308   /* api callbacks */
2309   ot->invoke = scroller_activate_invoke;
2310   ot->modal = scroller_activate_modal;
2311   ot->cancel = scroller_activate_cancel;
2312
2313   ot->poll = scroller_activate_poll;
2314 }
2315
2316 /** \} */
2317
2318 /* -------------------------------------------------------------------- */
2319 /** \name View Reset Operator
2320  * \{ */
2321
2322 static int reset_exec(bContext *C, wmOperator *UNUSED(op))
2323 {
2324   const uiStyle *style = UI_style_get();
2325   ARegion *region = CTX_wm_region(C);
2326   View2D *v2d = &region->v2d;
2327   int winx, winy;
2328   const int snap_test = ED_region_snap_size_test(region);
2329
2330   /* zoom 1.0 */
2331   winx = (float)(BLI_rcti_size_x(&v2d->mask) + 1);
2332   winy = (float)(BLI_rcti_size_y(&v2d->mask) + 1);
2333
2334   v2d->cur.xmax = v2d->cur.xmin + winx;
2335   v2d->cur.ymax = v2d->cur.ymin + winy;
2336
2337   /* align */
2338   if (v2d->align) {
2339     /* posx and negx flags are mutually exclusive, so watch out */
2340     if ((v2d->align & V2D_ALIGN_NO_POS_X) && !(v2d->align & V2D_ALIGN_NO_NEG_X)) {
2341       v2d->cur.xmax = 0.0f;
2342       v2d->cur.xmin = -winx * style->panelzoom;
2343     }
2344     else if ((v2d->align & V2D_ALIGN_NO_NEG_X) && !(v2d->align & V2D_ALIGN_NO_POS_X)) {
2345       v2d->cur.xmax = winx * style->panelzoom;
2346       v2d->cur.xmin = 0.0f;
2347     }
2348
2349     /* - posx and negx flags are mutually exclusive, so watch out */
2350     if ((v2d->align & V2D_ALIGN_NO_POS_Y) && !(v2d->align & V2D_ALIGN_NO_NEG_Y)) {
2351       v2d->cur.ymax = 0.0f;
2352       v2d->cur.ymin = -winy * style->panelzoom;
2353     }
2354     else if ((v2d->align & V2D_ALIGN_NO_NEG_Y) && !(v2d->align & V2D_ALIGN_NO_POS_Y)) {
2355       v2d->cur.ymax = winy * style->panelzoom;
2356       v2d->cur.ymin = 0.0f;
2357     }
2358   }
2359
2360   /* Inform v2d about changes after this operation. */
2361   UI_view2d_curRect_changed(C, v2d);
2362
2363   if (ED_region_snap_size_apply(region, snap_test)) {
2364     ScrArea *area = CTX_wm_area(C);
2365     ED_area_tag_redraw(area);
2366     WM_event_add_notifier(C, NC_SCREEN | NA_EDITED, NULL);
2367   }
2368
2369   /* request updates to be done... */
2370   ED_region_tag_redraw(region);
2371   UI_view2d_sync(CTX_wm_screen(C), CTX_wm_area(C), v2d, V2D_LOCK_COPY);
2372
2373   UI_view2d_zoom_cache_reset();
2374
2375   return OPERATOR_FINISHED;
2376 }
2377
2378 static void VIEW2D_OT_reset(wmOperatorType *ot)
2379 {
2380   /* identifiers */
2381   ot->name = "Reset View";
2382   ot->description = "Reset the view";
2383   ot->idname = "VIEW2D_OT_reset";
2384
2385   /* api callbacks */
2386   ot->exec = reset_exec;
2387   ot->poll = view2d_poll;
2388 }
2389
2390 /** \} */
2391
2392 /* -------------------------------------------------------------------- */
2393 /** \name Registration
2394  * \{ */
2395
2396 void ED_operatortypes_view2d(void)
2397 {
2398   WM_operatortype_append(VIEW2D_OT_pan);
2399   WM_operatortype_append(VIEW2D_OT_edge_pan);
2400
2401   WM_operatortype_append(VIEW2D_OT_scroll_left);
2402   WM_operatortype_append(VIEW2D_OT_scroll_right);
2403   WM_operatortype_append(VIEW2D_OT_scroll_up);
2404   WM_operatortype_append(VIEW2D_OT_scroll_down);
2405
2406   WM_operatortype_append(VIEW2D_OT_zoom_in);
2407   WM_operatortype_append(VIEW2D_OT_zoom_out);
2408
2409   WM_operatortype_append(VIEW2D_OT_zoom);
2410   WM_operatortype_append(VIEW2D_OT_zoom_border);
2411
2412 #ifdef WITH_INPUT_NDOF
2413   WM_operatortype_append(VIEW2D_OT_ndof);
2414 #endif
2415
2416   WM_operatortype_append(VIEW2D_OT_smoothview);
2417
2418   WM_operatortype_append(VIEW2D_OT_scroller_activate);
2419
2420   WM_operatortype_append(VIEW2D_OT_reset);
2421 }
2422
2423 void ED_keymap_view2d(wmKeyConfig *keyconf)
2424 {
2425   WM_keymap_ensure(keyconf, "View2D", 0, 0);
2426 }
2427
2428 /** \} */