patch 8.1.1609: the user cannot easily close a popup window

Problem:    The user cannot easily close a popup window.
Solution:   Add the "close" property. (mostly by Masato Nishihata,
            closes #4601)
diff --git a/src/popupwin.c b/src/popupwin.c
index 96263bc..a10d4e0 100644
--- a/src/popupwin.c
+++ b/src/popupwin.c
@@ -180,6 +180,17 @@
 	    || (col == popup_width(wp) - 1 && wp->w_popup_border[1] > 0);
 }
 
+/*
+ * Return TRUE if "row"/"col" is on the "X" button of the popup.
+ * The values are relative to the top-left corner.
+ * Caller should check w_popup_close is POPCLOSE_BUTTON.
+ */
+    int
+popup_on_X_button(win_T *wp, int row, int col)
+{
+    return row == 0 && col == popup_width(wp) - 1;
+}
+
 // Values set when dragging a popup window starts.
 static int drag_start_row;
 static int drag_start_col;
@@ -384,6 +395,30 @@
     if (di != NULL)
 	wp->w_popup_drag = dict_get_number(dict, (char_u *)"drag");
 
+    di = dict_find(dict, (char_u *)"close", -1);
+    if (di != NULL)
+    {
+	int ok = TRUE;
+
+	if (di->di_tv.v_type == VAR_STRING && di->di_tv.vval.v_string != NULL)
+	{
+	    char_u  *s = di->di_tv.vval.v_string;
+
+	    if (STRCMP(s, "none") == 0)
+		wp->w_popup_close = POPCLOSE_NONE;
+	    else if (STRCMP(s, "button") == 0)
+		wp->w_popup_close = POPCLOSE_BUTTON;
+	    else if (STRCMP(s, "click") == 0)
+		wp->w_popup_close = POPCLOSE_CLICK;
+	    else
+		ok = FALSE;
+	}
+	else
+	    ok = FALSE;
+	if (!ok)
+	    semsg(_(e_invargNval), "close", tv_get_string(&di->di_tv));
+    }
+
     str = dict_get_string(dict, (char_u *)"highlight", FALSE);
     if (str != NULL)
 	set_string_option_direct_in_win(wp, (char_u *)"wincolor", -1,
@@ -1072,6 +1107,7 @@
 
     // set default values
     wp->w_zindex = POPUPWIN_DEFAULT_ZINDEX;
+    wp->w_popup_close = POPCLOSE_NONE;
 
     if (type == TYPE_NOTIFICATION)
     {
@@ -1106,6 +1142,7 @@
 	wp->w_zindex = POPUPWIN_NOTIFICATION_ZINDEX;
 	wp->w_minwidth = 20;
 	wp->w_popup_drag = 1;
+	wp->w_popup_close = POPCLOSE_CLICK;
 	for (i = 0; i < 4; ++i)
 	    wp->w_popup_border[i] = 1;
 	wp->w_popup_padding[1] = 1;
@@ -1242,6 +1279,19 @@
 }
 
 /*
+ * Close popup "wp" because of a mouse click.
+ */
+    void
+popup_close_for_mouse_click(win_T *wp)
+{
+    typval_T res;
+
+    res.v_type = VAR_NUMBER;
+    res.vval.v_number = -2;
+    popup_close_and_callback(wp, &res);
+}
+
+/*
  * In a filter: check if the typed key is a mouse event that is used for
  * dragging the popup.
  */
@@ -1816,6 +1866,10 @@
 		break;
 	    }
 
+	dict_add_string(dict, "close", (char_u *)(
+		    wp->w_popup_close == POPCLOSE_BUTTON ? "button"
+		    : wp->w_popup_close == POPCLOSE_CLICK ? "click" : "none"));
+
 # if defined(FEAT_TIMERS)
 	dict_add_number(dict, "time", wp->w_popup_timer != NULL
 				 ?  (long)wp->w_popup_timer->tr_interval : 0L);
@@ -2434,6 +2488,14 @@
 	    }
 	}
 
+	if (wp->w_popup_close == POPCLOSE_BUTTON)
+	{
+	    // close button goes on top of anything at the top-right corner
+	    buf[mb_char2bytes('X', buf)] = NUL;
+	    screen_puts(buf, wp->w_winrow, wp->w_wincol + total_width - 1,
+		      wp->w_popup_border[0] > 0 ? border_attr[0] : popup_attr);
+	}
+
 	update_popup_transparent(wp, 0);
 
 	// Back to the normal zindex.
diff --git a/src/proto/popupwin.pro b/src/proto/popupwin.pro
index 7ac8612..a251f9a 100644
--- a/src/proto/popupwin.pro
+++ b/src/proto/popupwin.pro
@@ -1,5 +1,6 @@
 /* popupwin.c */
 int popup_on_border(win_T *wp, int row, int col);
+int popup_on_X_button(win_T *wp, int row, int col);
 void popup_start_drag(win_T *wp);
 void popup_drag(win_T *wp);
 void popup_set_firstline(win_T *wp);
@@ -10,6 +11,7 @@
 void f_popup_clear(typval_T *argvars, typval_T *rettv);
 void f_popup_create(typval_T *argvars, typval_T *rettv);
 void f_popup_atcursor(typval_T *argvars, typval_T *rettv);
+void popup_close_for_mouse_click(win_T *wp);
 void f_popup_filter_menu(typval_T *argvars, typval_T *rettv);
 void f_popup_filter_yesno(typval_T *argvars, typval_T *rettv);
 void f_popup_dialog(typval_T *argvars, typval_T *rettv);
diff --git a/src/structs.h b/src/structs.h
index 208a11e..b579de3 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -1996,6 +1996,12 @@
     POPPOS_CENTER
 } poppos_T;
 
+typedef enum {
+    POPCLOSE_NONE,
+    POPCLOSE_BUTTON,
+    POPCLOSE_CLICK
+} popclose_T;
+
 # define POPUPWIN_DEFAULT_ZINDEX	 50
 # define POPUPMENU_ZINDEX		100
 # define POPUPWIN_DIALOG_ZINDEX		200
@@ -2920,6 +2926,7 @@
     colnr_T	w_popup_mincol;	    // close popup if cursor before this col
     colnr_T	w_popup_maxcol;	    // close popup if cursor after this col
     int		w_popup_drag;	    // allow moving the popup with the mouse
+    popclose_T	w_popup_close;	    // allow closing the popup with the mouse
     list_T	*w_popup_mask;	    // list of lists for "mask"
 
 # if defined(FEAT_TIMERS)
diff --git a/src/testdir/dumps/Test_popupwin_close_01.dump b/src/testdir/dumps/Test_popupwin_close_01.dump
new file mode 100644
index 0000000..e11ae83
--- /dev/null
+++ b/src/testdir/dumps/Test_popupwin_close_01.dump
@@ -0,0 +1,10 @@
+>╔+0#0000001#ffd7ff255|═@5|X| +0#0000000#ffffff0@66
+|║+0#0000001#ffd7ff255|f|o@1|b|a|r|║| +0#0000000#ffffff0@66
+|╚+0#0000001#ffd7ff255|═@5|╝| +0#0000000#ffffff0@5|n+0#0000001#ffd7ff255|o|t|i|f|i|c|a|t|i|o|n| +0#0000000#ffffff0@48
+|4| @73
+|5| |n+0#0000001#ffd7ff255|o| |b|o|r|d|e|r| |h|e|r|X| +0#0000000#ffffff0@5| +0#0000001#ffd7ff255@12|X| +0#0000000#ffffff0@38
+|6| @20| +0#0000001#ffd7ff255|o|n|l|y| |p|a|d@1|i|n|g| | +0#0000000#ffffff0@38
+|7| @20| +0#0000001#ffd7ff255@13| +0#0000000#ffffff0@38
+|8| @73
+|9| @73
+@57|1|,|1| @10|T|o|p| 
diff --git a/src/testdir/dumps/Test_popupwin_close_02.dump b/src/testdir/dumps/Test_popupwin_close_02.dump
new file mode 100644
index 0000000..75342ef
--- /dev/null
+++ b/src/testdir/dumps/Test_popupwin_close_02.dump
@@ -0,0 +1,10 @@
+>1+0&#ffffff0| @73
+|2| @73
+|3| @12|n+0#0000001#ffd7ff255|o|t|i|f|i|c|a|t|i|o|n| +0#0000000#ffffff0@48
+|4| @73
+|5| |n+0#0000001#ffd7ff255|o| |b|o|r|d|e|r| |h|e|r|X| +0#0000000#ffffff0@5| +0#0000001#ffd7ff255@12|X| +0#0000000#ffffff0@38
+|6| @20| +0#0000001#ffd7ff255|o|n|l|y| |p|a|d@1|i|n|g| | +0#0000000#ffffff0@38
+|7| @20| +0#0000001#ffd7ff255@13| +0#0000000#ffffff0@38
+|8| @73
+|9| @73
+|:|c|a|l@1| |C|l|o|s|e|W|i|t|h|X|(|)| @38|1|,|1| @10|T|o|p| 
diff --git a/src/testdir/dumps/Test_popupwin_close_03.dump b/src/testdir/dumps/Test_popupwin_close_03.dump
new file mode 100644
index 0000000..82438c8
--- /dev/null
+++ b/src/testdir/dumps/Test_popupwin_close_03.dump
@@ -0,0 +1,10 @@
+>1+0&#ffffff0| @73
+|2| @73
+|3| @73
+|4| @73
+|5| |n+0#0000001#ffd7ff255|o| |b|o|r|d|e|r| |h|e|r|X| +0#0000000#ffffff0@5| +0#0000001#ffd7ff255@12|X| +0#0000000#ffffff0@38
+|6| @20| +0#0000001#ffd7ff255|o|n|l|y| |p|a|d@1|i|n|g| | +0#0000000#ffffff0@38
+|7| @20| +0#0000001#ffd7ff255@13| +0#0000000#ffffff0@38
+|8| @73
+|9| @73
+|P|o|p|u|p| |c|l|o|s|e|d| |w|i|t|h| |-|2| @36|1|,|1| @10|T|o|p| 
diff --git a/src/testdir/test_popupwin.vim b/src/testdir/test_popupwin.vim
index f4bc33c..a3e6acc 100644
--- a/src/testdir/test_popupwin.vim
+++ b/src/testdir/test_popupwin.vim
@@ -365,6 +365,63 @@
   call delete('XtestPopupDrag')
 endfunc
 
+func Test_popup_close_with_mouse()
+  if !CanRunVimInTerminal()
+    throw 'Skipped: cannot make screendumps'
+  endif
+  let lines =<< trim END
+	call setline(1, range(1, 20))
+	" With border, can click on X
+	let winid = popup_create('foobar', {
+	      \ 'close': 'button',
+	      \ 'border': [],
+	      \ 'line': 1,
+	      \ 'col': 1,
+	      \ })
+	func CloseMsg(id, result)
+	  echomsg 'Popup closed with ' .. a:result
+	endfunc
+	let winid = popup_create('notification', {
+	      \ 'close': 'click',
+	      \ 'line': 3,
+	      \ 'col': 15,
+	      \ 'callback': 'CloseMsg',
+	      \ })
+	let winid = popup_create('no border here', {
+	      \ 'close': 'button',
+	      \ 'line': 5,
+	      \ 'col': 3,
+	      \ })
+	let winid = popup_create('only padding', {
+	      \ 'close': 'button',
+	      \ 'padding': [],
+	      \ 'line': 5,
+	      \ 'col': 23,
+	      \ })
+	func CloseWithX()
+	  call feedkeys("\<F3>\<LeftMouse>\<LeftRelease>", "xt")
+	endfunc
+	map <silent> <F3> :call test_setmouse(1, len('foobar') + 2)<CR>
+	func CloseWithClick()
+	  call feedkeys("\<F4>\<LeftMouse>\<LeftRelease>", "xt")
+	endfunc
+	map <silent> <F4> :call test_setmouse(3, 17)<CR>
+  END
+  call writefile(lines, 'XtestPopupClose')
+  let buf = RunVimInTerminal('-S XtestPopupClose', {'rows': 10})
+  call VerifyScreenDump(buf, 'Test_popupwin_close_01', {})
+
+  call term_sendkeys(buf, ":call CloseWithX()\<CR>")
+  call VerifyScreenDump(buf, 'Test_popupwin_close_02', {})
+
+  call term_sendkeys(buf, ":call CloseWithClick()\<CR>")
+  call VerifyScreenDump(buf, 'Test_popupwin_close_03', {})
+
+  " clean up
+  call StopVimInTerminal(buf)
+  call delete('XtestPopupClose')
+endfunction
+
 func Test_popup_with_mask()
   if !CanRunVimInTerminal()
     throw 'Skipped: cannot make screendumps'
diff --git a/src/ui.c b/src/ui.c
index d09e0ab..1732fe8 100644
--- a/src/ui.c
+++ b/src/ui.c
@@ -2929,6 +2929,7 @@
 #endif
 #ifdef FEAT_TEXT_PROP
     static int   in_popup_win = FALSE;
+    static win_T *click_in_popup_win = NULL;
 #endif
     static int	prev_row = -1;
     static int	prev_col = -1;
@@ -2957,7 +2958,11 @@
 	dragwin = NULL;
 	did_drag = FALSE;
 #ifdef FEAT_TEXT_PROP
+	if (click_in_popup_win != NULL && popup_dragwin == NULL)
+	    popup_close_for_mouse_click(click_in_popup_win);
+
 	popup_dragwin = NULL;
+	click_in_popup_win = NULL;
 #endif
     }
 
@@ -3001,6 +3006,7 @@
 	// Continue a modeless selection in a popup window or dragging it.
 	if (in_popup_win)
 	{
+	    click_in_popup_win = NULL;  // don't close it on release
 	    if (popup_dragwin != NULL)
 	    {
 		// dragging a popup window
@@ -3050,13 +3056,27 @@
 	{
 	    on_sep_line = 0;
 	    in_popup_win = TRUE;
-	    if (wp->w_popup_drag && popup_on_border(wp, row, col))
+	    if (wp->w_popup_close == POPCLOSE_BUTTON
+		    && which_button == MOUSE_LEFT
+		    && popup_on_X_button(wp, row, col))
+	    {
+		popup_close_for_mouse_click(wp);
+		return IN_UNKNOWN;
+	    }
+	    else if (wp->w_popup_drag && popup_on_border(wp, row, col))
 	    {
 		popup_dragwin = wp;
 		popup_start_drag(wp);
 		return IN_UNKNOWN;
 	    }
-	    if (which_button == MOUSE_LEFT)
+	    // Only close on release, otherwise it's not possible to drag or do
+	    // modeless selection.
+	    else if (wp->w_popup_close == POPCLOSE_CLICK
+		    && which_button == MOUSE_LEFT)
+	    {
+		click_in_popup_win = wp;
+	    }
+	    else if (which_button == MOUSE_LEFT)
 		// If the click is in the scrollbar, may scroll up/down.
 		popup_handle_scrollbar_click(wp, row, col);
 # ifdef FEAT_CLIPBOARD
@@ -3244,6 +3264,7 @@
 		return IN_UNKNOWN;
 	    }
 	    // continue a modeless selection in a popup window
+	    click_in_popup_win = NULL;
 	    return IN_OTHER_WIN;
 	}
 #endif
diff --git a/src/version.c b/src/version.c
index 656cb05..89b7f4f 100644
--- a/src/version.c
+++ b/src/version.c
@@ -778,6 +778,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1609,
+/**/
     1608,
 /**/
     1607,