patch 8.2.4674: cannot force getting MouseMove events

Problem:    Cannot force getting MouseMove events.
Solution:   Add the 'mousemoveevent' option with implementaiton for the GUI.
            (Ernie Rael, closes #10044)
diff --git a/runtime/doc/gui.txt b/runtime/doc/gui.txt
index d4c24bf..84d6984 100644
--- a/runtime/doc/gui.txt
+++ b/runtime/doc/gui.txt
@@ -261,6 +261,7 @@
 'mousefocus'	window focus follows mouse pointer |gui-mouse-focus|
 'mousemodel'	what mouse button does which action
 'mousehide'	hide mouse pointer while typing text
+'mousemoveevent' enable mouse move events so that <MouseMove> can be mapped
 'selectmode'	whether to start Select mode or Visual mode
 
 A quick way to set these is with the ":behave" command.
@@ -406,6 +407,9 @@
 application, with shift-left mouse allowing for extending the visual area
 rather than the right mouse button.
 
+<MouseMove> may be mapped, but 'mousemoveevent' must be enabled to use the
+mapping.
+
 Mouse mapping with modifiers does not work for modeless selection.
 
 
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index bdc2522..d5e5e68 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -5517,6 +5517,18 @@
 
 	The 'mousemodel' option is set by the |:behave| command.
 
+						*'mousemoveevent'* *'mousemev'*
+'mousemoveevent' 'mousemev'  boolean	(default off)
+			global
+			{only works in the GUI}
+	When on, mouse move events are delivered to the input queue and are
+	available for mapping. The default, off, avoids the mouse movement
+	overhead except when needed. See |gui-mouse-mapping|.
+	Warning: Setting this option can make pending mappings to be aborted
+	when the mouse is moved.
+	Currently only works in the GUI, may be made to work in a terminal
+	later.
+
 					*'mouseshape'* *'mouses'* *E547*
 'mouseshape' 'mouses'	string	(default "i-r:beam,s:updown,sd:udsizing,
 					vs:leftright,vd:lrsizing,m:no,
diff --git a/runtime/doc/testing.txt b/runtime/doc/testing.txt
index 2a74883..1b726f7 100644
--- a/runtime/doc/testing.txt
+++ b/runtime/doc/testing.txt
@@ -131,8 +131,8 @@
 		    forward:	set to 1 for forward search.
 
 		"mouse":
-		  Inject a mouse button click event.  The supported items in
-		  {args} are:
+		  Inject either a mouse button click, or a mouse move, event.
+		  The supported items in {args} are:
 		    button:	mouse button.  The supported values are:
 				    0	right mouse button
 				    1	middle mouse button
@@ -151,6 +151,28 @@
 				    4	shift is pressed
 				    8	alt is pressed
 				   16	ctrl is pressed
+		    move:	Optional; if used and TRUE then a mouse move
+			        event can be generated.
+				Only {args} row: and col: are used and
+				required; they are interpreted as pixels.
+				Only results in an event when 'mousemoveevent'
+				is set or a popup uses mouse move events.
+
+		"scrollbar":
+		  Set or drag the left, right or horizontal scrollbar.  Only
+		  works when the scrollbar actually exists.  The supported
+		  items in {args} are:
+		    which:	scrollbar. The supported values are:
+				    left  Left scrollbar of the current window
+				    right Right scrollbar of the current window
+				    hor   Horizontal scrollbar
+		    value:	amount to scroll.  For the vertical scrollbars
+				the value can be 1 to the line-count of the
+				buffer.  For the horizontal scrollbar the
+				value can be between 1 and the maximum line
+				length, assuming 'wrap' is not set.
+		    dragging:	1 to drag the scrollbar and 0 to click in the
+				scrollbar.
 
 		"scrollbar":
 		  Set or drag the left, right or horizontal scrollbar.  Only
diff --git a/src/gui.c b/src/gui.c
index 3e383a4..3b8c0d0 100644
--- a/src/gui.c
+++ b/src/gui.c
@@ -3142,13 +3142,26 @@
 		if (hold_gui_events)
 		    return;
 
+		row = gui_xy2colrow(x, y, &col);
+		// Don't report a mouse move unless moved to a
+		// different character position.
+		if (button == MOUSE_MOVE)
+		{
+		    if (row == prev_row && col == prev_col)
+			return;
+		    else
+		    {
+			prev_row = row >= 0 ? row : 0;
+			prev_col = col;
+		    }
+		}
+
 		string[3] = CSI;
 		string[4] = KS_EXTRA;
 		string[5] = (int)button_char;
 
 		// Pass the pointer coordinates of the scroll event so that we
 		// know which window to scroll.
-		row = gui_xy2colrow(x, y, &col);
 		string[6] = (char_u)(col / 128 + ' ' + 1);
 		string[7] = (char_u)(col % 128 + ' ' + 1);
 		string[8] = (char_u)(row / 128 + ' ' + 1);
@@ -4967,12 +4980,14 @@
     // apply 'mousefocus' and pointer shape
     gui_mouse_focus(x, y);
 
+    if (p_mousemev
 #ifdef FEAT_PROP_POPUP
-    if (popup_uses_mouse_move)
-	// Generate a mouse-moved event, so that the popup can perhaps be
-	// closed, just like in the terminal.
-	gui_send_mouse_event(MOUSE_MOVE, x, y, FALSE, 0);
+	|| popup_uses_mouse_move
 #endif
+   )
+	// Generate a mouse-moved event. For a <MouseMove> mapping. Or so the
+	// popup can perhaps be closed, just like in the terminal.
+	gui_send_mouse_event(MOUSE_MOVE, x, y, FALSE, 0);
 }
 
 /*
diff --git a/src/option.h b/src/option.h
index e344675..85f2fa8 100644
--- a/src/option.h
+++ b/src/option.h
@@ -760,6 +760,9 @@
 EXTERN int	p_mh;		// 'mousehide'
 #endif
 EXTERN char_u	*p_mousem;	// 'mousemodel'
+#ifdef FEAT_GUI
+EXTERN int	p_mousemev;	// 'mousemoveevent'
+#endif
 EXTERN long	p_mouset;	// 'mousetime'
 EXTERN int	p_more;		// 'more'
 #ifdef FEAT_MZSCHEME
diff --git a/src/optiondefs.h b/src/optiondefs.h
index 650c622..9eb0e78 100644
--- a/src/optiondefs.h
+++ b/src/optiondefs.h
@@ -1746,6 +1746,13 @@
 # endif
 #endif
 				(char_u *)0L} SCTX_INIT},
+    {"mousemoveevent",   "mousemev",   P_BOOL|P_VI_DEF,
+#ifdef FEAT_GUI
+			    (char_u *)&p_mousemev, PV_NONE,
+#else
+			    (char_u *)NULL, PV_NONE,
+#endif
+			    {(char_u *)FALSE, (char_u *)0L} SCTX_INIT},
     {"mouseshape",  "mouses",  P_STRING|P_VI_DEF|P_ONECOMMA|P_NODUP,
 #ifdef FEAT_MOUSESHAPE
 			    (char_u *)&p_mouseshape, PV_NONE,
diff --git a/src/testdir/test_gui.vim b/src/testdir/test_gui.vim
index 7947288..c5332b8 100644
--- a/src/testdir/test_gui.vim
+++ b/src/testdir/test_gui.vim
@@ -1194,6 +1194,78 @@
   set mousemodel&
 endfunc
 
+func Test_gui_mouse_move_event()
+  let args = #{move: 1, button: 0, multiclick: 0, modifiers: 0}
+
+  " default, do not generate mouse move events
+  set mousemev&
+  call assert_false(&mousemev)
+
+  let n_event = 0
+  nnoremap <special> <MouseMove> :let n_event += 1<CR>
+
+  " start at mouse pos (1,1), clear counter
+  call extend(args, #{row: 1, col:1})
+  call test_gui_event('mouse', args)
+  call feedkeys('', 'Lx!')
+  let n_event = 0
+
+  call extend(args, #{row: 30, col:300})
+  call test_gui_event('mouse', args)
+  call feedkeys('', 'Lx!')
+
+  call extend(args, #{row: 100, col:300})
+  call test_gui_event('mouse', args)
+  call feedkeys('', 'Lx!')
+
+  " no events since mousemev off
+  call assert_equal(0, n_event)
+
+  " turn on mouse events and try the same thing
+  set mousemev
+  call extend(args, #{row: 1, col:1})
+  call test_gui_event('mouse', args)
+  call feedkeys('', 'Lx!')
+  let n_event = 0
+
+  call extend(args, #{row: 30, col:300})
+  call test_gui_event('mouse', args)
+  call feedkeys('', 'Lx!')
+
+  call extend(args, #{row: 100, col:300})
+  call test_gui_event('mouse', args)
+  call feedkeys('', 'Lx!')
+
+  call assert_equal(2, n_event)
+
+  " wiggle the mouse around, shouldn't get events
+  call extend(args, #{row: 1, col:1})
+  call test_gui_event('mouse', args)
+  call feedkeys('', 'Lx!')
+  let n_event = 0
+
+  call extend(args, #{row: 1, col:2})
+  call test_gui_event('mouse', args)
+  call feedkeys('', 'Lx!')
+
+  call extend(args, #{row: 2, col:2})
+  call test_gui_event('mouse', args)
+  call feedkeys('', 'Lx!')
+
+  call extend(args, #{row: 2, col:1})
+  call test_gui_event('mouse', args)
+  call feedkeys('', 'Lx!')
+
+  call extend(args, #{row: 1, col:1})
+  call test_gui_event('mouse', args)
+  call feedkeys('', 'Lx!')
+
+  call assert_equal(0, n_event)
+
+  unmap <MouseMove>
+  set mousemev&
+endfunc
+
 " Test for 'guitablabel' and 'guitabtooltip' options
 func TestGuiTabLabel()
   call add(g:TabLabels, v:lnum + 100)
diff --git a/src/testing.c b/src/testing.c
index 48ba14d..c053487 100644
--- a/src/testing.c
+++ b/src/testing.c
@@ -1368,22 +1368,35 @@
     int		col;
     int		repeated_click;
     int_u	mods;
+    int		move;
 
-    if (dict_find(args, (char_u *)"button", -1) == NULL
-	    || dict_find(args, (char_u *)"row", -1) == NULL
-	    || dict_find(args, (char_u *)"col", -1) == NULL
-	    || dict_find(args, (char_u *)"multiclick", -1) == NULL
-	    || dict_find(args, (char_u *)"modifiers", -1) == NULL)
+    if (dict_find(args, (char_u *)"row", -1) == NULL
+	    || dict_find(args, (char_u *)"col", -1) == NULL)
 	return FALSE;
 
-    button = (int)dict_get_number(args, (char_u *)"button");
+    // Note: "move" is optional, requires fewer arguments
+    move = (int)dict_get_bool(args, (char_u *)"move", FALSE);
+
+    if (!move && (dict_find(args, (char_u *)"button", -1) == NULL
+	    || dict_find(args, (char_u *)"multiclick", -1) == NULL
+	    || dict_find(args, (char_u *)"modifiers", -1) == NULL))
+	return FALSE;
+
     row = (int)dict_get_number(args, (char_u *)"row");
     col = (int)dict_get_number(args, (char_u *)"col");
-    repeated_click = (int)dict_get_number(args, (char_u *)"multiclick");
-    mods = (int)dict_get_number(args, (char_u *)"modifiers");
 
-    gui_send_mouse_event(button, TEXT_X(col - 1), TEXT_Y(row - 1),
+    if (move)
+	gui_mouse_moved(col, row);
+    else
+    {
+	button = (int)dict_get_number(args, (char_u *)"button");
+	repeated_click = (int)dict_get_number(args, (char_u *)"multiclick");
+	mods = (int)dict_get_number(args, (char_u *)"modifiers");
+
+	gui_send_mouse_event(button, TEXT_X(col - 1), TEXT_Y(row - 1),
 							repeated_click, mods);
+    }
+
     return TRUE;
 }
 
diff --git a/src/version.c b/src/version.c
index 9b92724..602366f 100644
--- a/src/version.c
+++ b/src/version.c
@@ -751,6 +751,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    4674,
+/**/
     4673,
 /**/
     4672,