From: Yasuhiro Matsumoto Date: Thu, 16 Apr 2026 20:29:33 +0000 (+0000) Subject: patch 9.2.0360: Cannot handle mouse-clicks in the tabpanel X-Git-Tag: v9.2.0360^0 X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=1c299f26316cde3f32ec95b7c440e868ada0cb20;p=thirdparty%2Fvim.git patch 9.2.0360: Cannot handle mouse-clicks in the tabpanel Problem: Cannot handle mouse-clicks in the tabpanel Solution: Add support using the %[FuncName] atom for the tabpanel (Yasuhiro Matsumoto) Extend the statusline/tabline click region mechanism to work with 'tabpanel'. The callback receives a dict with "area" set to "tabpanel" and a "tabnr" key indicating which tab page label was clicked. closes: #19960 Signed-off-by: Yasuhiro Matsumoto Signed-off-by: Christian Brabandt --- diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 31a9629c0b..62d7ef8dfd 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -1,4 +1,4 @@ -*options.txt* For Vim version 9.2. Last change: 2026 Apr 15 +*options.txt* For Vim version 9.2. Last change: 2026 Apr 16 VIM REFERENCE MANUAL by Bram Moolenaar @@ -8689,7 +8689,7 @@ A jump table for the options with a short description can be found at |Q_op|. *stl-%[FuncName]* %[ defines clickable regions in the statusline. When the user clicks on a region with the mouse, the specified function is called. The - same syntax can also be used in 'tabline'. + same syntax can also be used in 'tabline' and 'tabpanel'. %[FuncName] Start of a clickable region. "FuncName" is the name of a Vim function to call when the region is clicked. @@ -8708,16 +8708,16 @@ A jump table for the options with a short description can be found at |Q_op|. "mods" Modifier keys: combination of "s" (shift), "c" (ctrl), "a" (alt). Empty string if no modifiers. "winid" |window-ID| of the window whose statusline was clicked, - or 0 when the click was in 'tabline'. - "area" "statusline" or "tabline". Indicates which option the - clicked region belongs to. Useful when a single - callback is shared between 'statusline' and 'tabline'. + or 0 when the click was in 'tabline' or 'tabpanel'. + "area" "statusline", "tabline", or "tabpanel". Indicates + which option the clicked region belongs to. + "tabnr" (tabpanel only) Tab page number of the clicked label. If the function returns non-zero, the statusline is redrawn. Dragging the statusline to resize the window still works even when - click handlers are defined. When used in 'tabline', clicks in - %[FuncName] regions are dispatched to the callback instead of the - default tab page selection behavior. + click handlers are defined. When used in 'tabline' or 'tabpanel', + clicks in %[FuncName] regions are dispatched to the callback + instead of the default tab-selection behavior. Example: > func! ClickFile(info) diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt index ee24e29010..7fa53f87d8 100644 --- a/runtime/doc/version9.txt +++ b/runtime/doc/version9.txt @@ -1,4 +1,4 @@ -*version9.txt* For Vim version 9.2. Last change: 2026 Apr 15 +*version9.txt* For Vim version 9.2. Last change: 2026 Apr 16 VIM REFERENCE MANUAL by Bram Moolenaar @@ -52613,8 +52613,8 @@ Other ~ pairs individually (e.g. 'listchars', 'fillchars', 'diffopt'). - |system()| and |systemlist()| functions accept a list as first argument, bypassing the shell completely. -- Allow mouse clickable regions in the |status-line| using the - |stl-%[FuncName]| atom. +- Allow mouse clickable regions in the 'statusline', 'tabline' and the + 'tabpanel' using the |stl-%[FuncName]| atom. - Enable reflow support in the |:terminal|. Platform specific ~ diff --git a/src/globals.h b/src/globals.h index 20450aa68c..9a6c285937 100644 --- a/src/globals.h +++ b/src/globals.h @@ -108,6 +108,10 @@ EXTERN short *TabPageIdxs INIT(= NULL); EXTERN stl_click_region_T *tabline_stl_click INIT(= NULL); EXTERN int tabline_stl_click_count INIT(= 0); +// Click regions for 'tabpanel' (%[FuncName]). +EXTERN stl_click_region_T *tabpanel_stl_click INIT(= NULL); +EXTERN int tabpanel_stl_click_count INIT(= 0); + #ifdef FEAT_PROP_POPUP // Array with size Rows x Columns containing zindex of popups. EXTERN short *popup_mask INIT(= NULL); diff --git a/src/mouse.c b/src/mouse.c index 1077c5ee72..45ffd4b513 100644 --- a/src/mouse.c +++ b/src/mouse.c @@ -20,9 +20,11 @@ static long mouse_hor_step = 6; static long mouse_vert_step = 3; static win_T *dragwin = NULL; // window being dragged -static int stl_click_handler(win_T *wp, int mcol, int which_button, int mods); +static int stl_click_handler(win_T *wp, int mrow, int mcol, int which_button, + int mods); static int stl_click_handler_regions(stl_click_region_T *regions, - int region_count, int winid, int mcol, + int region_count, int winid, + char_u *area_name, int mrow, int mcol, int which_button, int mods); void @@ -492,6 +494,17 @@ do_mouse( if (mouse_col < firstwin->w_wincol || mouse_col >= firstwin->w_wincol + topframe->fr_width) { + // Dispatch 'tabpanel' %[FuncName] click regions before falling through + // to tab-page selection. On drag events fall through to the normal + // tab-drag handling. + if (is_click && !is_drag + && stl_click_handler_regions(tabpanel_stl_click, + tabpanel_stl_click_count, + 0, (char_u *)"tabpanel", + mouse_row, mouse_col, + which_button, mod_mask)) + return FALSE; + tp_label.is_panel = true; tp_label.just_in = true; tp_label.nr = get_tabpagenr_on_tabpanel(); @@ -511,8 +524,9 @@ do_mouse( if (is_click && !is_drag && stl_click_handler_regions(tabline_stl_click, tabline_stl_click_count, - 0, mouse_col, which_button, - mod_mask)) + 0, (char_u *)"tabline", + mouse_row, mouse_col, + which_button, mod_mask)) return FALSE; tp_label.just_in = true; @@ -778,7 +792,7 @@ do_mouse( // Check for statusline click handler early, before visual mode or // other button-specific handling can interfere. if (in_status_line && is_click && !is_drag - && stl_click_handler(dragwin, mouse_col, + && stl_click_handler(dragwin, mouse_row, mouse_col, which_button, mod_mask)) { #ifdef FEAT_MOUSESHAPE @@ -1663,6 +1677,8 @@ stl_click_handler_regions( stl_click_region_T *regions, int region_count, int winid, + char_u *area_name, + int mrow, int mcol, int which_button, int mods) @@ -1682,10 +1698,12 @@ stl_click_handler_regions( if (regions == NULL || region_count == 0) return FALSE; - // Find the click region at the given column. + // Find the click region at the given row and column. for (n = 0; n < region_count; n++) { - if (col >= regions[n].col_start && col < regions[n].col_end) + if (regions[n].row == mrow + && col >= regions[n].col_start + && col < regions[n].col_end) break; } if (n >= region_count || regions[n].funcname == NULL) @@ -1728,10 +1746,13 @@ stl_click_handler_regions( dict_add_number(info, "winid", winid); // "area": which option the clicked region belongs to. Lets a shared - // dispatcher distinguish 'statusline' from 'tabline' (and future areas) - // without having to overload winid == 0. - dict_add_string(info, "area", - winid == 0 ? (char_u *)"tabline" : (char_u *)"statusline"); + // dispatcher distinguish 'statusline', 'tabline' and 'tabpanel' without + // having to overload winid == 0. + dict_add_string(info, "area", area_name); + + // Expose tab page number for 'tabpanel' regions. + if (regions[n].tabnr > 0) + dict_add_number(info, "tabnr", regions[n].tabnr); // Call the function with the info dict as argument. argvars[0].v_type = VAR_DICT; @@ -1755,9 +1776,14 @@ stl_click_handler_regions( { // Make sure the tabline gets redrawn too when the callback asks for // a redraw (redraw_statuslines() only redraws the tabline when - // redraw_tabline is set). + // redraw_tabline is set). For tabpanel the whole screen needs to be + // refreshed. if (winid == 0) redraw_tabline = TRUE; +# ifdef FEAT_TABPANEL + if (STRCMP(area_name, "tabpanel") == 0) + redraw_all_later(UPD_NOT_VALID); +# endif redraw_statuslines(); } @@ -1766,6 +1792,8 @@ stl_click_handler_regions( (void)regions; (void)region_count; (void)winid; + (void)area_name; + (void)mrow; (void)mcol; (void)which_button; (void)mods; @@ -1778,12 +1806,13 @@ stl_click_handler_regions( * Returns TRUE if the function was called and handled the click. */ static int -stl_click_handler(win_T *wp, int mcol, int which_button, int mods) +stl_click_handler(win_T *wp, int mrow, int mcol, int which_button, int mods) { if (wp == NULL) return FALSE; return stl_click_handler_regions(wp->w_stl_click, wp->w_stl_click_count, - wp->w_id, mcol, which_button, mods); + wp->w_id, (char_u *)"statusline", + mrow, mcol, which_button, mods); } // dragwin is declared near the top of the file diff --git a/src/screen.c b/src/screen.c index eb7cc9cdd1..b9bde1d574 100644 --- a/src/screen.c +++ b/src/screen.c @@ -1492,8 +1492,13 @@ win_redr_custom( stl_click_region_T **out_regions; int *out_count; int base_col; + int base_row; int click_count = 0; + // clicktab reflects the last iteration of the draw loop above, so + // the regions belong to the last drawn row. + base_row = row + stlh_cnt - 1; + if (wp != NULL) { out_regions = &wp->w_stl_click; @@ -1547,11 +1552,13 @@ win_redr_custom( // Close previous region if there was one. if (cur_funcname != NULL) { + regions[rcount].row = base_row; regions[rcount].col_start = region_start; regions[rcount].col_end = base_col + len; regions[rcount].funcname = vim_strsave(cur_funcname); regions[rcount].minwid = cur_minwid; + regions[rcount].tabnr = 0; rcount++; } @@ -1563,11 +1570,13 @@ win_redr_custom( // Close final region if it extends to the end. if (cur_funcname != NULL) { + regions[rcount].row = base_row; regions[rcount].col_start = region_start; regions[rcount].col_end = base_col + maxwidth; regions[rcount].funcname = vim_strsave(cur_funcname); regions[rcount].minwid = cur_minwid; + regions[rcount].tabnr = 0; rcount++; } diff --git a/src/structs.h b/src/structs.h index 648ceec905..e76c651d2e 100644 --- a/src/structs.h +++ b/src/structs.h @@ -1451,10 +1451,12 @@ typedef struct { * Per-window resolved click regions (screen column based). */ typedef struct { + int row; // screen row where region lives int col_start; // screen column where region starts int col_end; // screen column where region ends char_u *funcname; // function name (allocated copy) int minwid; // minwid value + int tabnr; // tab page number (tabpanel only, 0 otherwise) } stl_click_region_T; diff --git a/src/tabpanel.c b/src/tabpanel.c index 51929a8b1c..4138f9760a 100644 --- a/src/tabpanel.c +++ b/src/tabpanel.c @@ -17,6 +17,9 @@ static void do_by_tplmode(int tplmode, int col_start, int col_end, int *pcurtab_row, int *ptabpagenr); +static void tabpanel_free_click_regions(void); +static void tabpanel_append_click_regions(stl_clickrec_T *clicktab, + char_u *buf, int row, int col_start, int col_end, int tabnr); // set pcurtab_row. don't redraw tabpanel. #define TPLMODE_GET_CURTAB_ROW 0 @@ -135,6 +138,108 @@ tabpanel_leftcol(void) return tpl_align == ALIGN_RIGHT ? 0 : tabpanel_width(); } +/* + * Free previously resolved 'tabpanel' click regions. + */ + static void +tabpanel_free_click_regions(void) +{ + int n; + + if (tabpanel_stl_click != NULL) + { + for (n = 0; n < tabpanel_stl_click_count; n++) + vim_free(tabpanel_stl_click[n].funcname); + VIM_CLEAR(tabpanel_stl_click); + } + tabpanel_stl_click_count = 0; +} + +/* + * Convert click records produced by build_stl_str_hl() for one line of + * 'tabpanel' into screen-column based regions and append them to the global + * tabpanel_stl_click array. The caller keeps ownership of the funcname + * strings inside "clicktab" — this function makes its own copies. + */ + static void +tabpanel_append_click_regions( + stl_clickrec_T *clicktab, + char_u *buf, + int row, + int col_start, + int col_end, + int tabnr) +{ + int count = 0; + int n; + int base_col; + int acc_width = 0; + int max_w = col_end - col_start; + char_u *p; + char_u *cur_funcname = NULL; + int cur_minwid = 0; + int region_start_col; + stl_click_region_T *new_arr; + int limit; + + if (clicktab == NULL) + return; + + for (n = 0; clicktab[n].start != NULL; n++) + count++; + if (count == 0) + return; + + base_col = (tpl_align == ALIGN_RIGHT ? topframe->fr_width : 0) + col_start; + region_start_col = base_col; + + // Grow the global array to make room for up to "count" more regions + // (one close for each record plus a possible trailing region). + new_arr = vim_realloc(tabpanel_stl_click, + sizeof(stl_click_region_T) * (tabpanel_stl_click_count + count + 1)); + if (new_arr == NULL) + return; + tabpanel_stl_click = new_arr; + + p = buf; + for (n = 0; clicktab[n].start != NULL; n++) + { + acc_width += vim_strnsize(p, (int)(clicktab[n].start - p)); + p = clicktab[n].start; + limit = acc_width < max_w ? acc_width : max_w; + + if (cur_funcname != NULL) + { + stl_click_region_T *r = + &tabpanel_stl_click[tabpanel_stl_click_count]; + r->row = row; + r->col_start = region_start_col; + r->col_end = base_col + limit; + r->funcname = vim_strsave(cur_funcname); + r->minwid = cur_minwid; + r->tabnr = tabnr; + tabpanel_stl_click_count++; + } + + cur_funcname = clicktab[n].funcname; + cur_minwid = clicktab[n].minwid; + region_start_col = base_col + limit; + } + + // Close the final region if it extends to the end. + if (cur_funcname != NULL) + { + stl_click_region_T *r = &tabpanel_stl_click[tabpanel_stl_click_count]; + r->row = row; + r->col_start = region_start_col; + r->col_end = base_col + max_w; + r->funcname = vim_strsave(cur_funcname); + r->minwid = cur_minwid; + r->tabnr = tabnr; + tabpanel_stl_click_count++; + } +} + /* * draw the tabpanel. */ @@ -150,7 +255,13 @@ draw_tabpanel(void) int is_right = tpl_align == ALIGN_RIGHT; if (maxwidth == 0) + { + tabpanel_free_click_regions(); return; + } + + // Discard old click regions — they'll be rebuilt during redraw below. + tabpanel_free_click_regions(); // Reset got_int to avoid build_stl_str_hl() isn't evaluated. got_int = FALSE; @@ -495,6 +606,7 @@ do_by_tplmode( char_u buf[IOSIZE]; stl_hlrec_T *hltab; stl_hlrec_T *tabtab; + stl_clickrec_T *clicktab = NULL; if (args.maxrow <= row - args.offsetrow) break; @@ -508,13 +620,31 @@ do_by_tplmode( (args.cwp, buf, sizeof(buf), &usefmt, opt_name, opt_scope, TPL_FILLCHAR, args.col_end - args.col_start, &hltab, &tabtab, - NULL); + tplmode == TPLMODE_REDRAW ? &clicktab : NULL); args.prow = &row; args.pcol = &col; draw_tabpanel_with_highlight(tplmode, buf, hltab, &args); + // Record any %[FuncName] click regions for this line once + // the text has been drawn. Only visible rows participate. + if (tplmode == TPLMODE_REDRAW && clicktab != NULL) + { + int screen_row = row - args.offsetrow; + int m; + + if (screen_row >= 0 && screen_row < args.maxrow) + tabpanel_append_click_regions(clicktab, buf, + screen_row, args.col_start, args.col_end, + (int)v.vval.v_number); + // We took ownership of the click records — free the + // function names (matches the non-NULL clicktab path in + // build_stl_str_hl()). + for (m = 0; clicktab[m].start != NULL; m++) + vim_free(clicktab[m].funcname); + } + // Move to next line for %@ if (*usefmt != NUL) { diff --git a/src/testdir/test_tabpanel.vim b/src/testdir/test_tabpanel.vim index 4bb7f39eb0..7803a0eb4d 100644 --- a/src/testdir/test_tabpanel.vim +++ b/src/testdir/test_tabpanel.vim @@ -249,6 +249,76 @@ function Test_tabpanel_mouse() let &showtabline = save_showtabline endfunc +func g:TplClickTestFunc(info) + let g:tpl_click_info = a:info + return 0 +endfunc + +function Test_tabpanel_click_handler() + let save_mouse = &mouse + let save_stal = &showtabline + let save_stpl = &showtabpanel + let save_tpl = &tabpanel + let save_tplo = &tabpanelopt + set mouse=a + set showtabline=0 + set showtabpanel=2 + set tabpanelopt=columns:16 + tabnew + tabnew + + " Place two adjacent %[FuncName] regions on every tab label. + set tabpanel=%1[TplClickTestFunc][A]%[]%2[TplClickTestFunc][B]%[] + redraw! + + " Click on [A] region in the first tab label (row 1). + call test_setmouse(1, 2) + call feedkeys("\\", 'xt') + call assert_true(exists('g:tpl_click_info')) + call assert_equal('l', g:tpl_click_info.button) + call assert_equal(1, g:tpl_click_info.nclicks) + call assert_equal(1, g:tpl_click_info.minwid) + call assert_equal(0, g:tpl_click_info.winid) + call assert_equal('tabpanel', g:tpl_click_info.area) + call assert_equal(1, g:tpl_click_info.tabnr) + unlet! g:tpl_click_info + + " Click on [B] region in the second tab label (row 2). + call test_setmouse(2, 5) + call feedkeys("\\", 'xt') + call assert_true(exists('g:tpl_click_info')) + call assert_equal(2, g:tpl_click_info.minwid) + call assert_equal(2, g:tpl_click_info.tabnr) + unlet! g:tpl_click_info + + " Middle click on [A] in tab 3. + call test_setmouse(3, 2) + call feedkeys("\\", 'xt') + call assert_true(exists('g:tpl_click_info')) + call assert_equal('m', g:tpl_click_info.button) + call assert_equal(1, g:tpl_click_info.minwid) + call assert_equal(3, g:tpl_click_info.tabnr) + unlet! g:tpl_click_info + + " A click outside any region (but still in the panel) must not fire the + " callback, and should fall through to the normal tab selection. + set tabpanel=xxx%1[TplClickTestFunc][Y]%[] + redraw! + tabfirst + call test_setmouse(2, 1) + call feedkeys("\\", 'xt') + call assert_false(exists('g:tpl_click_info')) + call assert_equal(2, tabpagenr()) + + tabonly! + call s:reset() + let &tabpanel = save_tpl + let &tabpanelopt = save_tplo + let &showtabpanel = save_stpl + let &showtabline = save_stal + let &mouse = save_mouse +endfunc + function Test_tabpanel_drawing() CheckScreendump diff --git a/src/version.c b/src/version.c index b015465ed5..2361deb7de 100644 --- a/src/version.c +++ b/src/version.c @@ -734,6 +734,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 360, /**/ 359, /**/