diff --git a/.github/actions/build_vim_on_linux/action.yml b/.github/actions/build_vim_on_linux/action.yml index 9a2105164b..c6486cd34f 100644 --- a/.github/actions/build_vim_on_linux/action.yml +++ b/.github/actions/build_vim_on_linux/action.yml @@ -88,6 +88,7 @@ runs: sudo cp ci/pinned-pkgs /etc/apt/preferences.d/pinned-pkgs echo '::endgroup::' + # TODO: switch to GTK4 GUI - name: Install packages shell: bash run: | diff --git a/Filelist b/Filelist index ceb06739b9..dfc828a759 100644 --- a/Filelist +++ b/Filelist @@ -498,6 +498,9 @@ SRC_UNIX = \ src/gui_gtk_f.c \ src/gui_gtk_f.h \ src/gui_gtk_x11.c \ + src/gui_gtk4.c \ + src/gui_gtk4_f.c \ + src/gui_gtk4_f.h \ src/gui_gtk_res.xml \ src/gui_motif.c \ src/gui_xmdlg.c \ @@ -527,6 +530,7 @@ SRC_UNIX = \ src/proto/gui_gtk.pro \ src/proto/gui_gtk_x11.pro \ src/proto/gui_gtk_gresources.pro \ + src/proto/gui_gtk4.pro \ src/proto/gui_motif.pro \ src/proto/gui_xmdlg.pro \ src/proto/gui_x11.pro \ diff --git a/src/INSTALL b/src/INSTALL index 083780cc82..96f32607fc 100644 --- a/src/INSTALL +++ b/src/INSTALL @@ -65,7 +65,11 @@ To build Vim on Ubuntu from scratch on a clean system using git: % sudo apt install libwayland-dev % make reconfig - Add GUI support: + Add GUI (GTK3) support: + % sudo apt install libgtk-3-dev + % make reconfig + + Add GUI (GTK4) support: % sudo apt install libgtk-3-dev % make reconfig diff --git a/src/Makefile b/src/Makefile index 97200a6b07..e08b6cccfd 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1232,6 +1232,23 @@ GTK_MAN_TARGETS = yes GTK_TESTTARGET = gui GTK_BUNDLE = +### GTK4 GUI +GTK4_SRC = gui.c gui_gtk4.c gui_gtk4_f.c \ + $(GRESOURCE_SRC) +GTK4_OBJ = objects/gui.o objects/gui_gtk4.o \ + objects/gui_gtk4_f.o \ + $(GRESOURCE_OBJ) +GTK4_DEFS = -DFEAT_GUI_GTK $(NARROW_PROTO) +GTK4_IPATH = $(GUI_INC_LOC) +GTK4_LIBS_DIR = $(GUI_LIB_LOC) +GTK4_LIBS1 = +GTK4_LIBS2 = $(GTK_LIBNAME) +GTK4_INSTALL = install_normal install_gui_extra +GTK4_TARGETS = installglinks +GTK4_MAN_TARGETS = yes +GTK4_TESTTARGET = gui +GTK4_BUNDLE = + ### Motif GUI MOTIF_SRC = gui.c gui_motif.c gui_x11.c gui_beval.c \ gui_xmdlg.c gui_xmebw.c @@ -1289,8 +1306,8 @@ HAIKUGUI_TESTTARGET = gui HAIKUGUI_BUNDLE = # All GUI files -ALL_GUI_SRC = gui.c gui_gtk.c gui_gtk_f.c gui_motif.c gui_xmdlg.c gui_xmebw.c gui_gtk_x11.c gui_x11.c gui_haiku.cc -ALL_GUI_PRO = proto/gui.pro proto/gui_gtk.pro proto/gui_motif.pro proto/gui_xmdlg.pro proto/gui_gtk_x11.pro proto/gui_x11.pro proto/gui_w32.pro proto/gui_photon.pro +ALL_GUI_SRC = gui.c gui_gtk.c gui_gtk_f.c gui_gtk4.c gui_gtk4_f.c gui_motif.c gui_xmdlg.c gui_xmebw.c gui_gtk_x11.c gui_x11.c gui_haiku.cc +ALL_GUI_PRO = proto/gui.pro proto/gui_gtk.pro proto/gui_gtk4.pro proto/gui_motif.pro proto/gui_xmdlg.pro proto/gui_gtk_x11.pro proto/gui_x11.pro proto/gui_w32.pro proto/gui_photon.pro # }}} @@ -3364,6 +3381,13 @@ objects/gui_gtk_gresources.o: auto/gui_gtk_gresources.c objects/gui_gtk_x11.o: gui_gtk_x11.c $(CCC) -o $@ gui_gtk_x11.c +objects/gui_gtk4.o: gui_gtk4.c + $(CCC) -o $@ gui_gtk4.c + +objects/gui_gtk4_f.o: gui_gtk4_f.c + $(CCC) -o $@ gui_gtk4_f.c + + objects/gui_haiku.o: gui_haiku.cc $(CCC) -o $@ gui_haiku.cc @@ -4797,6 +4821,7 @@ proto/winclip.pro: winclip.c proto/window.pro: window.c proto/gui.pro: gui.c proto/gui_gtk.pro: gui_gtk.c +proto/gui_gtk4.pro: gui_gtk4.c proto/gui_motif.pro: gui_motif.c proto/gui_xmdlg.pro: gui_xmdlg.c proto/gui_gtk_x11.pro: gui_gtk_x11.c diff --git a/src/auto/configure b/src/auto/configure index 1705525156..ff4d5e361c 100755 --- a/src/auto/configure +++ b/src/auto/configure @@ -862,6 +862,7 @@ with_x enable_gui enable_gtk2_check enable_gnome_check +enable_gtk4_check enable_gtk3_check enable_motif_check enable_gtktest @@ -1538,9 +1539,10 @@ Optional Features: --disable-farsi Deprecated. --enable-xim Include XIM input support. --enable-fontset Include X fontset output support. - --enable-gui=OPTS X11 GUI. default=auto OPTS=auto/no/gtk2/gnome2/gtk3/motif/haiku/photon/carbon + --enable-gui=OPTS X11 GUI. default=auto OPTS=auto/no/gtk2/gnome2/gtk3/gtk4/motif/haiku/photon/carbon --enable-gtk2-check If auto-select GUI, check for GTK+ 2 default=yes --enable-gnome-check If GTK GUI, check for GNOME default=no + --enable-gtk4-check If auto-select GUI, check for GTK 4 default=yes --enable-gtk3-check If auto-select GUI, check for GTK+ 3 default=yes --enable-motif-check If auto-select GUI, check for Motif default=yes --disable-gtktest Do not try to compile and run a test GTK program @@ -10486,6 +10488,7 @@ enable_gui_canon=`echo "_$enable_gui" | \ SKIP_GTK2=YES SKIP_GTK3=YES +SKIP_GTK4=YES SKIP_GNOME=YES SKIP_MOTIF=YES SKIP_PHOTON=YES @@ -10516,7 +10519,7 @@ printf "%s\n" "no GUI support" >&6; } SKIP_PHOTON=YES ;; yes|""|auto) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: automatic GUI support" >&5 printf "%s\n" "automatic GUI support" >&6; } - gui_auto=yes ;; + gui_auto=yes ;; photon) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: Photon GUI support" >&5 printf "%s\n" "Photon GUI support" >&6; } ;; *) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: Sorry, $enable_gui GUI is not supported" >&5 @@ -10530,7 +10533,7 @@ elif test "x$MACOS_X" = "xyes" -a "x$with_x" = "xno" ; then printf "%s\n" "no GUI support" >&6; } ;; yes|"") { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes - automatic GUI support" >&5 printf "%s\n" "yes - automatic GUI support" >&6; } - gui_auto=yes ;; + gui_auto=yes ;; auto) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: auto - disable GUI support for Mac OS" >&5 printf "%s\n" "auto - disable GUI support for Mac OS" >&6; } ;; *) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: Sorry, $enable_gui GUI is not supported" >&5 @@ -10546,6 +10549,7 @@ printf "%s\n" "yes/auto - automatic GUI support" >&6; } gui_auto=yes SKIP_GTK2= SKIP_GTK3= + SKIP_GTK4= SKIP_GNOME= SKIP_MOTIF=;; gtk2) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: GTK+ 2.x GUI support" >&5 @@ -10558,6 +10562,9 @@ printf "%s\n" "GNOME 2.x GUI support" >&6; } gtk3) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: GTK+ 3.x GUI support" >&5 printf "%s\n" "GTK+ 3.x GUI support" >&6; } SKIP_GTK3=;; + gtk4) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: GTK 4.x GUI support" >&5 +printf "%s\n" "GTK 4.x GUI support" >&6; } + SKIP_GTK4=;; motif) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: Motif GUI support" >&5 printf "%s\n" "Motif GUI support" >&6; } SKIP_MOTIF=;; @@ -10607,6 +10614,25 @@ printf "%s\n" "$enable_gnome_check" >&6; } fi fi +if test "x$SKIP_GTK4" != "xYES" -a "$enable_gui_canon" != "gtk4"; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether or not to look for GTK 4" >&5 +printf %s "checking whether or not to look for GTK 4... " >&6; } + # Check whether --enable-gtk4-check was given. +if test ${enable_gtk4_check+y} +then : + enableval=$enable_gtk4_check; +else case e in #( + e) enable_gtk4_check="yes" ;; +esac +fi + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $enable_gtk4_check" >&5 +printf "%s\n" "$enable_gtk4_check" >&6; } + if test "x$enable_gtk4_check" = "xno"; then + SKIP_GTK4=YES + fi +fi + if test "x$SKIP_GTK3" != "xYES" -a "$enable_gui_canon" != "gtk3"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether or not to look for GTK+ 3" >&5 printf %s "checking whether or not to look for GTK+ 3... " >&6; } @@ -10681,6 +10707,8 @@ printf "%s\n" "gtk test disabled" >&6; } gtk_pkg_name="gtk+-2.0" ;; #( 3.*) : gtk_pkg_name="gtk+-3.0" ;; #( + 4.*) : + gtk_pkg_name="gtk4" ;; #( *) : { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} @@ -10697,7 +10725,7 @@ then : printf "%s\n" "found" >&6; } { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for GTK - version >= $min_gtk_version" >&5 printf %s "checking for GTK - version >= $min_gtk_version... " >&6; } - GTK_CPPFLAGS=`$PKG_CONFIG --cflags-only-I $gtk_pkg_name` + GTK_CPPFLAGS=`$PKG_CONFIG --cflags-only-I $gtk_pkg_name` GTK_CFLAGS=`$PKG_CONFIG --cflags-only-other $gtk_pkg_name` GTK_LIBDIR=`$PKG_CONFIG --libs-only-L $gtk_pkg_name` GTK_LIBS=`$PKG_CONFIG --libs $gtk_pkg_name` @@ -10821,6 +10849,7 @@ fi if test -n "$GTK_CPPFLAGS"; then SKIP_GTK2=YES + SKIP_GTK4=YES SKIP_GNOME=YES SKIP_MOTIF=YES GUITYPE=GTK @@ -10831,6 +10860,196 @@ fi fi fi +if test -z "$SKIP_GTK4"; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking --disable-gtktest argument" >&5 +printf %s "checking --disable-gtktest argument... " >&6; } + # Check whether --enable-gtktest was given. +if test ${enable_gtktest+y} +then : + enableval=$enable_gtktest; +else case e in #( + e) enable_gtktest=yes ;; +esac +fi + + if test "x$enable_gtktest" = "xyes" ; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: gtk test enabled" >&5 +printf "%s\n" "gtk test enabled" >&6; } + else + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: gtk test disabled" >&5 +printf "%s\n" "gtk test disabled" >&6; } + fi + + if test "x$PKG_CONFIG" != "xno"; then + + min_gtk_version="4.0.0" + + if test "$PKG_CONFIG" != "no"; then + case $min_gtk_version in #( + 2.*) : + gtk_pkg_name="gtk+-2.0" ;; #( + 3.*) : + gtk_pkg_name="gtk+-3.0" ;; #( + 4.*) : + gtk_pkg_name="gtk4" ;; #( + *) : + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 +printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} +as_fn_error $? "The configure script does not know which pkg-config name to use for GTK $min_gtk_version\" +See 'config.log' for more details" "$LINENO" 5; } ;; +esac + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for pkg-config $gtk_pkg_name" >&5 +printf %s "checking for pkg-config $gtk_pkg_name... " >&6; } + if "$PKG_CONFIG" --exists "$gtk_pkg_name" +then : + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: found" >&5 +printf "%s\n" "found" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for GTK - version >= $min_gtk_version" >&5 +printf %s "checking for GTK - version >= $min_gtk_version... " >&6; } + GTK_CPPFLAGS=`$PKG_CONFIG --cflags-only-I $gtk_pkg_name` + GTK_CFLAGS=`$PKG_CONFIG --cflags-only-other $gtk_pkg_name` + GTK_LIBDIR=`$PKG_CONFIG --libs-only-L $gtk_pkg_name` + GTK_LIBS=`$PKG_CONFIG --libs $gtk_pkg_name` + gtk_major_version=`$PKG_CONFIG --modversion $gtk_pkg_name | \ + sed 's/\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)/\1/'` + gtk_minor_version=`$PKG_CONFIG --modversion $gtk_pkg_name | \ + sed 's/\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)/\2/'` + gtk_micro_version=`$PKG_CONFIG --modversion $gtk_pkg_name | \ + sed 's/\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)/\3/'` + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes; found version $gtk_major_version.$gtk_minor_version.$gtk_micro_version" >&5 +printf "%s\n" "yes; found version $gtk_major_version.$gtk_minor_version.$gtk_micro_version" >&6; } + +else case e in #( + e) + GTK_CPPFLAGS="" + GTK_CFLAGS="" + GTK_LIBDIR="" + GTK_LIBS="" + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no; consider installing your distro GTK -dev package" >&5 +printf "%s\n" "no; consider installing your distro GTK -dev package" >&6; } + if test "$fail_if_missing" = "yes" -a "$gui_auto" != "yes"; then + as_fn_error $? "pkg-config could not find $gtk_pkg_name" "$LINENO" 5 + fi + ;; +esac +fi + fi + + gtktest_success="yes" + if test "$enable_gtktest" = "yes"; then + { + ac_save_CPPFLAGS="$CPPFLAGS" + ac_save_CFLAGS="$CFLAGS" + ac_save_LIBS="$LIBS" + CPPFLAGS="$CPPFLAGS $GTK_CPPFLAGS" + CFLAGS="$CFLAGS $GTK_CFLAGS" + LIBS="$LIBS $GTK_LIBS" + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking ability to compile GTK test program" >&5 +printf %s "checking ability to compile GTK test program... " >&6; } + if test "$cross_compiling" = yes +then : + echo $ac_n "cross compiling; assumed OK... $ac_c" +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +#include +#include +#if STDC_HEADERS +# include +# include +#endif + +int +main () +{ + int ex_major = $gtk_major_version; + int ex_minor = $gtk_minor_version; + int ex_micro = $gtk_micro_version; + + #if $gtk_major_version == 2 + guint ob_major = gtk_major_version; + guint ob_minor = gtk_minor_version; + guint ob_micro = gtk_micro_version; + #else + guint ob_major = gtk_get_major_version(); + guint ob_minor = gtk_get_minor_version(); + guint ob_micro = gtk_get_micro_version(); + #endif + + if ((ob_major > ex_major) || + ((ob_major == ex_major) + && (ob_minor > ex_minor)) || + ((ob_major == ex_major) + && (ob_minor == ex_minor) + && (ob_micro >= ex_micro))) + return 0; + else + return 1; +} + +_ACEOF +if ac_fn_c_try_run "$LINENO" +then : + gtktest_success="yes"; { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } +else case e in #( + e) gtktest_success="no"; { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } ;; +esac +fi +rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ + conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac +fi + + CPPFLAGS="$ac_save_CPPFLAGS" + CFLAGS="$ac_save_CFLAGS" + LIBS="$ac_save_LIBS" + } + fi + + if test "$gtktest_success" = "yes"; then + GUI_LIB_LOC="$GTK_LIBDIR" + GTK_LIBNAME="$GTK_LIBS" + GUI_INC_LOC="$GTK_CPPFLAGS" + else + GTK_CPPFLAGS="" + GTK_CFLAGS="" + GTK_LIBDIR="" + GTK_LIBS="" + if test "$fail_if_missing" = "yes" -a "$gui_auto" != "yes"; then + as_fn_error $? "Failed to compile GTK test program." "$LINENO" 5 + fi + fi + + + + + + if test -n "$GTK_CPPFLAGS"; then + SKIP_GTK3=YES + SKIP_GTK2=YES + SKIP_GNOME=YES + SKIP_MOTIF=YES + GUITYPE=GTK4 + + printf "%s\n" "#define USE_GTK4 1" >>confdefs.h + + X_LIBS= + X_PRE_LIBS= + X_EXTRA_LIBS= + X_LIB= + if test "$enable_xim" = "auto"; then + enable_xim="yes" + fi + fi + fi +fi + if test -z "$SKIP_GTK2"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking --disable-gtktest argument" >&5 printf %s "checking --disable-gtktest argument... " >&6; } @@ -10861,6 +11080,8 @@ printf "%s\n" "gtk test disabled" >&6; } gtk_pkg_name="gtk+-2.0" ;; #( 3.*) : gtk_pkg_name="gtk+-3.0" ;; #( + 4.*) : + gtk_pkg_name="gtk4" ;; #( *) : { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} @@ -10877,7 +11098,7 @@ then : printf "%s\n" "found" >&6; } { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for GTK - version >= $min_gtk_version" >&5 printf %s "checking for GTK - version >= $min_gtk_version... " >&6; } - GTK_CPPFLAGS=`$PKG_CONFIG --cflags-only-I $gtk_pkg_name` + GTK_CPPFLAGS=`$PKG_CONFIG --cflags-only-I $gtk_pkg_name` GTK_CFLAGS=`$PKG_CONFIG --cflags-only-other $gtk_pkg_name` GTK_LIBDIR=`$PKG_CONFIG --libs-only-L $gtk_pkg_name` GTK_LIBS=`$PKG_CONFIG --libs $gtk_pkg_name` diff --git a/src/beval.h b/src/beval.h index 694bec4dc5..4225e926d2 100644 --- a/src/beval.h +++ b/src/beval.h @@ -11,7 +11,7 @@ #define BEVAL__H #ifdef FEAT_GUI_GTK -# ifdef USE_GTK3 +# if defined(USE_GTK3) || defined(USE_GTK4) # include # else # include diff --git a/src/clipboard.c b/src/clipboard.c index 902f9d6032..6f847edc1c 100644 --- a/src/clipboard.c +++ b/src/clipboard.c @@ -1891,7 +1891,8 @@ clip_x11_set_selection(Clipboard_T *cbd UNUSED) # endif -# if defined(FEAT_XCLIPBOARD) || defined(FEAT_GUI_X11) || defined(FEAT_GUI_GTK) +# if (defined(FEAT_XCLIPBOARD) || defined(FEAT_GUI_X11) || defined(FEAT_GUI_GTK)) \ + && !defined(USE_GTK4) /* * Get the contents of the X CUT_BUFFER0 and put it in "cbd". */ @@ -3298,7 +3299,7 @@ did_set_clipboard(optset_T *args UNUSED) vim_regfree(clip_exclude_prog); clip_exclude_prog = new_exclude_prog; # endif -# ifdef FEAT_GUI_GTK +# if defined(FEAT_GUI_GTK) && !defined(USE_GTK4) if (gui.in_use) { gui_gtk_set_selection_targets((GdkAtom)GDK_SELECTION_PRIMARY); diff --git a/src/config.h.in b/src/config.h.in index 699fde7ae0..e4273f5b7c 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -496,6 +496,9 @@ /* Define if GTK+ GUI is to be linked against GTK+ 3 */ #undef USE_GTK3 +/* Define if GTK GUI is to be linked against GTK 4 */ +#undef USE_GTK4 + /* Define if we have isinf() */ #undef HAVE_ISINF diff --git a/src/configure.ac b/src/configure.ac index 5f79bb6a0e..a808530d24 100644 --- a/src/configure.ac +++ b/src/configure.ac @@ -2613,7 +2613,7 @@ test "x$with_x" = xno -a "x$HAIKU" != "xyes" -a "x$MACOS_X" != "xyes" -a "x$QNX" AC_MSG_CHECKING(--enable-gui argument) AC_ARG_ENABLE(gui, - [ --enable-gui[=OPTS] X11 GUI. [default=auto] [OPTS=auto/no/gtk2/gnome2/gtk3/motif/haiku/photon/carbon]], , enable_gui="auto") + [ --enable-gui[=OPTS] X11 GUI. [default=auto] [OPTS=auto/no/gtk2/gnome2/gtk3/gtk4/motif/haiku/photon/carbon]], , enable_gui="auto") dnl Canonicalize the --enable-gui= argument so that it can be easily compared. dnl Do not use character classes for portability with old tools. @@ -2623,6 +2623,7 @@ enable_gui_canon=`echo "_$enable_gui" | \ dnl Skip everything by default. SKIP_GTK2=YES SKIP_GTK3=YES +SKIP_GTK4=YES SKIP_GNOME=YES SKIP_MOTIF=YES SKIP_PHOTON=YES @@ -2646,7 +2647,7 @@ elif test "x$QNX" = "xyes" -a "x$with_x" = "xno" ; then no) AC_MSG_RESULT(no GUI support) SKIP_PHOTON=YES ;; yes|""|auto) AC_MSG_RESULT(automatic GUI support) - gui_auto=yes ;; + gui_auto=yes ;; photon) AC_MSG_RESULT(Photon GUI support) ;; *) AC_MSG_RESULT([Sorry, $enable_gui GUI is not supported]) SKIP_PHOTON=YES ;; @@ -2656,7 +2657,7 @@ elif test "x$MACOS_X" = "xyes" -a "x$with_x" = "xno" ; then case "$enable_gui_canon" in no) AC_MSG_RESULT(no GUI support) ;; yes|"") AC_MSG_RESULT(yes - automatic GUI support) - gui_auto=yes ;; + gui_auto=yes ;; auto) AC_MSG_RESULT(auto - disable GUI support for Mac OS) ;; *) AC_MSG_RESULT([Sorry, $enable_gui GUI is not supported]) ;; esac @@ -2668,6 +2669,7 @@ else gui_auto=yes SKIP_GTK2= SKIP_GTK3= + SKIP_GTK4= SKIP_GNOME= SKIP_MOTIF=;; gtk2) AC_MSG_RESULT(GTK+ 2.x GUI support) @@ -2677,6 +2679,8 @@ else SKIP_GTK2=;; gtk3) AC_MSG_RESULT(GTK+ 3.x GUI support) SKIP_GTK3=;; + gtk4) AC_MSG_RESULT(GTK 4.x GUI support) + SKIP_GTK4=;; motif) AC_MSG_RESULT(Motif GUI support) SKIP_MOTIF=;; *) AC_MSG_RESULT([Sorry, $enable_gui GUI is not supported]) ;; @@ -2708,6 +2712,17 @@ if test "x$SKIP_GNOME" != "xYES" -a "$enable_gui_canon" != "gnome2"; then fi fi +if test "x$SKIP_GTK4" != "xYES" -a "$enable_gui_canon" != "gtk4"; then + AC_MSG_CHECKING(whether or not to look for GTK 4) + AC_ARG_ENABLE(gtk4-check, + [ --enable-gtk4-check If auto-select GUI, check for GTK 4 [default=yes]], + , enable_gtk4_check="yes") + AC_MSG_RESULT($enable_gtk4_check) + if test "x$enable_gtk4_check" = "xno"; then + SKIP_GTK4=YES + fi +fi + if test "x$SKIP_GTK3" != "xYES" -a "$enable_gui_canon" != "gtk3"; then AC_MSG_CHECKING(whether or not to look for GTK+ 3) AC_ARG_ENABLE(gtk3-check, @@ -2747,6 +2762,7 @@ AC_DEFUN(AM_PATH_GTK, AS_CASE([$min_gtk_version], [2.*], [gtk_pkg_name="gtk+-2.0"], [3.*], [gtk_pkg_name="gtk+-3.0"], + [4.*], [gtk_pkg_name="gtk4"], [AC_MSG_FAILURE([The configure script does not know which pkg-config name to use for GTK $min_gtk_version"])]) AC_MSG_CHECKING([for pkg-config $gtk_pkg_name]) @@ -2754,9 +2770,9 @@ AC_DEFUN(AM_PATH_GTK, [ AC_MSG_RESULT(found) AC_MSG_CHECKING([for GTK - version >= $min_gtk_version]) - dnl We should be using PKG_CHECK_MODULES() instead of this hack. - dnl But I guess the dependency on pkgconfig.m4 is not wanted or - dnl something like that. +dnl We should be using PKG_CHECK_MODULES() instead of this hack. +dnl But I guess the dependency on pkgconfig.m4 is not wanted or +dnl something like that. GTK_CPPFLAGS=`$PKG_CONFIG --cflags-only-I $gtk_pkg_name` GTK_CFLAGS=`$PKG_CONFIG --cflags-only-other $gtk_pkg_name` GTK_LIBDIR=`$PKG_CONFIG --libs-only-L $gtk_pkg_name` @@ -2935,7 +2951,7 @@ AC_DEFUN([GNOME_INIT],[ ]) dnl --------------------------------------------------------------------------- -dnl Check for GTK3. If it succeeds, skip the check for GTK2. +dnl Check for GTK3. If it succeeds, skip the check for GTK2/GTK4 dnl --------------------------------------------------------------------------- if test -z "$SKIP_GTK3"; then AC_MSG_CHECKING(--disable-gtktest argument) @@ -2954,6 +2970,7 @@ if test -z "$SKIP_GTK3"; then GUI_INC_LOC="$GTK_CPPFLAGS"]) if test -n "$GTK_CPPFLAGS"; then SKIP_GTK2=YES + SKIP_GTK4=YES SKIP_GNOME=YES SKIP_MOTIF=YES GUITYPE=GTK @@ -2963,6 +2980,47 @@ if test -z "$SKIP_GTK3"; then fi fi +dnl --------------------------------------------------------------------------- +dnl Check for GTK4. If it succeeds, skip the check for GTK3/GTK2. +dnl --------------------------------------------------------------------------- +if test -z "$SKIP_GTK4"; then + AC_MSG_CHECKING(--disable-gtktest argument) + AC_ARG_ENABLE(gtktest, [ --disable-gtktest Do not try to compile and run a test GTK program], + , enable_gtktest=yes) + if test "x$enable_gtktest" = "xyes" ; then + AC_MSG_RESULT(gtk test enabled) + else + AC_MSG_RESULT(gtk test disabled) + fi + + if test "x$PKG_CONFIG" != "xno"; then + AM_PATH_GTK(4.0.0, + [GUI_LIB_LOC="$GTK_LIBDIR" + GTK_LIBNAME="$GTK_LIBS" + GUI_INC_LOC="$GTK_CPPFLAGS"]) + if test -n "$GTK_CPPFLAGS"; then + SKIP_GTK3=YES + SKIP_GTK2=YES + SKIP_GNOME=YES + SKIP_MOTIF=YES + GUITYPE=GTK4 + AC_SUBST(GTK_LIBNAME) + AC_DEFINE(USE_GTK4) + dnl GTK4 does not use any X11 APIs directly. + dnl GTK4 itself links against X11 for its backend, so the + dnl dynamic linker resolves X11 symbols via GTK4's dependency. + X_LIBS= + X_PRE_LIBS= + X_EXTRA_LIBS= + X_LIB= + dnl automatically enable XIM for GTK4 + if test "$enable_xim" = "auto"; then + enable_xim="yes" + fi + fi + fi +fi + dnl --------------------------------------------------------------------------- dnl Check for GTK2. If it fails, then continue on for Motif as before... dnl --------------------------------------------------------------------------- diff --git a/src/evalfunc.c b/src/evalfunc.c index ed03592635..01c19298b9 100644 --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -7133,6 +7133,13 @@ f_has(typval_T *argvars, typval_T *rettv) 1 #else 0 +#endif + }, + {"gui_gtk4", +#if defined(FEAT_GUI_GTK) && defined(USE_GTK4) + 1 +#else + 0 #endif }, {"gui_gnome", diff --git a/src/feature.h b/src/feature.h index 3f3c67a176..115db703d2 100644 --- a/src/feature.h +++ b/src/feature.h @@ -296,6 +296,14 @@ # define FEAT_POSTSCRIPT #endif +/* + * +gtk_print Native GTK print dialog for :hardcopy (GTK4). + * Uses GtkPrintOperation + Pango/Cairo instead of PostScript. + */ +#if defined(FEAT_PRINTER) && defined(FEAT_GUI_GTK) && defined(USE_GTK4) +# define FEAT_GUI_GTK_PRINT +#endif + /* * +diff Displaying diffs in a nice way. * Can be enabled in autoconf already. @@ -505,7 +513,8 @@ /* * GUI dark theme variant */ -#if (defined(FEAT_GUI_GTK) && defined(USE_GTK3)) || defined(FEAT_GUI_MSWIN) +#if (defined(FEAT_GUI_GTK) && (defined(USE_GTK3) || defined(USE_GTK4))) \ + || defined(FEAT_GUI_MSWIN) # define FEAT_GUI_DARKTHEME #endif @@ -805,7 +814,7 @@ * +X11 Unix only. Include code for xterm title saving and X * clipboard. Only works if HAVE_X11 is also defined. */ -#if defined(FEAT_NORMAL) || defined(FEAT_GUI_MOTIF) +#if (defined(FEAT_NORMAL) || defined(FEAT_GUI_MOTIF)) && !defined(USE_GTK4) # define WANT_X11 #endif @@ -910,7 +919,8 @@ #if defined(FEAT_NORMAL) \ && (defined(UNIX) || defined(VMS)) \ - && defined(WANT_X11) && defined(HAVE_X11) + && defined(WANT_X11) && defined(HAVE_X11) \ + && !defined(USE_GTK4) # define FEAT_XCLIPBOARD # ifndef FEAT_CLIPBOARD # define FEAT_CLIPBOARD diff --git a/src/getchar.c b/src/getchar.c index 2e3079a1e4..b995698128 100644 --- a/src/getchar.c +++ b/src/getchar.c @@ -2018,7 +2018,7 @@ vgetc(void) continue; } #endif -#if defined(FEAT_GUI) && defined(FEAT_GUI_GTK) && defined(FEAT_MENU) +#if defined(FEAT_GUI) && defined(FEAT_GUI_GTK) && !defined(USE_GTK4) && defined(FEAT_MENU) // GTK: normally selects the menu, but it's passed until // here to allow mapping it. Intercept and invoke the GTK // behavior if it's not mapped. diff --git a/src/gui.c b/src/gui.c index bf277e2b93..1448c86bdb 100644 --- a/src/gui.c +++ b/src/gui.c @@ -1709,6 +1709,12 @@ gui_set_shellsize( #if defined(MSWIN) || defined(FEAT_GUI_GTK) // If not setting to a user specified size and maximized, calculate the // number of characters that fit in the maximized window. + // FIXME: gui_mch_newfont() is called here even when the font hasn't + // changed at all. For example, ":set guioptions=k" triggers this path + // via gui_init_which_components() -> gui_set_shellsize(FALSE, ...). + // The intent is to keep the window size and recalculate Rows/Columns, + // which has nothing to do with fonts. This should be a separate + // function with a more descriptive name. if (!mustset && (vim_strchr(p_go, GO_KEEPWINSIZE) != NULL || gui_mch_maximized())) { diff --git a/src/gui.h b/src/gui.h index 4639b3a129..d1e5d2d5c0 100644 --- a/src/gui.h +++ b/src/gui.h @@ -15,7 +15,17 @@ # ifdef VMS # include "gui_gtk_vms.h" # endif -# include +# ifdef USE_GTK4 +// Types used in proto files but not available without X11 headers +typedef void *Widget; +typedef void *XtAppContext; +typedef void Display; +typedef unsigned long Window; +typedef unsigned long Atom; +typedef GdkEvent GdkEventKey; // GTK4: GdkEventKey merged into GdkEvent +# else +# include +# endif # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wstrict-prototypes" # include @@ -344,7 +354,7 @@ typedef struct Gui #endif #ifdef FEAT_GUI_GTK -# ifndef USE_GTK3 +# if !defined(USE_GTK3) && !defined(USE_GTK4) int visibility; // Is shell partially/fully obscured? # endif GdkCursor *blank_pointer; // Blank pointer @@ -365,7 +375,7 @@ typedef struct Gui GtkWidget *menubar_h; // menubar handle GtkWidget *toolbar_h; // toolbar handle # endif -# ifdef USE_GTK3 +# if defined(USE_GTK3) || defined(USE_GTK4) GdkRGBA *fgcolor; // GDK-styled foreground color GdkRGBA *bgcolor; // GDK-styled background color GdkRGBA *spcolor; // GDK-styled special color @@ -374,7 +384,7 @@ typedef struct Gui GdkColor *bgcolor; // GDK-styled background color GdkColor *spcolor; // GDK-styled special color # endif -# ifdef USE_GTK3 +# if defined(USE_GTK3) || defined(USE_GTK4) cairo_surface_t *surface; // drawarea surface # else GdkGC *text_gc; // cached GC for normal text @@ -386,7 +396,9 @@ typedef struct Gui GtkWidget *tabline; // tab pages line handle # endif +# ifndef USE_GTK4 GtkAccelGroup *accel_group; +# endif GtkWidget *filedlg; // file selection dialog char_u *browse_fname; // file name from filedlg @@ -553,7 +565,7 @@ typedef enum * For Solaris Studio, that is not the case. An explicit type cast is needed * to suppress warnings on that particular conversion. */ -# if defined(__SUNPRO_C) && defined(USE_GTK3) +# if defined(__SUNPRO_C) && (defined(USE_GTK3) || defined(USE_GTK4)) # define FUNC2GENERIC(func) (void *)(func) # else # define FUNC2GENERIC(func) G_CALLBACK(func) diff --git a/src/gui_beval.c b/src/gui_beval.c index 1d41089fec..c928d25685 100644 --- a/src/gui_beval.c +++ b/src/gui_beval.c @@ -16,7 +16,9 @@ #if !defined(FEAT_GUI_MSWIN) # ifdef FEAT_GUI_GTK -# if GTK_CHECK_VERSION(3,0,0) +# ifdef USE_GTK4 +# include +# elif GTK_CHECK_VERSION(3,0,0) # include # else # include @@ -390,7 +392,11 @@ pointer_event(BalloonEval *beval, int x, int y, unsigned state) beval->x = x; beval->y = y; +# ifdef USE_GTK4 + if (state & (int)GDK_ALT_MASK) +# else if (state & (int)GDK_MOD1_MASK) +# endif { /* * Alt is pressed -- enter super-evaluate-mode, @@ -416,14 +422,24 @@ key_event(BalloonEval *beval, unsigned keyval, int is_keypress) { switch (keyval) { +# ifdef USE_GTK4 + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: +# else case GDK_Shift_L: case GDK_Shift_R: +# endif beval->showState = ShS_UPDATE_PENDING; (*beval->msgCB)(beval, (is_keypress) ? (int)GDK_SHIFT_MASK : 0); break; +# ifdef USE_GTK4 + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: +# else case GDK_Control_L: case GDK_Control_R: +# endif beval->showState = ShS_UPDATE_PENDING; (*beval->msgCB)(beval, (is_keypress) ? (int)GDK_CONTROL_MASK : 0); diff --git a/src/gui_gtk4.c b/src/gui_gtk4.c new file mode 100644 index 0000000000..24ad6ddce2 --- /dev/null +++ b/src/gui_gtk4.c @@ -0,0 +1,4638 @@ +/* vi:set ts=8 sts=4 sw=4 noet: + * + * VIM - Vi IMproved by Bram Moolenaar + * + * Do ":help uganda" in Vim to read copying and usage conditions. + * Do ":help credits" in Vim to see a list of people who contributed. + * See README.txt for an overview of the Vim source code. + * + * GTK4 GUI implementation: main window, events, drawing, menus, + * scrollbars, dialogs, and toolbar. This is a clean implementation for + * GTK4, separate from gui_gtk_x11.c which handles GTK2/GTK3. + * + * GTK4 differences from GTK3: + * - No GdkWindow (use GdkSurface for top-level only) + * - No GtkContainer (use gtk_widget_set_parent/gtk_box_append) + * - Events via GtkEventController, not signal+mask + * - Drawing via GtkSnapshot or gtk_drawing_area_set_draw_func + * - No gtk_dialog_run (async dialogs) + * - No GdkAtom (string-based content types) + * - No GtkSocket/GtkPlug + * - gtk_window_new() takes no arguments + */ + +#include "vim.h" + +#ifdef FEAT_GUI_GTK + +#include +#include +#include +#include "gui_gtk4_f.h" + +/* + * Geometry string parser, replacing XParseGeometry to remove X11 dependency. + * Format: [WIDTHxHEIGHT][{+-}XOFF{+-}YOFF] + */ +#define NoValue 0x0000 +#define XValue 0x0001 +#define YValue 0x0002 +#define WidthValue 0x0004 +#define HeightValue 0x0008 +#define XNegative 0x0010 +#define YNegative 0x0020 + + static int +vim_parse_geometry(const char *str, int *x, int *y, + unsigned int *width, unsigned int *height) +{ + int mask = NoValue; + char *end; + long val; + + if (str == NULL || *str == NUL) + return mask; + + // Parse width + if (*str != '+' && *str != '-') + { + val = strtol(str, &end, 10); + if (end != str) + { + *width = (unsigned int)val; + mask |= WidthValue; + str = end; + } + } + + // Parse 'x' or 'X' separator and height + if (*str == 'x' || *str == 'X') + { + str++; + val = strtol(str, &end, 10); + if (end != str) + { + *height = (unsigned int)val; + mask |= HeightValue; + str = end; + } + } + + // Parse x offset + if (*str == '+' || *str == '-') + { + int negative = (*str == '-'); + str++; + val = strtol(str, &end, 10); + if (end != str) + { + *x = negative ? -(int)val : (int)val; + mask |= XValue; + if (negative) + mask |= XNegative; + str = end; + } + } + + // Parse y offset + if (*str == '+' || *str == '-') + { + int negative = (*str == '-'); + str++; + val = strtol(str, &end, 10); + if (end != str) + { + *y = negative ? -(int)val : (int)val; + mask |= YValue; + if (negative) + mask |= YNegative; + } + } + + return mask; +} + +#ifdef FEAT_SOCKETSERVER +# include + +// Used to track the source for the listening socket +static guint socket_server_source_id = 0; +#endif + +#if defined(FEAT_MOUSESHAPE) +// Last set mouse pointer shape +static int last_shape = 0; +#endif + +#define DEFAULT_FONT "Monospace 10" + +// Menu action group for GMenu-based menus +static GSimpleActionGroup *menu_action_group = NULL; + +// Cursor blinking state +static enum { + BLINK_NONE, + BLINK_OFF, + BLINK_ON +} blink_state = BLINK_NONE; + +// GTK4 main loop compatibility +static int gtk4_main_loop_level = 0; +static int gtk4_main_loop_quit = FALSE; + +#ifdef USE_GRESOURCE +# include "auto/gui_gtk_gresources.h" +#endif + +typedef gboolean timeout_cb_type; + +/* + * Table of special key mappings. + */ +static struct special_key +{ + guint key_sym; + char_u code0; + char_u code1; +} +const special_keys[] = +{ + {GDK_KEY_Up, 'k', 'u'}, + {GDK_KEY_Down, 'k', 'd'}, + {GDK_KEY_Left, 'k', 'l'}, + {GDK_KEY_Right, 'k', 'r'}, + {GDK_KEY_F1, 'k', '1'}, + {GDK_KEY_F2, 'k', '2'}, + {GDK_KEY_F3, 'k', '3'}, + {GDK_KEY_F4, 'k', '4'}, + {GDK_KEY_F5, 'k', '5'}, + {GDK_KEY_F6, 'k', '6'}, + {GDK_KEY_F7, 'k', '7'}, + {GDK_KEY_F8, 'k', '8'}, + {GDK_KEY_F9, 'k', '9'}, + {GDK_KEY_F10, 'k', ';'}, + {GDK_KEY_F11, 'F', '1'}, + {GDK_KEY_F12, 'F', '2'}, + {GDK_KEY_Help, '%', '1'}, + {GDK_KEY_Undo, '&', '8'}, + {GDK_KEY_BackSpace, 'k', 'b'}, + {GDK_KEY_Insert, 'k', 'I'}, + {GDK_KEY_Delete, 'k', 'D'}, + {GDK_KEY_Home, 'k', 'h'}, + {GDK_KEY_End, '@', '7'}, + {GDK_KEY_Prior, 'k', 'P'}, + {GDK_KEY_Next, 'k', 'N'}, + {GDK_KEY_Print, '%', '9'}, + {GDK_KEY_KP_Left, 'k', 'l'}, + {GDK_KEY_KP_Right, 'k', 'r'}, + {GDK_KEY_KP_Up, 'k', 'u'}, + {GDK_KEY_KP_Down, 'k', 'd'}, + {GDK_KEY_KP_Insert, KS_EXTRA, (char_u)KE_KINS}, + {GDK_KEY_KP_Delete, KS_EXTRA, (char_u)KE_KDEL}, + {GDK_KEY_KP_Home, 'K', '1'}, + {GDK_KEY_KP_End, 'K', '4'}, + {GDK_KEY_KP_Prior, 'K', '3'}, + {GDK_KEY_KP_Next, 'K', '5'}, + {GDK_KEY_KP_Add, 'K', '6'}, + {GDK_KEY_KP_Subtract, 'K', '7'}, + {GDK_KEY_KP_Divide, 'K', '8'}, + {GDK_KEY_KP_Multiply, 'K', '9'}, + {GDK_KEY_KP_Enter, 'K', 'A'}, + {GDK_KEY_KP_Decimal, 'K', 'B'}, + {GDK_KEY_KP_0, 'K', 'C'}, + {GDK_KEY_KP_1, 'K', 'D'}, + {GDK_KEY_KP_2, 'K', 'E'}, + {GDK_KEY_KP_3, 'K', 'F'}, + {GDK_KEY_KP_4, 'K', 'G'}, + {GDK_KEY_KP_5, 'K', 'H'}, + {GDK_KEY_KP_6, 'K', 'I'}, + {GDK_KEY_KP_7, 'K', 'J'}, + {GDK_KEY_KP_8, 'K', 'K'}, + {GDK_KEY_KP_9, 'K', 'L'}, + {0, 0, 0} +}; + + static int +keyval_to_string(unsigned int keyval, char_u *string) +{ + int len; + guint32 uc; + + uc = gdk_keyval_to_unicode(keyval); + if (uc != 0) + { + len = utf_char2bytes((int)uc, string); + } + else + { + len = 1; + switch (keyval) + { + case GDK_KEY_Tab: case GDK_KEY_KP_Tab: case GDK_KEY_ISO_Left_Tab: + string[0] = TAB; + break; + case GDK_KEY_Linefeed: + string[0] = NL; + break; + case GDK_KEY_Return: case GDK_KEY_ISO_Enter: case GDK_KEY_3270_Enter: + string[0] = CAR; + break; + case GDK_KEY_Escape: + string[0] = ESC; + break; + default: + len = 0; + break; + } + } + string[len] = NUL; + return len; +} + + static int +modifiers_gdk2vim(guint state) +{ + int modifiers = 0; + + if (state & GDK_SHIFT_MASK) + modifiers |= MOD_MASK_SHIFT; + if (state & GDK_CONTROL_MASK) + modifiers |= MOD_MASK_CTRL; + if (state & GDK_ALT_MASK) + modifiers |= MOD_MASK_ALT; + if (state & GDK_META_MASK) + modifiers |= MOD_MASK_META; + if (state & GDK_SUPER_MASK) + modifiers |= MOD_MASK_CMD; + + return modifiers; +} + +static GtkWidget *vbox; // the main vertical box + +// Forward declarations for event callbacks +static void draw_event(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data); +static gboolean key_press_event(GtkEventControllerKey *controller, guint keyval, guint keycode, GdkModifierType state, gpointer data); +static void key_release_event(GtkEventControllerKey *controller, guint keyval, guint keycode, GdkModifierType state, gpointer data); +static void button_press_event(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data); +static void button_release_event(GtkGestureClick *gesture, int n_press, double x, double y, gpointer data); +static void motion_notify_event(GtkEventControllerMotion *controller, double x, double y, gpointer data); +static void enter_notify_event(GtkEventControllerMotion *controller, double x, double y, gpointer data); +static void leave_notify_event(GtkEventControllerMotion *controller, gpointer data); +static gboolean scroll_event(GtkEventControllerScroll *controller, double dx, double dy, gpointer data); +static void focus_in_event(GtkEventControllerFocus *controller, gpointer data); +static void focus_out_event(GtkEventControllerFocus *controller, gpointer data); +#ifdef FEAT_DND +static gboolean drop_cb(GtkDropTarget *target, const GValue *value, double x, double y, gpointer data); +#endif +static void mainwin_destroy_cb(GObject *object, gpointer data); +static gboolean delete_event_cb(GtkWindow *window, gpointer data); +static void drawarea_realize_cb(GtkWidget *widget, gpointer data); +static void drawarea_unrealize_cb(GtkWidget *widget, gpointer data); +static void drawarea_resize_cb(GtkDrawingArea *area, int width, int height, gpointer data); + +/* + * Parse the GUI related command-line arguments. Any arguments used are + * deleted from argv, and *argc is decremented accordingly. This is called + * when vim is started, whether or not the GUI has been started. + */ + void +gui_mch_prepare(int *argc, char **argv) +{ + // Don't call gtk_init() here. It will be called in + // gui_mch_init_check() after the fork. Calling it before fork + // breaks the display connection in the child process, causing gvim + // to fail to start without --nofork. +} + +/* + * Free all GUI related resources. + */ + void +gui_mch_free_all(void) +{ +} + + static guint +timeout_add(int time, timeout_cb_type (*callback)(gpointer), int *flagp) +{ + return g_timeout_add((guint)time, (GSourceFunc)callback, flagp); +} + + static void +timeout_remove(guint timer) +{ + g_source_remove(timer); +} + +static long_u blink_waittime = 700; +static long_u blink_ontime = 400; +static long_u blink_offtime = 250; +static guint blink_timer = 0; + + static timeout_cb_type +blink_cb(gpointer data UNUSED) +{ + if (blink_state == BLINK_ON) + { + gui_undraw_cursor(); + blink_state = BLINK_OFF; + blink_timer = timeout_add(blink_offtime, blink_cb, NULL); + } + else + { + gui_update_cursor(TRUE, FALSE); + blink_state = BLINK_ON; + blink_timer = timeout_add(blink_ontime, blink_cb, NULL); + } + return FALSE; +} + + int +gui_mch_is_blinking(void) +{ + return blink_state != BLINK_NONE; +} + + int +gui_mch_is_blink_off(void) +{ + return blink_state == BLINK_OFF; +} + + void +gui_mch_set_blinking(long waittime, long on, long off) +{ + blink_waittime = waittime; + blink_ontime = on; + blink_offtime = off; +} + + void +gui_mch_stop_blink(int may_call_gui_update_cursor) +{ + if (blink_timer) + { + timeout_remove(blink_timer); + blink_timer = 0; + } + if (blink_state == BLINK_OFF && may_call_gui_update_cursor) + gui_update_cursor(TRUE, FALSE); + blink_state = BLINK_NONE; +} + + void +gui_mch_start_blink(void) +{ + if (blink_timer) + { + timeout_remove(blink_timer); + blink_timer = 0; + } + if (blink_waittime && blink_ontime && blink_offtime && gui.in_focus) + { + blink_timer = timeout_add(blink_waittime, blink_cb, NULL); + blink_state = BLINK_ON; + gui_update_cursor(TRUE, FALSE); + } +} + + int +gui_mch_early_init_check(int give_message UNUSED) +{ + return OK; +} + + int +gui_mch_init_check(void) +{ + // This defaults to argv[0], but we want it to match the name of the + // shipped gvim.desktop so that Vim's windows can be associated with this + // file. Also sets WM_CLASS on X11. + g_set_prgname("gvim"); + + // Suppress noisy EGL warnings when GL is not available. Only set + // this when actually starting the GUI, so non-GUI invocations are + // not affected. + if (g_getenv("EGL_LOG_LEVEL") == NULL) + setenv("EGL_LOG_LEVEL", "fatal", 0); + + // Call gtk_init() here after fork(). Calling it before fork() breaks + // the display connection in the child process. + gtk_init(); + return OK; +} + +/* + * Initialise the GUI. Create all the windows, set up all the callbacks etc. + * Returns OK for success, FAIL when the GUI can't be started. + */ + int +gui_mch_init(void) +{ + // Allocate GdkRGBA color structs. + gui.fgcolor = g_new(GdkRGBA, 1); + gui.bgcolor = g_new(GdkRGBA, 1); + gui.spcolor = g_new(GdkRGBA, 1); + + gui.def_norm_pixel = 0x00000000; // black + gui.def_back_pixel = 0x00ffffff; // white + gui.norm_pixel = gui.def_norm_pixel; + gui.back_pixel = gui.def_back_pixel; + + gui.scrollbar_width = SB_DEFAULT_WIDTH; + gui.scrollbar_height = SB_DEFAULT_WIDTH; + + // Create the main window. + gui.mainwin = gtk_window_new(); + gtk_widget_set_name(gui.mainwin, "vim-main-window"); + + // Create the PangoContext used for drawing all text. + gui.text_context = gtk_widget_create_pango_context(gui.mainwin); + pango_context_set_base_dir(gui.text_context, PANGO_DIRECTION_LTR); + + g_signal_connect(G_OBJECT(gui.mainwin), "close-request", + G_CALLBACK(delete_event_cb), NULL); + + // A vertical box holds the menubar, toolbar and main text window. + vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_box_set_homogeneous(GTK_BOX(vbox), FALSE); + gtk_window_set_child(GTK_WINDOW(gui.mainwin), vbox); + +#ifdef FEAT_MENU + { + GMenu *gmenu = g_menu_new(); + gui.menubar = gtk_popover_menu_bar_new_from_model( + G_MENU_MODEL(gmenu)); + g_object_set_data_full(G_OBJECT(gui.menubar), "vim-gmenu", + gmenu, g_object_unref); + gtk_widget_set_name(gui.menubar, "vim-menubar"); + gtk_widget_set_visible(gui.menubar, FALSE); + gtk_box_append(GTK_BOX(vbox), gui.menubar); + } +#endif + +#ifdef FEAT_TOOLBAR + gui.toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_set_name(gui.toolbar, "vim-toolbar"); + gtk_widget_set_visible(gui.toolbar, FALSE); + gtk_box_append(GTK_BOX(vbox), gui.toolbar); +#endif + +#ifdef FEAT_GUI_TABLINE + gui.tabline = gtk_notebook_new(); + gtk_notebook_set_show_border(GTK_NOTEBOOK(gui.tabline), FALSE); + gtk_notebook_set_show_tabs(GTK_NOTEBOOK(gui.tabline), FALSE); + gtk_notebook_set_scrollable(GTK_NOTEBOOK(gui.tabline), TRUE); + gtk_widget_set_visible(gui.tabline, FALSE); + gtk_box_append(GTK_BOX(vbox), gui.tabline); +#endif + + // The form widget manages absolute positioning of scrollbars. + gui.formwin = gui_gtk_form_new(); + gtk_widget_set_name(gui.formwin, "vim-gtk-form"); + // formwin is overlaid on top of drawarea for scrollbar positioning. + // Disable input targeting so mouse events pass through to drawarea. + gtk_widget_set_can_target(gui.formwin, FALSE); + + // The drawing area for the editor content. + // Placed in an overlay so it fills the formwin, with scrollbars on top. + gui.drawarea = gtk_drawing_area_new(); + gui.surface = NULL; + gtk_widget_set_focusable(gui.drawarea, TRUE); + gtk_widget_set_vexpand(gui.drawarea, TRUE); + gtk_widget_set_hexpand(gui.drawarea, TRUE); + + { + // Use GtkOverlay: drawarea as the main child, formwin as overlay + GtkWidget *overlay = gtk_overlay_new(); + gtk_overlay_set_child(GTK_OVERLAY(overlay), gui.drawarea); + gtk_overlay_add_overlay(GTK_OVERLAY(overlay), gui.formwin); + gtk_widget_set_vexpand(overlay, TRUE); + gtk_widget_set_hexpand(overlay, TRUE); + gtk_box_append(GTK_BOX(vbox), overlay); + } + + // Set up drawing. + gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(gui.drawarea), + (GtkDrawingAreaDrawFunc)draw_event, NULL, NULL); + + g_signal_connect(G_OBJECT(gui.drawarea), "realize", + G_CALLBACK(drawarea_realize_cb), NULL); + g_signal_connect(G_OBJECT(gui.drawarea), "unrealize", + G_CALLBACK(drawarea_unrealize_cb), NULL); + g_signal_connect(G_OBJECT(gui.drawarea), "resize", + G_CALLBACK(drawarea_resize_cb), NULL); + + // Set up event controllers. + { + GtkEventController *key_ctrl = gtk_event_controller_key_new(); + g_signal_connect(key_ctrl, "key-pressed", + G_CALLBACK(key_press_event), NULL); + g_signal_connect(key_ctrl, "key-released", + G_CALLBACK(key_release_event), NULL); + gtk_widget_add_controller(gui.mainwin, key_ctrl); + } + + { + GtkGesture *click = gtk_gesture_click_new(); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click), 0); + g_signal_connect(click, "pressed", + G_CALLBACK(button_press_event), NULL); + g_signal_connect(click, "released", + G_CALLBACK(button_release_event), NULL); + gtk_widget_add_controller(gui.drawarea, GTK_EVENT_CONTROLLER(click)); + } + + { + GtkEventController *motion = gtk_event_controller_motion_new(); + g_signal_connect(motion, "motion", + G_CALLBACK(motion_notify_event), NULL); + g_signal_connect(motion, "enter", + G_CALLBACK(enter_notify_event), NULL); + g_signal_connect(motion, "leave", + G_CALLBACK(leave_notify_event), NULL); + gtk_widget_add_controller(gui.drawarea, motion); + } + + { + GtkEventController *scroll = gtk_event_controller_scroll_new( + GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES); + g_signal_connect(scroll, "scroll", + G_CALLBACK(scroll_event), NULL); + gtk_widget_add_controller(gui.drawarea, scroll); + } + + { + GtkEventController *focus = gtk_event_controller_focus_new(); + g_signal_connect(focus, "enter", + G_CALLBACK(focus_in_event), NULL); + g_signal_connect(focus, "leave", + G_CALLBACK(focus_out_event), NULL); + gtk_widget_add_controller(gui.drawarea, focus); + } + +#ifdef FEAT_DND + // Set up drag-and-drop target for files and text. + { + GtkDropTarget *drop = gtk_drop_target_new(G_TYPE_INVALID, GDK_ACTION_COPY); + GType types[] = { GDK_TYPE_FILE_LIST, G_TYPE_STRING }; + gtk_drop_target_set_gtypes(drop, types, 2); + g_signal_connect(drop, "drop", + G_CALLBACK(drop_cb), NULL); + gtk_widget_add_controller(gui.drawarea, GTK_EVENT_CONTROLLER(drop)); + } +#endif + + gui.border_offset = gui.border_width; + + // Create a blank (invisible) cursor for hiding the mouse pointer. + gui.blank_pointer = gdk_cursor_new_from_name("none", NULL); + + return OK; +} + +/* + * Called when the foreground or background color has been changed. + */ + static void +surface_fill_bg(void) +{ + if (gui.surface != NULL) + { + cairo_t *cr = cairo_create(gui.surface); + cairo_set_source_rgba(cr, + gui.bgcolor->red, gui.bgcolor->green, + gui.bgcolor->blue, gui.bgcolor->alpha); + cairo_paint(cr); + cairo_destroy(cr); + } +} + + void +gui_mch_new_colors(void) +{ + surface_fill_bg(); + if (gui.drawarea != NULL && gtk_widget_get_realized(gui.drawarea)) + gtk_widget_queue_draw(gui.drawarea); +} + +/* + * Open the GUI window which was created by a call to gui_mch_init(). + */ + int +gui_mch_open(void) +{ + guicolor_T fg_pixel = INVALCOLOR; + guicolor_T bg_pixel = INVALCOLOR; + guint pixel_width; + guint pixel_height; + + if (gui.geom != NULL) + { + int mask; + unsigned int w, h; + int x = 0; + int y = 0; + + mask = vim_parse_geometry((char *)gui.geom, &x, &y, &w, &h); + + if (mask & WidthValue) + Columns = w; + if (mask & HeightValue) + { + if (p_window > (long)h - 1 || !option_was_set((char_u *)"window")) + p_window = h - 1; + Rows = h; + } + limit_screen_size(); + + VIM_CLEAR(gui.geom); + } + + // Use 80x24 as the default GUI size, unless geometry was specified. + if (Columns > 80 && gui.geom == NULL) + Columns = 80; + if (Rows > 24 && gui.geom == NULL) + Rows = 24; + pixel_width = (guint)(gui_get_base_width() + Columns * gui.char_width); + pixel_height = (guint)(gui_get_base_height() + Rows * gui.char_height); + gtk_window_set_default_size(GTK_WINDOW(gui.mainwin), + pixel_width, pixel_height); + + if (foreground_argument != NULL) + fg_pixel = gui_get_color((char_u *)foreground_argument); + if (fg_pixel == INVALCOLOR) + fg_pixel = gui_get_color((char_u *)"Black"); + + if (background_argument != NULL) + bg_pixel = gui_get_color((char_u *)background_argument); + if (bg_pixel == INVALCOLOR) + bg_pixel = gui_get_color((char_u *)"White"); + + if (found_reverse_arg) + { + gui.def_norm_pixel = bg_pixel; + gui.def_back_pixel = fg_pixel; + } + else + { + gui.def_norm_pixel = fg_pixel; + gui.def_back_pixel = bg_pixel; + } + + set_normal_colors(); + gui_check_colors(); + highlight_gui_started(); + + g_signal_connect(G_OBJECT(gui.mainwin), "destroy", + G_CALLBACK(mainwin_destroy_cb), NULL); + // Resize is handled by GtkForm's size_allocate callback. + + gtk_widget_set_visible(gui.mainwin, TRUE); + + // Make sure the drawing area gets keyboard focus. + gtk_widget_grab_focus(gui.drawarea); + gui_focus_change(TRUE); + + return OK; +} + + void +gui_mch_exit(int rc UNUSED) +{ + if (gui.mainwin != NULL) + gtk_window_destroy(GTK_WINDOW(gui.mainwin)); +} + + int +gui_mch_get_winpos(int *x, int *y) +{ + // GTK4 does not provide a window position API. + *x = 0; + *y = 0; + return FAIL; +} + + void +gui_mch_set_winpos(int x UNUSED, int y UNUSED) +{ + // GTK4/Wayland: window positioning not available +} + + int +gui_mch_maximized(void) +{ + return gtk_window_is_maximized(GTK_WINDOW(gui.mainwin)); +} + + void +gui_mch_unmaximize(void) +{ + if (gui.mainwin != NULL) + gtk_window_unmaximize(GTK_WINDOW(gui.mainwin)); +} + +/* + * Called when the font changed while the window is maximized or GO_KEEPWINSIZE + * is set. Recalculate Rows and Columns based on the current window size. + * + * NOTE: gui_set_shellsize() calls this when GO_KEEPWINSIZE ('k') is in + * 'guioptions', even when the font hasn't actually changed (e.g. just setting + * "guioptions=k" triggers it via gui_init_which_components()). This is + * arguably a design problem in the common code, but we must not call + * gui_set_shellsize() back from here or it will cause infinite recursion and + * crash. Use gui_resize_shell() to recalculate Rows/Columns from the current + * window size instead. + */ + void +gui_mch_newfont(void) +{ + int w, h; + + w = gtk_widget_get_width(gui.formwin); + h = gtk_widget_get_height(gui.formwin); + w -= get_menu_tool_width(); + h -= get_menu_tool_height(); + gui_resize_shell(w, h); +} + + void +gui_mch_settitle(char_u *title, char_u *icon UNUSED) +{ + if (title != NULL && gui.mainwin != NULL) + gtk_window_set_title(GTK_WINDOW(gui.mainwin), (const char *)title); +} + +static int in_set_shellsize = FALSE; + + void +gui_mch_set_shellsize(int width, int height, + int min_width UNUSED, int min_height UNUSED, + int base_width UNUSED, int base_height UNUSED, + int direction UNUSED) +{ + // Only set window size if it hasn't been shown yet (initial sizing). + // After that, the window size is controlled by the user/WM and + // Vim adapts to it via form_size_allocate -> gui_resize_shell. + if (!gtk_widget_get_realized(gui.mainwin)) + { + width += get_menu_tool_width(); + height += get_menu_tool_height(); + gtk_window_set_default_size(GTK_WINDOW(gui.mainwin), width, height); + } +} + + void +gui_mch_get_screen_dimensions(int *screen_w, int *screen_h) +{ + GdkDisplay *display = gtk_widget_get_display(gui.mainwin); + GdkSurface *surface = gtk_native_get_surface(GTK_NATIVE(gui.mainwin)); + + if (surface != NULL) + { + GdkMonitor *monitor = gdk_display_get_monitor_at_surface(display, + surface); + if (monitor != NULL) + { + GdkRectangle geom; + gdk_monitor_get_geometry(monitor, &geom); + *screen_w = geom.width; + *screen_h = geom.height; + return; + } + } + + *screen_w = 800; + *screen_h = 600; +} + +#ifdef FEAT_MENU + void +gui_mch_enable_menu(int showit) +{ + if (gui.menubar != NULL) + gtk_widget_set_visible(gui.menubar, showit); +} +#endif + +#ifdef FEAT_TOOLBAR + void +gui_mch_show_toolbar(int showit) +{ + if (gui.toolbar != NULL) + gtk_widget_set_visible(gui.toolbar, showit); +} +#endif + + void +gui_mch_set_dark_theme(int dark) +{ + // GTK4: use GtkSettings + GtkSettings *settings = gtk_settings_get_default(); + if (settings != NULL) + g_object_set(settings, "gtk-application-prefer-dark-theme", + (gboolean)dark, NULL); +} + +/* + * ============================================================ + * Font handling + * ============================================================ + */ + + int +gui_mch_adjust_charheight(void) +{ + PangoFontMetrics *metrics; + int ascent; + int descent; + + metrics = pango_context_get_metrics(gui.text_context, gui.norm_font, + pango_context_get_language(gui.text_context)); + ascent = pango_font_metrics_get_ascent(metrics); + descent = pango_font_metrics_get_descent(metrics); + pango_font_metrics_unref(metrics); + + gui.char_height = (ascent + descent + (PANGO_SCALE * 15) / 16) + / PANGO_SCALE + p_linespace; + gui.char_ascent = PANGO_PIXELS(ascent + p_linespace * PANGO_SCALE / 2); + gui.char_ascent = MAX(gui.char_ascent, 0); + gui.char_height = MAX(gui.char_height, gui.char_ascent + 1); + + return OK; +} + +typedef struct { + PangoFontDescription *result; + gboolean done; +} FontDialogData; + + static void +font_dialog_finish_cb(GObject *source, GAsyncResult *res, gpointer data) +{ + FontDialogData *fdd = (FontDialogData *)data; + fdd->result = gtk_font_dialog_choose_font_finish( + GTK_FONT_DIALOG(source), res, NULL); + fdd->done = TRUE; +} + + static gboolean +font_filter(gpointer item, gpointer data UNUSED) +{ + if (PANGO_IS_FONT_FAMILY(item)) + return pango_font_family_is_monospace(PANGO_FONT_FAMILY(item)); + if (PANGO_IS_FONT_FACE(item)) + { + PangoFontFamily *family = pango_font_face_get_family( + PANGO_FONT_FACE(item)); + if (family != NULL) + return pango_font_family_is_monospace(family); + } + return TRUE; +} + + char_u * +gui_mch_font_dialog(char_u *oldval) +{ + GtkFontDialog *dlg; + PangoFontDescription *initial = NULL; + char_u *fontname = NULL; + FontDialogData fdd; + + dlg = gtk_font_dialog_new(); + gtk_font_dialog_set_modal(dlg, TRUE); + gtk_font_dialog_set_filter(dlg, + GTK_FILTER(gtk_custom_filter_new( + (GtkCustomFilterFunc)font_filter, NULL, NULL))); + + if (oldval != NULL && oldval[0] != NUL) + { + char_u *oldname; + + if (output_conv.vc_type != CONV_NONE) + oldname = string_convert(&output_conv, oldval, NULL); + else + oldname = oldval; + + if (STRLEN(oldname) > 0 && !vim_isdigit(oldname[STRLEN(oldname) - 1])) + { + char_u *p = vim_strnsave(oldname, STRLEN(oldname) + 3); + if (p != NULL) + { + STRCPY(p + STRLEN(p), " 10"); + if (oldname != oldval) + vim_free(oldname); + oldname = p; + } + } + + initial = pango_font_description_from_string((const char *)oldname); + if (oldname != oldval) + vim_free(oldname); + } + else + initial = pango_font_description_from_string(DEFAULT_FONT); + + fdd.result = NULL; + fdd.done = FALSE; + + gtk_font_dialog_choose_font(dlg, GTK_WINDOW(gui.mainwin), + initial, NULL, font_dialog_finish_cb, &fdd); + + while (!fdd.done) + g_main_context_iteration(NULL, TRUE); + + if (fdd.result != NULL) + { + char *name = pango_font_description_to_string(fdd.result); + if (name != NULL) + { + char_u *p; + + p = vim_strsave_escaped((char_u *)name, (char_u *)","); + g_free(name); + if (p != NULL && input_conv.vc_type != CONV_NONE) + { + fontname = string_convert(&input_conv, p, NULL); + vim_free(p); + } + else + fontname = p; + } + pango_font_description_free(fdd.result); + } + + if (initial != NULL) + pango_font_description_free(initial); + g_object_unref(dlg); + + return fontname; +} + +/* + * Build a table of glyphs for ASCII characters 32..126. + * This avoids the overhead of itemize+shape for the common case. + */ + static void +ascii_glyph_table_init(void) +{ + char_u ascii_chars[2 * 128]; + PangoAttrList *attr_list; + GList *item_list; + int i; + + if (gui.ascii_glyphs != NULL) + pango_glyph_string_free(gui.ascii_glyphs); + if (gui.ascii_font != NULL) + g_object_unref(gui.ascii_font); + + gui.ascii_glyphs = NULL; + gui.ascii_font = NULL; + + for (i = 0; i < 128; ++i) + { + if (i >= 32 && i < 127) + ascii_chars[2 * i] = i; + else + ascii_chars[2 * i] = '?'; + ascii_chars[2 * i + 1] = ' '; + } + + attr_list = pango_attr_list_new(); + item_list = pango_itemize(gui.text_context, (const char *)ascii_chars, + 0, sizeof(ascii_chars), attr_list, NULL); + + if (item_list != NULL && item_list->next == NULL) + { + PangoItem *item; + int width; + + item = (PangoItem *)item_list->data; + width = gui.char_width * PANGO_SCALE; + + gui.ascii_font = item->analysis.font; + g_object_ref(gui.ascii_font); + + gui.ascii_glyphs = pango_glyph_string_new(); + + pango_shape((const char *)ascii_chars, sizeof(ascii_chars), + &item->analysis, gui.ascii_glyphs); + + if (gui.ascii_glyphs->num_glyphs == (int)sizeof(ascii_chars)) + { + for (i = 0; i < gui.ascii_glyphs->num_glyphs; ++i) + { + PangoGlyphGeometry *geom; + + geom = &gui.ascii_glyphs->glyphs[i].geometry; + geom->x_offset += MAX(0, width - geom->width) / 2; + geom->width = width; + } + } + else + { + pango_glyph_string_free(gui.ascii_glyphs); + gui.ascii_glyphs = NULL; + g_object_unref(gui.ascii_font); + gui.ascii_font = NULL; + } + } + + g_list_foreach(item_list, (GFunc)(void *)&pango_item_free, NULL); + g_list_free(item_list); + pango_attr_list_unref(attr_list); +} + + static void +get_styled_font_variants(void) +{ + PangoFontDescription *bold_font_desc; + PangoFont *plain_font; + PangoFont *bold_font; + + gui.font_can_bold = FALSE; + + plain_font = pango_context_load_font(gui.text_context, gui.norm_font); + if (plain_font == NULL) + return; + + bold_font_desc = pango_font_description_copy_static(gui.norm_font); + pango_font_description_set_weight(bold_font_desc, PANGO_WEIGHT_BOLD); + + bold_font = pango_context_load_font(gui.text_context, bold_font_desc); + if (bold_font != NULL) + { + gui.font_can_bold = (bold_font != plain_font); + g_object_unref(bold_font); + } + + pango_font_description_free(bold_font_desc); + g_object_unref(plain_font); +} + + int +gui_mch_init_font(char_u *font_name, int fontset UNUSED) +{ + PangoFontDescription *font_desc; + PangoLayout *layout; + int width; + + if (font_name == NULL) + font_name = (char_u *)DEFAULT_FONT; + + font_desc = gui_mch_get_font(font_name, FALSE); + if (font_desc == NULL) + return FAIL; + + gui_mch_free_font(gui.norm_font); + gui.norm_font = font_desc; + + pango_context_set_font_description(gui.text_context, font_desc); + + layout = pango_layout_new(gui.text_context); + pango_layout_set_text(layout, "MW", 2); + pango_layout_get_size(layout, &width, NULL); + g_object_unref(layout); + + gui.char_width = (width / 2 + PANGO_SCALE - 1) / PANGO_SCALE; + if (gui.char_width <= 0) + gui.char_width = 8; + + gui_mch_adjust_charheight(); + + hl_set_font_name(font_name); + + get_styled_font_variants(); + ascii_glyph_table_init(); + + return OK; +} + + GuiFont +gui_mch_get_font(char_u *name, int report_error) +{ + PangoFontDescription *font; + + if (name == NULL) + return NULL; + + font = pango_font_description_from_string((const char *)name); + if (font == NULL) + { + if (report_error) + semsg(_(e_unknown_font_str), name); + return NULL; + } + + // Ensure a size is set + if (pango_font_description_get_size(font) <= 0) + pango_font_description_set_size(font, 10 * PANGO_SCALE); + + return font; +} + + char_u * +gui_mch_get_fontname(GuiFont font, char_u *name UNUSED) +{ + if (font != NOFONT) + { + char *desc = pango_font_description_to_string(font); + char_u *ret = vim_strsave((char_u *)desc); + g_free(desc); + return ret; + } + return NULL; +} + + void +gui_mch_free_font(GuiFont font) +{ + if (font != NOFONT) + pango_font_description_free(font); +} + + void +gui_mch_expand_font( + optexpand_T *args, + void *param, + int (*add_match)(char_u *val)) +{ + PangoFontFamily **font_families = NULL; + int n_families = 0; + int wide = *(int *)param; + + if (args->oe_include_orig_val && *args->oe_opt_value == NUL && !wide) + { + // If guifont is empty, suggest the default so the user can modify it. + if (add_match((char_u *)DEFAULT_FONT) != OK) + return; + } + + pango_context_list_families( + gui.text_context, + &font_families, + &n_families); + + for (int i = 0; i < n_families; i++) + { + if (!wide && !pango_font_family_is_monospace(font_families[i])) + continue; + + const char *fam_name = pango_font_family_get_name(font_families[i]); + if (input_conv.vc_type != CONV_NONE) + { + char_u *buf = string_convert(&input_conv, + (char_u *)fam_name, NULL); + if (buf != NULL) + { + if (add_match(buf) != OK) + { + vim_free(buf); + break; + } + vim_free(buf); + } + else + break; + } + else + { + if (add_match((char_u *)fam_name) != OK) + break; + } + } + + g_free(font_families); +} + +/* + * ============================================================ + * Color handling + * ============================================================ + */ + + guicolor_T +gui_mch_get_color(char_u *name) +{ + if (!gui.in_use) + return INVALCOLOR; + + if (name != NULL) + return gui_get_color_cmn(name); + + return INVALCOLOR; +} + + guicolor_T +gui_mch_get_rgb_color(int r, int g, int b) +{ + return gui_get_rgb_color_cmn(r, g, b); +} + + static GdkRGBA +color_to_rgba(guicolor_T color) +{ + GdkRGBA rgba; + rgba.red = ((color & 0xff0000) >> 16) / 255.0; + rgba.green = ((color & 0xff00) >> 8) / 255.0; + rgba.blue = (color & 0xff) / 255.0; + rgba.alpha = 1.0; + return rgba; +} + + void +gui_mch_set_fg_color(guicolor_T color) +{ + *gui.fgcolor = color_to_rgba(color); +} + + void +gui_mch_set_bg_color(guicolor_T color) +{ + *gui.bgcolor = color_to_rgba(color); +} + + void +gui_mch_set_sp_color(guicolor_T color) +{ + *gui.spcolor = color_to_rgba(color); +} + + guicolor_T +gui_mch_get_rgb(guicolor_T pixel) +{ + return pixel; +} + +/* + * ============================================================ + * Drawing + * ============================================================ + */ + +static void set_cairo_source_from_pixel(cairo_t *cr, guicolor_T pixel); + + static void +draw_event(GtkDrawingArea *area UNUSED, cairo_t *cr, + int width, int height, gpointer data UNUSED) +{ + // Surface creation/resizing is handled by drawarea_resize_cb. + // Here we only paint the surface to the widget. + + // Fill background with Vim's background color + set_cairo_source_from_pixel(cr, gui.back_pixel); + cairo_rectangle(cr, 0, 0, width, height); + cairo_fill(cr); + + // Paint the Vim surface on top + if (gui.surface != NULL) + { + cairo_set_source_surface(cr, gui.surface, 0, 0); + cairo_paint(cr); + } +} + + static void +set_cairo_source_from_pixel(cairo_t *cr, guicolor_T pixel) +{ + cairo_set_source_rgb(cr, + ((pixel & 0xff0000) >> 16) / 255.0, + ((pixel & 0xff00) >> 8) / 255.0, + (pixel & 0xff) / 255.0); +} + + void +gui_mch_clear_block(int row1, int col1, int row2, int col2) +{ + cairo_t *cr; + + if (gui.surface == NULL) + return; + + cr = cairo_create(gui.surface); + set_cairo_source_from_pixel(cr, gui.back_pixel); + cairo_rectangle(cr, + FILL_X(col1), FILL_Y(row1), + (col2 - col1 + 1) * gui.char_width, + (row2 - row1 + 1) * gui.char_height); + cairo_fill(cr); + cairo_destroy(cr); + + if (gui.drawarea != NULL) + gtk_widget_queue_draw(gui.drawarea); +} + + void +gui_mch_clear_all(void) +{ + cairo_t *cr; + + if (gui.surface == NULL) + return; + + cr = cairo_create(gui.surface); + set_cairo_source_from_pixel(cr, gui.back_pixel); + cairo_paint(cr); + cairo_destroy(cr); + + if (gui.drawarea != NULL) + gtk_widget_queue_draw(gui.drawarea); +} + + static void +surface_copy_rect(int dest_x, int dest_y, + int src_x, int src_y, + int width, int height) +{ + cairo_t *cr; + cairo_surface_t *tmp; + + if (gui.surface == NULL || width <= 0 || height <= 0) + return; + + // Use a temporary surface to avoid overlap issues + tmp = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cr = cairo_create(tmp); + cairo_set_source_surface(cr, gui.surface, -src_x, -src_y); + cairo_paint(cr); + cairo_destroy(cr); + + cr = cairo_create(gui.surface); + cairo_set_source_surface(cr, tmp, dest_x, dest_y); + cairo_paint(cr); + cairo_destroy(cr); + cairo_surface_destroy(tmp); +} + + void +gui_mch_delete_lines(int row, int num_lines) +{ + int ncols = gui.scroll_region_right - gui.scroll_region_left + 1; + int nrows = gui.scroll_region_bot - row + 1; + int src_nrows = nrows - num_lines; + + surface_copy_rect( + FILL_X(gui.scroll_region_left), FILL_Y(row), + FILL_X(gui.scroll_region_left), FILL_Y(row + num_lines), + gui.char_width * ncols + 1, gui.char_height * src_nrows); + gui_clear_block( + gui.scroll_region_bot - num_lines + 1, gui.scroll_region_left, + gui.scroll_region_bot, gui.scroll_region_right); + + gtk_widget_queue_draw(gui.drawarea); +} + + void +gui_mch_insert_lines(int row, int num_lines) +{ + int ncols = gui.scroll_region_right - gui.scroll_region_left + 1; + int nrows = gui.scroll_region_bot - row + 1; + int src_nrows = nrows - num_lines; + + surface_copy_rect( + FILL_X(gui.scroll_region_left), FILL_Y(row + num_lines), + FILL_X(gui.scroll_region_left), FILL_Y(row), + gui.char_width * ncols + 1, gui.char_height * src_nrows); + gui_clear_block( + row, gui.scroll_region_left, + row + num_lines - 1, gui.scroll_region_right); + + gtk_widget_queue_draw(gui.drawarea); +} + + void +gui_mch_draw_hollow_cursor(guicolor_T color) +{ + cairo_t *cr; + int i = 1; + + if (gui.surface == NULL) + return; + + cr = cairo_create(gui.surface); + gui_mch_set_fg_color(color); + cairo_set_source_rgba(cr, + gui.fgcolor->red, gui.fgcolor->green, + gui.fgcolor->blue, gui.fgcolor->alpha); + if (mb_lefthalve(gui.row, gui.col)) + i = 2; + cairo_set_line_width(cr, 1.0); + cairo_rectangle(cr, + FILL_X(gui.col) + 0.5, FILL_Y(gui.row) + 0.5, + i * gui.char_width - 1, gui.char_height - 1); + cairo_stroke(cr); + cairo_destroy(cr); + + gtk_widget_queue_draw(gui.drawarea); +} + + void +gui_mch_draw_part_cursor(int w, int h, guicolor_T color) +{ + cairo_t *cr; + + if (gui.surface == NULL) + return; + + gui_mch_set_fg_color(color); + cr = cairo_create(gui.surface); + cairo_set_source_rgba(cr, + gui.fgcolor->red, gui.fgcolor->green, + gui.fgcolor->blue, gui.fgcolor->alpha); + cairo_rectangle(cr, +#ifdef FEAT_RIGHTLEFT + CURSOR_BAR_RIGHT ? FILL_X(gui.col + 1) - w : +#endif + FILL_X(gui.col), FILL_Y(gui.row) + gui.char_height - h, + w, h); + cairo_fill(cr); + cairo_destroy(cr); + + gtk_widget_queue_draw(gui.drawarea); +} + + void +gui_mch_flash(int msec) +{ + // Invert the screen, wait, then invert back + if (gui.surface == NULL) + return; + + gui_mch_invert_rectangle(0, 0, (int)Rows - 1, (int)Columns - 1); + gui_mch_flush(); + ui_delay((long)msec, TRUE); + gui_mch_invert_rectangle(0, 0, (int)Rows - 1, (int)Columns - 1); +} + + void +gui_mch_invert_rectangle(int r, int c, int nr, int nc) +{ + cairo_t *cr; + + if (gui.surface == NULL) + return; + + cr = cairo_create(gui.surface); + cairo_set_operator(cr, CAIRO_OPERATOR_DIFFERENCE); + cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); + cairo_rectangle(cr, + FILL_X(c), FILL_Y(r), + (nc + 1) * gui.char_width, (nr + 1) * gui.char_height); + cairo_fill(cr); + cairo_destroy(cr); + + gtk_widget_queue_draw(gui.drawarea); +} + +/* + * ============================================================ + * Event handling + * ============================================================ + */ + + static gboolean +key_press_event(GtkEventControllerKey *controller UNUSED, + guint key_sym, guint keycode UNUSED, + GdkModifierType state, gpointer data UNUSED) +{ + char_u string[32], string2[32]; + int len; + int i; + int modifiers; + int key; + char_u *s, *d; + +#ifdef FEAT_XIM + // Let the input method have a go at the key event. + // If it consumed the event, we're done. + if (xic != NULL) + { + GdkEvent *event = gtk_event_controller_get_current_event( + GTK_EVENT_CONTROLLER(controller)); + if (event != NULL && gtk_im_context_filter_keypress(xic, event)) + return TRUE; + } +#endif + + len = keyval_to_string(key_sym, string2); + + if (len > 1 && input_conv.vc_type != CONV_NONE) + len = convert_input(string2, len, sizeof(string2)); + + s = string2; + d = string; + for (i = 0; i < len; ++i) + { + *d++ = s[i]; + if (d[-1] == CSI && d + 2 < string + sizeof(string)) + { + *d++ = KS_EXTRA; + *d++ = (int)KE_CSI; + } + } + len = d - string; + + // Shift-Tab results in Left_Tab + if (key_sym == GDK_KEY_ISO_Left_Tab) + { + key_sym = GDK_KEY_Tab; + state |= GDK_SHIFT_MASK; + } + + // Check for special keys + if (len == 0 || len == 1) + { + for (i = 0; special_keys[i].key_sym != 0; i++) + { + if (special_keys[i].key_sym == key_sym) + { + string[0] = CSI; + string[1] = special_keys[i].code0; + string[2] = special_keys[i].code1; + len = -3; + break; + } + } + } + + if (len == 0) + return TRUE; + + if (len == -3) + key = TO_SPECIAL(string[1], string[2]); + else + { + string[len] = NUL; + key = mb_ptr2char(string); + } + + modifiers = modifiers_gdk2vim(state); + + key = simplify_key(key, &modifiers); + if (key == CSI) + key = K_CSI; + if (IS_SPECIAL(key)) + { + string[0] = CSI; + string[1] = K_SECOND(key); + string[2] = K_THIRD(key); + len = 3; + } + else + { + key = may_adjust_key_for_ctrl(modifiers, key); + modifiers = may_remove_shift_modifier(modifiers, key); + len = mb_char2bytes(key, string); + } + + if (modifiers != 0) + { + string2[0] = CSI; + string2[1] = KS_MODIFIER; + string2[2] = modifiers; + add_to_input_buf(string2, 3); + } + + { + int int_ch = check_for_interrupt(key, modifiers); + if (int_ch != NUL) + { + trash_input_buf(); + string[0] = int_ch; + len = 1; + } + } + + add_to_input_buf(string, len); + + if (p_mh) + gui_mch_mousehide(TRUE); + + return TRUE; +} + + static void +key_release_event(GtkEventControllerKey *controller UNUSED, + guint keyval UNUSED, guint keycode UNUSED, + GdkModifierType state UNUSED, gpointer data UNUSED) +{ +} + +static int mouse_timed_out = TRUE; +static guint mouse_click_timer = 0; + + static timeout_cb_type +mouse_click_timer_cb(gpointer data) +{ + *(int *)data = TRUE; + return FALSE; +} + + static int +modifiers_gdk2mouse(guint state) +{ + int modifiers = 0; + + if (state & GDK_SHIFT_MASK) + modifiers |= MOUSE_SHIFT; + if (state & GDK_CONTROL_MASK) + modifiers |= MOUSE_CTRL; + if (state & GDK_ALT_MASK) + modifiers |= MOUSE_ALT; + + return modifiers; +} + +// Track which mouse button is currently pressed for drag detection. +// GtkEventControllerMotion's modifier state may not include button masks +// on all backends (e.g. Wayland), so we track it ourselves. +// -1 means no button is pressed (MOUSE_LEFT is 0x00, so can't use 0). +static int mouse_pressed_button = -1; + + static void +button_press_event(GtkGestureClick *gesture, int n_press UNUSED, + double x, double y, gpointer data UNUSED) +{ + int button; + int repeated_click = FALSE; + int_u vim_modifiers; + guint btn; + GdkModifierType state; + GdkEvent *event; + + event = gtk_event_controller_get_current_event( + GTK_EVENT_CONTROLLER(gesture)); + state = gdk_event_get_modifier_state(event); + btn = gdk_button_event_get_button(event); + + if (!mouse_timed_out && mouse_click_timer) + { + timeout_remove(mouse_click_timer); + mouse_click_timer = 0; + repeated_click = TRUE; + } + + mouse_timed_out = FALSE; + mouse_click_timer = timeout_add(p_mouset, mouse_click_timer_cb, + &mouse_timed_out); + + switch (btn) + { + case 1: button = MOUSE_LEFT; break; + case 2: button = MOUSE_MIDDLE; break; + case 3: button = MOUSE_RIGHT; break; + case 8: button = MOUSE_X1; break; + case 9: button = MOUSE_X2; break; + default: return; + } + + mouse_pressed_button = button; + vim_modifiers = modifiers_gdk2mouse(state); + gui_send_mouse_event(button, (int)x, (int)y, repeated_click, vim_modifiers); +} + + static void +button_release_event(GtkGestureClick *gesture, int n_press UNUSED, + double x, double y, gpointer data UNUSED) +{ + int vim_modifiers; + GdkModifierType state; + GdkEvent *event; + + event = gtk_event_controller_get_current_event( + GTK_EVENT_CONTROLLER(gesture)); + state = gdk_event_get_modifier_state(event); + vim_modifiers = modifiers_gdk2mouse(state); + + mouse_pressed_button = -1; + gui_send_mouse_event(MOUSE_RELEASE, (int)x, (int)y, FALSE, vim_modifiers); +} + + static void +motion_notify_event(GtkEventControllerMotion *controller UNUSED, + double x, double y, gpointer data UNUSED) +{ + if (mouse_pressed_button >= 0) + { + GdkModifierType state; + GdkEvent *event; + + event = gtk_event_controller_get_current_event( + GTK_EVENT_CONTROLLER(controller)); + if (event != NULL) + { + state = gdk_event_get_modifier_state(event); + gui_send_mouse_event(MOUSE_DRAG, (int)x, (int)y, + FALSE, modifiers_gdk2mouse(state)); + } + } + + if (p_mh) + gui_mch_mousehide(FALSE); +} + + static void +enter_notify_event(GtkEventControllerMotion *controller UNUSED, + double x UNUSED, double y UNUSED, gpointer data UNUSED) +{ + if (blink_state == BLINK_NONE) + gui_mch_start_blink(); + + // Make sure keyboard input goes to the drawing area. + if (!gtk_widget_has_focus(gui.drawarea)) + gtk_widget_grab_focus(gui.drawarea); +} + + static void +leave_notify_event(GtkEventControllerMotion *controller UNUSED, + gpointer data UNUSED) +{ + if (blink_state != BLINK_NONE) + gui_mch_stop_blink(TRUE); +} + + static gboolean +scroll_event(GtkEventControllerScroll *controller UNUSED, + double dx UNUSED, double dy, gpointer data UNUSED) +{ + int button; + int_u vim_modifiers; + GdkModifierType state; + GdkEvent *event; + + event = gtk_event_controller_get_current_event( + GTK_EVENT_CONTROLLER(controller)); + if (event == NULL) + return FALSE; + state = gdk_event_get_modifier_state(event); + + if (dy < 0) + button = MOUSE_4; // scroll up + else if (dy > 0) + button = MOUSE_5; // scroll down + else if (dx < 0) + button = MOUSE_7; // scroll left + else if (dx > 0) + button = MOUSE_6; // scroll right + else + return FALSE; + + vim_modifiers = modifiers_gdk2mouse(state); + + { + double mx, my; + gdk_event_get_position(event, &mx, &my); + gui_send_mouse_event(button, (int)mx, (int)my, FALSE, vim_modifiers); + } + + return TRUE; +} + + static void +focus_in_event(GtkEventControllerFocus *controller UNUSED, + gpointer data UNUSED) +{ + gui_focus_change(TRUE); + if (blink_state == BLINK_NONE) + gui_mch_start_blink(); +} + + static void +focus_out_event(GtkEventControllerFocus *controller UNUSED, + gpointer data UNUSED) +{ + gui_focus_change(FALSE); + if (blink_state != BLINK_NONE) + gui_mch_stop_blink(TRUE); +} + + static void +drawarea_realize_cb(GtkWidget *widget UNUSED, gpointer data UNUSED) +{ + int w, h; + + // Use formwin size since drawarea may not have its final size yet + if (gui.formwin != NULL) + { + w = gtk_widget_get_width(gui.formwin); + h = gtk_widget_get_height(gui.formwin); + } + else + { + w = gtk_widget_get_width(widget); + h = gtk_widget_get_height(widget); + } + + if (w <= 0) w = 800; + if (h <= 0) h = 600; + + if (gui.surface != NULL) + cairo_surface_destroy(gui.surface); + gui.surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); + + gui_mch_new_colors(); + +#ifdef FEAT_XIM + xim_init(); +#endif +} + + static void +drawarea_unrealize_cb(GtkWidget *widget UNUSED, gpointer data UNUSED) +{ + if (gui.surface != NULL) + { + cairo_surface_destroy(gui.surface); + gui.surface = NULL; + } +} + + static void +drawarea_resize_cb(GtkDrawingArea *area UNUSED, int width, int height, + gpointer data UNUSED) +{ + cairo_t *cr; + + if (width <= 0 || height <= 0) + return; + + if (gui.surface != NULL) + { + int sw = cairo_image_surface_get_width(gui.surface); + int sh = cairo_image_surface_get_height(gui.surface); + + if (sw == width && sh == height) + return; + + cairo_surface_destroy(gui.surface); + } + + // Create a fresh surface filled with the background color. + // Do not copy old surface content: gui_resize_shell() will trigger + // a full redraw, and stale content (e.g. intro screen text) would + // otherwise remain as ghost artifacts. + gui.surface = cairo_image_surface_create( + CAIRO_FORMAT_ARGB32, width, height); + cr = cairo_create(gui.surface); + set_cairo_source_from_pixel(cr, gui.back_pixel); + cairo_paint(cr); + cairo_destroy(cr); + + // Notify Vim about the new size - this will cause a full redraw + gui_resize_shell(width, height); +} + +#ifdef FEAT_DND +/* + * Drag-and-drop handler for files and text. + */ + static gboolean +drop_cb(GtkDropTarget *target UNUSED, const GValue *value, + double x, double y, gpointer data UNUSED) +{ + if (G_VALUE_HOLDS(value, GDK_TYPE_FILE_LIST)) + { + GSList *files = g_value_get_boxed(value); + int nfiles = g_slist_length(files); + char_u **fnames; + int i; + + if (nfiles <= 0) + return FALSE; + + fnames = ALLOC_MULT(char_u *, nfiles); + if (fnames == NULL) + return FALSE; + + i = 0; + for (GSList *l = files; l != NULL; l = l->next) + { + GFile *file = l->data; + char *path = g_file_get_path(file); + if (path != NULL) + fnames[i++] = vim_strsave((char_u *)path); + g_free(path); + } + nfiles = i; + + if (nfiles > 0) + gui_handle_drop((int)x, (int)y, 0, fnames, nfiles); + else + vim_free(fnames); + + return TRUE; + } + else if (G_VALUE_HOLDS(value, G_TYPE_STRING)) + { + const char *text = g_value_get_string(value); + char_u dropkey[6] = {CSI, KS_MODIFIER, 0, + CSI, KS_EXTRA, (char_u)KE_DROP}; + + if (text == NULL || *text == NUL) + return FALSE; + + dnd_yank_drag_data((char_u *)text, (long)STRLEN(text)); + add_to_input_buf(dropkey + 3, 3); + + return TRUE; + } + + return FALSE; +} +#endif + + static void +mainwin_destroy_cb(GObject *object UNUSED, gpointer data UNUSED) +{ + gui.mainwin = NULL; + gui.drawarea = NULL; + if (!exiting) + gui_shell_closed(); +} + + static gboolean +delete_event_cb(GtkWindow *window UNUSED, gpointer data UNUSED) +{ + gui_shell_closed(); + return TRUE; +} + +/* + * ============================================================ + * Misc functions + * ============================================================ + */ + + static timeout_cb_type +input_timer_cb(gpointer data) +{ + int *timed_out = (int *)data; + + *timed_out = TRUE; + return FALSE; // don't call me again +} + + void +gui_mch_update(void) +{ + int cnt = 0; + + while (g_main_context_pending(NULL) && !vim_is_input_buf_full() + && ++cnt < 100) + g_main_context_iteration(NULL, TRUE); +} + + int +gui_mch_wait_for_chars(long wtime) +{ + int focus; + guint timer; + static int timed_out; + int retval = FAIL; + + timed_out = FALSE; + + if (wtime >= 0) + timer = timeout_add(wtime == 0 ? 1L : wtime, + input_timer_cb, &timed_out); + else + timer = 0; + + focus = gui.in_focus; + + do + { + // Stop or start blinking when focus changes + if (gui.in_focus != focus) + { + if (gui.in_focus) + gui_mch_start_blink(); + else + gui_mch_stop_blink(TRUE); + focus = gui.in_focus; + } + +#ifdef MESSAGE_QUEUE +# ifdef FEAT_TIMERS + did_add_timer = FALSE; +# endif + parse_queued_messages(); +# ifdef FEAT_TIMERS + if (did_add_timer) + goto theend; +# endif +#endif + + if (gui.mainwin == NULL) + goto theend; + + // gtk_main_quit() is a wake-up request; consume it so later + // waits resume. + if (gtk4_main_loop_quit) + { + gtk4_main_loop_quit = FALSE; + goto theend; + } + + if (!input_available()) + { + ++gtk4_main_loop_level; + g_main_context_iteration(NULL, TRUE); + --gtk4_main_loop_level; + } + + if (input_available()) + { + retval = OK; + goto theend; + } + } while (wtime < 0 || !timed_out); + + gui_mch_update(); + +theend: + if (timer != 0 && !timed_out) + timeout_remove(timer); + + return retval; +} + + void +gui_mch_flush(void) +{ + // Ensure the offscreen surface content gets painted to the widget. + if (gui.drawarea != NULL) + gtk_widget_queue_draw(gui.drawarea); + if (gui.mainwin != NULL && gtk_widget_get_realized(gui.mainwin)) + gdk_display_flush(gtk_widget_get_display(gui.mainwin)); +} + + void +gui_mch_beep(void) +{ + GdkDisplay *display; + + if (gui.mainwin != NULL && gtk_widget_get_realized(gui.mainwin)) + { + display = gtk_widget_get_display(gui.mainwin); + if (display != NULL) + gdk_display_beep(display); + } +} + + void * +gui_mch_get_display(void) +{ + if (gui.mainwin != NULL && gtk_widget_get_display(gui.mainwin)) + return gtk_widget_get_display(gui.mainwin); + return NULL; +} + + void +gui_mch_iconify(void) +{ + gtk_window_minimize(GTK_WINDOW(gui.mainwin)); +} + + void +gui_mch_set_foreground(void) +{ + gtk_window_present(GTK_WINDOW(gui.mainwin)); +} + + void +gui_mch_getmouse(int *x, int *y) +{ + *x = 0; + *y = 0; + // GTK4: No reliable way to query pointer position synchronously. +} + + void +gui_mch_setmouse(int x UNUSED, int y UNUSED) +{ + // GTK4/Wayland: cannot warp pointer +} + + void +gui_mch_mousehide(int hide) +{ + if (gui.pointer_hidden == hide) + return; + + gui.pointer_hidden = hide; + if (gui.blank_pointer != NULL) + { + if (hide) + gtk_widget_set_cursor(gui.drawarea, gui.blank_pointer); + else +#ifdef FEAT_MOUSESHAPE + mch_set_mouse_shape(last_shape); +#else + gtk_widget_set_cursor(gui.drawarea, NULL); +#endif + } +} + + int +gui_mch_haskey(char_u *name) +{ + int i; + + for (i = 0; special_keys[i].key_sym != 0; i++) + if (name[0] == special_keys[i].code0 + && name[1] == special_keys[i].code1) + return OK; + return FAIL; +} + + void +gui_mch_forked(void) +{ +} + +/* + * ============================================================ + * Scrollbar + * ============================================================ + */ + + void +gui_mch_enable_scrollbar(scrollbar_T *sb, int flag) +{ + if (sb->id != NULL) + gtk_widget_set_visible(sb->id, flag); +} + +/* + * ============================================================ + * Menu stubs + * ============================================================ + */ + + void +gui_mch_menu_grey(vimmenu_T *menu, int grey) +{ + if (menu->id == NULL || menu_action_group == NULL) + return; + + // For toolbar items, use gtk_widget_set_sensitive + if (menu->parent != NULL && menu_is_toolbar(menu->parent->name)) + { + if (menu->id != (GtkWidget *)1) + gtk_widget_set_sensitive(menu->id, !grey); + return; + } + + // For menu items, enable/disable the GSimpleAction + if (menu->label != NULL) + { + GAction *action = g_action_map_lookup_action( + G_ACTION_MAP(menu_action_group), + (const char *)menu->label); + if (action != NULL) + g_simple_action_set_enabled(G_SIMPLE_ACTION(action), !grey); + } +} + + void +gui_mch_menu_hidden(vimmenu_T *menu UNUSED, int hidden UNUSED) +{ + // No-op: menu system not yet implemented for GTK4. +} + + void +gui_mch_draw_menubar(void) +{ + // No-op: menu system not yet implemented for GTK4. +} + +/* + * ============================================================ + * Tabline + * ============================================================ + */ + +#ifdef FEAT_GUI_TABLINE + void +gui_mch_show_tabline(int showit) +{ + if (gui.tabline != NULL) + gtk_widget_set_visible(gui.tabline, showit); +} + + int +gui_mch_showing_tabline(void) +{ + return gui.tabline != NULL && gtk_widget_get_visible(gui.tabline); +} + +static int ignore_tabline_evt = FALSE; + + void +gui_mch_update_tabline(void) +{ + GtkWidget *page; + GtkWidget *event_box; + GtkWidget *label; + tabpage_T *tp; + int nr = 0; + int tab_num; + int curtabidx = 0; + char_u *labeltext; + + if (gui.tabline == NULL) + return; + + ignore_tabline_evt = TRUE; + + for (tp = first_tabpage; tp != NULL; tp = tp->tp_next, ++nr) + { + if (tp == curtab) + curtabidx = nr; + + tab_num = nr + 1; + + page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(gui.tabline), nr); + if (page == NULL) + { + page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_visible(page, TRUE); + event_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_set_visible(event_box, TRUE); + label = gtk_label_new("-Empty-"); + gtk_box_append(GTK_BOX(event_box), label); + gtk_widget_set_visible(label, TRUE); + gtk_notebook_insert_page(GTK_NOTEBOOK(gui.tabline), + page, event_box, nr++); + gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(gui.tabline), + page, TRUE); + } + + event_box = gtk_notebook_get_tab_label(GTK_NOTEBOOK(gui.tabline), page); + g_object_set_data(G_OBJECT(event_box), "tab_num", + GINT_TO_POINTER(tab_num)); + label = gtk_widget_get_first_child(event_box); + get_tabline_label(tp, FALSE); + labeltext = CONVERT_TO_UTF8(NameBuff); + if (label != NULL && GTK_IS_LABEL(label)) + gtk_label_set_text(GTK_LABEL(label), (const char *)labeltext); + CONVERT_TO_UTF8_FREE(labeltext); + + get_tabline_label(tp, TRUE); + labeltext = CONVERT_TO_UTF8(NameBuff); + gtk_widget_set_tooltip_text(event_box, (const gchar *)labeltext); + CONVERT_TO_UTF8_FREE(labeltext); + } + + while (gtk_notebook_get_nth_page(GTK_NOTEBOOK(gui.tabline), nr) != NULL) + gtk_notebook_remove_page(GTK_NOTEBOOK(gui.tabline), nr); + + if (gtk_notebook_get_current_page(GTK_NOTEBOOK(gui.tabline)) != curtabidx) + gtk_notebook_set_current_page(GTK_NOTEBOOK(gui.tabline), curtabidx); + + ignore_tabline_evt = FALSE; +} + + void +gui_mch_set_curtab(int nr) +{ + if (gui.tabline != NULL) + gtk_notebook_set_current_page(GTK_NOTEBOOK(gui.tabline), nr - 1); +} +#endif + +/* + * ============================================================ + * Sign support + * ============================================================ + */ + +#if defined(FEAT_SIGN_ICONS) +# define SIGN_WIDTH (2 * gui.char_width) +# define SIGN_HEIGHT (gui.char_height) + + void +gui_mch_drawsign(int row, int col, int typenr) +{ + GdkPixbuf *sign; + cairo_t *cr; + int width, height; + + sign = (GdkPixbuf *)sign_get_image(typenr); + if (sign == NULL || gui.surface == NULL) + return; + + cr = cairo_create(gui.surface); + + width = gdk_pixbuf_get_width(sign); + height = gdk_pixbuf_get_height(sign); + + // Scale to fit the sign area if needed + if (width != SIGN_WIDTH || height != SIGN_HEIGHT) + { + GdkPixbuf *scaled = gdk_pixbuf_scale_simple(sign, + SIGN_WIDTH, SIGN_HEIGHT, GDK_INTERP_BILINEAR); + if (scaled != NULL) + { + gdk_cairo_set_source_pixbuf(cr, scaled, + FILL_X(col), FILL_Y(row)); + g_object_unref(scaled); + } + else + gdk_cairo_set_source_pixbuf(cr, sign, + FILL_X(col), FILL_Y(row)); + } + else + gdk_cairo_set_source_pixbuf(cr, sign, + FILL_X(col), FILL_Y(row)); + + cairo_paint(cr); + cairo_destroy(cr); + + gtk_widget_queue_draw(gui.drawarea); +} + + void * +gui_mch_register_sign(char_u *signfile) +{ + if (signfile[0] != NUL && signfile[0] != '-' && gui.in_use) + { + GdkPixbuf *sign; + GError *error = NULL; + + sign = gdk_pixbuf_new_from_file((const char *)signfile, &error); + if (error == NULL) + return sign; + + semsg("E255: %s", error->message); + g_error_free(error); + } + return NULL; +} + + void +gui_mch_destroy_sign(void *sign) +{ + if (sign != NULL) + g_object_unref(sign); +} +#endif + +/* + * ============================================================ + * Stubs for functions not yet implemented or not applicable in GTK4 + * ============================================================ + */ + +/* + * Ligature and text drawing support. + * Ported from gui_gtk_x11.c (GTK3) to support 'guiligatures' in GTK4. + */ + +#define INSERT_PANGO_ATTR(Attribute, AttrList, Start, End) \ + G_STMT_START{ \ + PangoAttribute *tmp_attr_; \ + tmp_attr_ = (Attribute); \ + tmp_attr_->start_index = (Start); \ + tmp_attr_->end_index = (End); \ + pango_attr_list_insert((AttrList), tmp_attr_); \ + }G_STMT_END + +/* + * Apply the 'guifontwide' font to double-width characters in the string. + */ + static void +apply_wide_font_attr(char_u *s, int len, PangoAttrList *attr_list) +{ + char_u *start = NULL; + char_u *p; + int uc; + + for (p = s; p < s + len; p += utf_byte2len(*p)) + { + uc = utf_ptr2char(p); + + if (start == NULL) + { + if (uc >= 0x80 && utf_char2cells(uc) == 2) + start = p; + } + else if (uc < 0x80 + || (utf_char2cells(uc) != 2 && !utf_iscomposing(uc))) + { + INSERT_PANGO_ATTR(pango_attr_font_desc_new(gui.wide_font), + attr_list, start - s, p - s); + start = NULL; + } + } + + if (start != NULL) + INSERT_PANGO_ATTR(pango_attr_font_desc_new(gui.wide_font), + attr_list, start - s, len); +} + +/* + * Count the number of display cells occupied by a glyph cluster. + */ + static int +count_cluster_cells(char_u *s, PangoItem *item, + PangoGlyphString *glyphs, int i, + int *cluster_width, + int *last_glyph_rbearing) +{ + char_u *p; + int next; + int start, end; + int width; + int uc; + int cellcount = 0; + + width = glyphs->glyphs[i].geometry.width; + + for (next = i + 1; next < glyphs->num_glyphs; ++next) + { + if (glyphs->glyphs[next].attr.is_cluster_start) + break; + else if (glyphs->glyphs[next].geometry.width > width) + width = glyphs->glyphs[next].geometry.width; + } + + start = item->offset + glyphs->log_clusters[i]; + end = item->offset + ((next < glyphs->num_glyphs) ? + glyphs->log_clusters[next] : item->length); + + for (p = s + start; p < s + end; p += utf_byte2len(*p)) + { + uc = utf_ptr2char(p); + if (uc < 0x80) + ++cellcount; + else if (!utf_iscomposing(uc)) + cellcount += utf_char2cells(uc); + } + + if (last_glyph_rbearing != NULL + && cellcount > 0 && next == glyphs->num_glyphs) + { + PangoRectangle ink_rect; + + pango_font_get_glyph_extents(item->analysis.font, + glyphs->glyphs[i].glyph, + &ink_rect, NULL); + + if (PANGO_RBEARING(ink_rect) > 0) + *last_glyph_rbearing = PANGO_RBEARING(ink_rect); + } + + if (cellcount > 0) + *cluster_width = width; + + return cellcount; +} + +/* + * Handle combining characters that form a zero-width cluster. + */ + static void +setup_zero_width_cluster(PangoItem *item, PangoGlyphInfo *glyph, + int last_cellcount, int last_cluster_width, + int last_glyph_rbearing) +{ + PangoRectangle ink_rect; + PangoRectangle logical_rect; + int width; + + width = last_cellcount * gui.char_width * PANGO_SCALE; + glyph->geometry.x_offset = -width + MAX(0, width - last_cluster_width) / 2; + glyph->geometry.width = 0; + + pango_font_get_glyph_extents(item->analysis.font, + glyph->glyph, + &ink_rect, &logical_rect); + if (ink_rect.x < 0) + { + glyph->geometry.x_offset += last_glyph_rbearing; + glyph->geometry.y_offset = logical_rect.height + - (gui.char_height - p_linespace) * PANGO_SCALE; + } + else + glyph->geometry.x_offset = -width + MAX(0, width - ink_rect.width) / 2; +} + +/* + * Draw a single glyph string segment: background, foreground, and fake bold. + */ + static void +draw_glyph_string(int row, int col, int num_cells, int flags, + PangoFont *font, PangoGlyphString *glyphs, + cairo_t *cr) +{ + if (!(flags & DRAW_TRANSP)) + { + cairo_set_source_rgba(cr, + gui.bgcolor->red, gui.bgcolor->green, gui.bgcolor->blue, + gui.bgcolor->alpha); + cairo_rectangle(cr, + FILL_X(col), FILL_Y(row), + num_cells * gui.char_width, gui.char_height); + cairo_fill(cr); + } + + cairo_set_source_rgba(cr, + gui.fgcolor->red, gui.fgcolor->green, gui.fgcolor->blue, + gui.fgcolor->alpha); + cairo_move_to(cr, TEXT_X(col), TEXT_Y(row)); + pango_cairo_show_glyph_string(cr, font, glyphs); + + // Redraw with offset of 1 to emulate bold + if ((flags & DRAW_BOLD) && !gui.font_can_bold) + { + cairo_move_to(cr, TEXT_X(col) + 1, TEXT_Y(row)); + pango_cairo_show_glyph_string(cr, font, glyphs); + } +} + +/* + * Draw underline, undercurl, and strikethrough decorations. + */ + static void +draw_under(int flags, int row, int col, int cells, cairo_t *cr) +{ + // Draw underline + if (flags & DRAW_UNDERL) + { + int y = FILL_Y(row + 1) - 1; + cairo_set_source_rgba(cr, + gui.fgcolor->red, gui.fgcolor->green, + gui.fgcolor->blue, gui.fgcolor->alpha); + cairo_set_line_width(cr, 1.0); + cairo_move_to(cr, FILL_X(col), y + 0.5); + cairo_line_to(cr, FILL_X(col + cells), y + 0.5); + cairo_stroke(cr); + } + + // Draw undercurl + if (flags & DRAW_UNDERC) + { + static const int val[8] = {1, 0, 0, 0, 1, 2, 2, 2}; + int y = FILL_Y(row + 1) - 1; + int i, offset; + + cairo_set_line_width(cr, 1.0); + cairo_set_source_rgba(cr, + gui.spcolor->red, gui.spcolor->green, + gui.spcolor->blue, gui.spcolor->alpha); + cairo_move_to(cr, FILL_X(col) + 1, y - 2 + 0.5); + for (i = FILL_X(col) + 1; i < FILL_X(col + cells); ++i) + { + offset = val[i % 8]; + cairo_line_to(cr, i, y - offset + 0.5); + } + cairo_stroke(cr); + } + + // Draw strikethrough + if (flags & DRAW_STRIKE) + { + int y = FILL_Y(row) + gui.char_height / 2; + cairo_set_source_rgba(cr, + gui.fgcolor->red, gui.fgcolor->green, + gui.fgcolor->blue, gui.fgcolor->alpha); + cairo_set_line_width(cr, 1.0); + cairo_move_to(cr, FILL_X(col), y + 0.5); + cairo_line_to(cr, FILL_X(col + cells), y + 0.5); + cairo_stroke(cr); + } +} + +/* + * Draw a string of characters on the screen. + * "force_pango" is set when ligature characters require Pango shaping + * instead of the fast ASCII glyph cache path. + * Returns the number of display cells used. + */ + int +gui_gtk_draw_string_ext( + int row, + int col, + char_u *s, + int len, + int flags, + int force_pango) +{ + GdkRectangle area; + PangoGlyphString *glyphs; + int column_offset = 0; + int i; + cairo_t *cr; + + if (gui.text_context == NULL || gui.surface == NULL) + return len; + + // Restrict all drawing to the current screen line. + area.x = gui.border_offset; + area.y = FILL_Y(row); + area.width = gui.num_cols * gui.char_width; + area.height = gui.char_height; + + cr = cairo_create(gui.surface); + cairo_rectangle(cr, area.x, area.y, area.width, area.height); + cairo_clip(cr); + + glyphs = pango_glyph_string_new(); + + // Fast path for pure ASCII: use cached glyph table. + // Skip this path when force_pango is set (ligatures need shaping). + if (!(flags & DRAW_ITALIC) + && !((flags & DRAW_BOLD) && gui.font_can_bold) + && gui.ascii_glyphs != NULL + && !force_pango) + { + char_u *p; + + for (p = s; p < s + len; ++p) + if (*p & 0x80) + goto not_ascii; + + pango_glyph_string_set_size(glyphs, len); + + for (i = 0; i < len; ++i) + { + glyphs->glyphs[i] = gui.ascii_glyphs->glyphs[2 * s[i]]; + glyphs->log_clusters[i] = i; + } + + draw_glyph_string(row, col, len, flags, gui.ascii_font, glyphs, cr); + + column_offset = len; + } + else +not_ascii: + { + PangoAttrList *attr_list; + GList *item_list; + int cluster_width; + int last_glyph_rbearing; + int cells = 0; + + // Safety check: pango crashes with invalid utf-8. + if (!utf_valid_string(s, s + len)) + { + column_offset = len; + goto skipitall; + } + + cluster_width = PANGO_SCALE * gui.char_width; + last_glyph_rbearing = PANGO_SCALE * gui.char_width; + + attr_list = pango_attr_list_new(); + + // If 'guifontwide' is set then use that for double-width characters. + if (gui.wide_font != NULL) + apply_wide_font_attr(s, len, attr_list); + + if ((flags & DRAW_BOLD) && gui.font_can_bold) + INSERT_PANGO_ATTR(pango_attr_weight_new(PANGO_WEIGHT_BOLD), + attr_list, 0, len); + if (flags & DRAW_ITALIC) + INSERT_PANGO_ATTR(pango_attr_style_new(PANGO_STYLE_ITALIC), + attr_list, 0, len); + + item_list = pango_itemize(gui.text_context, + (const char *)s, 0, len, attr_list, NULL); + + while (item_list != NULL) + { + PangoItem *item; + int item_cells = 0; + + item = (PangoItem *)item_list->data; + item_list = g_list_delete_link(item_list, item_list); + + // Force LTR direction; Vim handles bidi on its own. + item->analysis.level = (item->analysis.level + 1) & (~1U); + + pango_shape_full((const char *)s + item->offset, item->length, + (const char *)s, len, &item->analysis, glyphs); + + // Fixed-width hack: assign a fixed width to each glyph based on + // the number of cells it occupies, handling composing characters + // and cluster boundaries properly. + for (i = 0; i < glyphs->num_glyphs; ++i) + { + PangoGlyphInfo *glyph; + + glyph = &glyphs->glyphs[i]; + + if (glyph->attr.is_cluster_start) + { + int cellcount; + + cellcount = count_cluster_cells( + s, item, glyphs, i, &cluster_width, + (item_list != NULL) ? &last_glyph_rbearing : NULL); + + if (cellcount > 0) + { + int width; + + width = cellcount * gui.char_width * PANGO_SCALE; + glyph->geometry.x_offset += + MAX(0, width - cluster_width) / 2; + glyph->geometry.width = width; + } + else + { + setup_zero_width_cluster(item, glyph, cells, + cluster_width, + last_glyph_rbearing); + } + + item_cells += cellcount; + cells = cellcount; + } + else if (i > 0) + { + int width; + + if (glyph->geometry.x_offset >= 0) + { + glyphs->glyphs[i].geometry.width = + glyphs->glyphs[i - 1].geometry.width; + glyphs->glyphs[i - 1].geometry.width = 0; + } + width = cells * gui.char_width * PANGO_SCALE; + glyph->geometry.x_offset += + MAX(0, width - cluster_width) / 2; + } + else + { + glyph->geometry.width = 0; + } + } + + draw_glyph_string(row, col + column_offset, item_cells, + flags, item->analysis.font, glyphs, cr); + + pango_item_free(item); + + column_offset += item_cells; + } + + pango_attr_list_unref(attr_list); + } + +skipitall: + draw_under(flags, row, col, column_offset, cr); + + pango_glyph_string_free(glyphs); + + cairo_destroy(cr); + + if (gui.drawarea != NULL) + gtk_widget_queue_draw(gui.drawarea); + + return column_offset; +} + +/* + * Draw a string of characters on the screen using the current font and colors. + * Splits the string into ASCII and ligature/UTF-8 segments so that ligature + * characters are sent through Pango for proper shaping, while plain ASCII + * uses the fast cached glyph path. + * Returns the number of display cells used. + */ + int +gui_gtk_draw_string(int row, int col, char_u *s, int len, int flags) +{ + char_u *conv_buf = NULL; + int convlen; + int len_sum; + int byte_sum; + char_u *cs; + int needs_pango; + int should_need_pango = FALSE; + int slen; + int is_ligature; + int is_utf8; + char_u backup_ch; + + if (gui.text_context == NULL || gui.surface == NULL) + return len; + + if (output_conv.vc_type != CONV_NONE) + { + convlen = len; + conv_buf = string_convert(&output_conv, s, &convlen); + if (conv_buf != NULL) + { + s = conv_buf; + len = convlen; + } + } + + /* + * Ligature support: + * Split the string into segments that are either pure ASCII (fast path) + * or ligature/UTF-8 (Pango path). A single ligature character between + * ASCII characters is treated as ASCII since it can't form a ligature + * on its own. + */ + len_sum = 0; + byte_sum = 0; + cs = s; + + // First char decides starting mode. + is_utf8 = (*cs & 0x80); + is_ligature = gui.ligatures_map[*cs] && (len > 1); + if (is_ligature) + is_ligature = gui.ligatures_map[*(cs + 1)]; + if (!is_utf8 && len > 1) + is_utf8 = (*(cs + 1) & 0x80) != 0; + needs_pango = is_utf8 || is_ligature; + + while (cs < s + len) + { + slen = 0; + while (slen < (len - byte_sum)) + { + is_ligature = gui.ligatures_map[*(cs + slen)]; + // Look ahead: single ligature char between ASCII is ASCII. + if (is_ligature && !needs_pango) + { + if ((slen + 1) < (len - byte_sum)) + is_ligature = gui.ligatures_map[*(cs + slen + 1)]; + else + is_ligature = 0; + } + is_utf8 = *(cs + slen) & 0x80; + // ASCII followed by UTF-8 could be combining. + if ((!is_utf8) && ((slen + 1) < (len - byte_sum))) + is_utf8 = (*(cs + slen + 1) & 0x80); + should_need_pango = (is_ligature || is_utf8); + if (needs_pango != should_need_pango) + break; + if (needs_pango) + { + if (is_ligature) + { + slen++; + } + else + { + if ((*(cs + slen) & 0xC0) == 0x80) + { + while ((slen < (len - byte_sum)) + && ((*(cs + slen) & 0xC0) == 0x80)) + slen++; + } + else if ((*(cs + slen) & 0xE0) == 0xC0) + slen++; + else if ((*(cs + slen) & 0xF0) == 0xE0) + slen += 2; + else if ((*(cs + slen) & 0xF8) == 0xF0) + slen += 3; + else + slen++; + } + } + else + { + slen++; + } + } + + if (slen < len) + { + backup_ch = *(cs + slen); + *(cs + slen) = NUL; + } + len_sum += gui_gtk_draw_string_ext(row, col + len_sum, cs, slen, + flags, needs_pango); + if (slen < len) + *(cs + slen) = backup_ch; + cs += slen; + byte_sum += slen; + needs_pango = should_need_pango; + } + vim_free(conv_buf); + return len_sum; +} + + int +gui_get_x11_windis(Window *win UNUSED, Display **dis UNUSED) +{ + // GTK4: not applicable + return FAIL; +} + +#if defined(FEAT_SOCKETSERVER) + +/* + * Callback for new events from the socket server listening socket. + */ + static int +socket_server_poll_in(int fd UNUSED, GIOCondition cond, + void *user_data UNUSED) +{ + if (cond & G_IO_IN) + socket_server_accept_client(); + else if (cond & (G_IO_ERR | G_IO_HUP)) + { + socket_server_uninit(); + return FALSE; + } + + return TRUE; +} + +#endif // FEAT_SOCKETSERVER + +/* + * Initialize socket server for use in the GUI (does not actually initialize + * the socket server, only attaches a source). + */ + void +gui_gtk_init_socket_server(void) +{ +#if defined(FEAT_SOCKETSERVER) + if (socket_server_source_id > 0) + return; + // Register source for file descriptor to global default context + socket_server_source_id = g_unix_fd_add(socket_server_get_fd(), + G_IO_IN | G_IO_ERR | G_IO_HUP, socket_server_poll_in, NULL); +#endif +} + +/* + * Remove the source for the socket server listening socket. + */ + void +gui_gtk_uninit_socket_server(void) +{ +#if defined(FEAT_SOCKETSERVER) + if (socket_server_source_id > 0) + { + g_source_remove(socket_server_source_id); + socket_server_source_id = 0; + } +#endif +} + + void +gui_gtk_set_mnemonics(int enable UNUSED) +{ + // No-op: menu mnemonics depend on menu system, not yet implemented + // for GTK4. +} + + void +gui_make_popup(char_u *path_name UNUSED, int mouse_pos UNUSED) +{ + // No-op: popup menus depend on menu system, not yet implemented + // for GTK4. +} + + int +get_menu_tool_width(void) +{ + return 0; +} + + int +get_menu_tool_height(void) +{ + int height = 0; + +#ifdef FEAT_MENU + if (gui.menubar != NULL && gtk_widget_get_visible(gui.menubar)) + { + GtkRequisition req; + gtk_widget_get_preferred_size(gui.menubar, &req, NULL); + height += req.height; + } +#endif +#ifdef FEAT_TOOLBAR + if (gui.toolbar != NULL && gtk_widget_get_visible(gui.toolbar)) + { + GtkRequisition req; + gtk_widget_get_preferred_size(gui.toolbar, &req, NULL); + height += req.height; + } +#endif + return height; +} + +/* + * Get the GdkClipboard for the given Clipboard_T. + * clip_star (*) uses PRIMARY, clip_plus (+) uses CLIPBOARD. + */ + static GdkClipboard * +gtk4_get_clipboard(Clipboard_T *cbd) +{ + GdkDisplay *display; + + if (gui.mainwin == NULL) + return NULL; + + display = gtk_widget_get_display(gui.mainwin); + if (display == NULL) + return NULL; + + if (cbd == &clip_plus) + return gdk_display_get_clipboard(display); + else + return gdk_display_get_primary_clipboard(display); +} + +typedef struct { + Clipboard_T *cbd; + gboolean done; +} ClipReadData; + +/* + * Callback for gdk_clipboard_read_text_async(). + */ + static void +clip_read_text_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + GdkClipboard *clipboard = GDK_CLIPBOARD(source); + ClipReadData *crd = (ClipReadData *)user_data; + Clipboard_T *cbd = crd->cbd; + char *text; + GError *error = NULL; + + text = gdk_clipboard_read_text_finish(clipboard, result, &error); + if (text != NULL) + { + char_u *tmpbuf = NULL; + char_u *p; + int len; + int motion_type = MAUTO; + + len = (int)STRLEN(text); + + // Convert from UTF-8 to 'encoding' if needed. + if (input_conv.vc_type != CONV_NONE) + { + tmpbuf = string_convert(&input_conv, (char_u *)text, &len); + if (tmpbuf != NULL) + p = tmpbuf; + else + p = (char_u *)text; + } + else + p = (char_u *)text; + + // Chop off any trailing NUL bytes. + while (len > 0 && p[len - 1] == NUL) + --len; + + clip_yank_selection(motion_type, p, (long)len, cbd); + vim_free(tmpbuf); + g_free(text); + } + else + { + if (error != NULL) + g_error_free(error); + } + crd->done = TRUE; +} + +/* + * Request the selection from the clipboard. + */ + void +clip_mch_request_selection(Clipboard_T *cbd) +{ + GdkClipboard *clipboard; + ClipReadData crd; + time_t start; + + clipboard = gtk4_get_clipboard(cbd); + if (clipboard == NULL) + return; + + crd.cbd = cbd; + crd.done = FALSE; + gdk_clipboard_read_text_async(clipboard, NULL, clip_read_text_cb, &crd); + + // Spin until the async callback fires, with a 3-second wall-clock + // timeout as a safety net. + start = time(NULL); + while (!crd.done && time(NULL) < start + 3) + g_main_context_iteration(NULL, TRUE); +} + +/* + * Send the current selection to the clipboard. + */ + void +clip_mch_set_selection(Clipboard_T *cbd) +{ + GdkClipboard *clipboard; + char_u *str = NULL; + long_u len; + int motion_type; + + clipboard = gtk4_get_clipboard(cbd); + if (clipboard == NULL) + return; + + // Get the selection text from the register. + clip_get_selection(cbd); + motion_type = clip_convert_selection(&str, &len, cbd); + if (motion_type < 0 || str == NULL) + return; + + // Convert from 'encoding' to UTF-8 if needed. + if (output_conv.vc_type != CONV_NONE) + { + char_u *conv_str; + int conv_len = (int)len; + + conv_str = string_convert(&output_conv, str, &conv_len); + if (conv_str != NULL) + { + vim_free(str); + str = conv_str; + len = conv_len; + } + } + + // Ensure NUL-terminated string for GTK. + { + char_u *nul_str = alloc(len + 1); + + if (nul_str != NULL) + { + mch_memmove(nul_str, str, len); + nul_str[len] = NUL; + gdk_clipboard_set_text(clipboard, (const char *)nul_str); + vim_free(nul_str); + } + } + + vim_free(str); +} + +/* + * Own the selection. In GTK4, ownership is implicit when content is set + * on the clipboard. Return OK to indicate we can own it. + */ + int +clip_mch_own_selection(Clipboard_T *cbd UNUSED) +{ + return OK; +} + +/* + * Disown the selection. In GTK4, we clear the clipboard content to + * release ownership. + */ + void +clip_mch_lose_selection(Clipboard_T *cbd) +{ + GdkClipboard *clipboard; + + clipboard = gtk4_get_clipboard(cbd); + if (clipboard == NULL) + return; + + // Setting NULL content provider releases ownership. + gdk_clipboard_set_content(clipboard, NULL); +} + +// Balloon eval - use GTK4 tooltip + void +gui_mch_post_balloon(BalloonEval *beval UNUSED, char_u *mesg) +{ + if (mesg != NULL && gui.drawarea != NULL) + { + char_u *text = CONVERT_TO_UTF8(mesg); + gtk_widget_set_tooltip_text(gui.drawarea, (const char *)text); + CONVERT_TO_UTF8_FREE(text); + } + else if (gui.drawarea != NULL) + gtk_widget_set_tooltip_text(gui.drawarea, NULL); +} + + BalloonEval * +gui_mch_create_beval_area(void *target UNUSED, char_u *mesg UNUSED, + void (*mesgCB)(BalloonEval *, int) UNUSED, void *clientData UNUSED) +{ + return NULL; +} + + void +gui_mch_enable_beval_area(BalloonEval *beval UNUSED) +{ +} + + void +gui_mch_disable_beval_area(BalloonEval *beval UNUSED) +{ +} + +// GTK4 does not have gtk_main_level/gtk_main_quit. +// Provide compatibility stubs using a simple flag. + guint +gtk_main_level(void) +{ + return gtk4_main_loop_level; +} + + void +gtk_main_quit(void) +{ + gtk4_main_loop_quit = TRUE; +} + +#if defined(FEAT_MOUSESHAPE) + +// Table of CSS cursor names corresponding to Vim's mouse shape IDs. +// Keep in sync with the mshape_names[] table in misc2.c. +static const char *mshape_css_names[] = +{ + "default", // arrow + "none", // blank + "text", // beam + "ns-resize", // updown + "nwse-resize", // udsizing + "ew-resize", // leftright + "ew-resize", // lrsizing + "progress", // busy + "not-allowed", // no + "crosshair", // crosshair + "pointer", // hand1 + "pointer", // hand2 + "default", // pencil (no CSS analogue) + "help", // question + "default", // right-arrow (no CSS analogue) + "default", // up-arrow (no CSS analogue) + "default" // last entry +}; + + void +mch_set_mouse_shape(int shape) +{ + GdkCursor *c; + const char *css_name = "default"; + + if (gui.drawarea == NULL) + return; + + if (shape == MSHAPE_HIDE || gui.pointer_hidden) + gtk_widget_set_cursor(gui.drawarea, gui.blank_pointer); + else + { + if (shape >= MSHAPE_NUMBERED) + css_name = "default"; + else if (shape < (int)ARRAY_LENGTH(mshape_css_names)) + css_name = mshape_css_names[shape]; + else + return; + + // GTK4: gdk_cursor_new_from_name(name, fallback) + c = gdk_cursor_new_from_name(css_name, NULL); + gtk_widget_set_cursor(gui.drawarea, c); + g_object_unref(G_OBJECT(c)); + } + if (shape != MSHAPE_HIDE) + last_shape = shape; +} + +#else // !FEAT_MOUSESHAPE + + void +mch_set_mouse_shape(int shape UNUSED) +{ +} + +#endif // FEAT_MOUSESHAPE + + + +/* + * Menus, scrollbars, dialogs, toolbar. + * (merged from gui_gtk4.c) + */ + + + +static int last_text_area_w = 0; +static int last_text_area_h = 0; + +/* + * ============================================================ + * Menu functions + * ============================================================ + * TODO: Implement using GMenu + GtkPopoverMenuBar + */ + +/* + * Icon name table for toolbar buttons. + * Must match toolbar_names[] in menu.c. + */ +static const char * const toolbar_icon_names[] = +{ + /* 00 */ "document-new", + /* 01 */ "document-open", + /* 02 */ "document-save", + /* 03 */ "edit-undo", + /* 04 */ "edit-redo", + /* 05 */ "edit-cut", + /* 06 */ "edit-copy", + /* 07 */ "edit-paste", + /* 08 */ "document-print", + /* 09 */ "help-browser", + /* 10 */ "edit-find", + /* 11 */ "document-save", // save all (no standard icon) + /* 12 */ "document-save", // session save + /* 13 */ "document-new", // session new + /* 14 */ "document-open", // session load + /* 15 */ "system-run", + /* 16 */ "edit-find-replace", + /* 17 */ "window-close", + /* 18 */ "window-maximize-symbolic", // maximize + /* 19 */ "window-minimize-symbolic", // minimize + /* 20 */ "window-maximize-symbolic", // split (no standard icon) + /* 21 */ "utilities-terminal", // shell + /* 22 */ "go-previous", + /* 23 */ "go-next", + /* 24 */ "help-browser", // find help + /* 25 */ "edit-find", // convert (no standard icon) + /* 26 */ "go-jump", + /* 27 */ "go-previous", // back (reuse) + /* 28 */ "go-next", // forward (reuse) + /* 29 */ "image-missing", + /* 30 */ "image-missing", +}; + + static void +toolbar_button_clicked_cb(GtkWidget *widget UNUSED, gpointer data) +{ + gui_menu_cb((vimmenu_T *)data); +} + + static GtkWidget * +create_toolbar_icon(vimmenu_T *menu) +{ + char_u buf[MAXPATHL]; + GtkWidget *image = NULL; + + // Try specified icon file first + if (menu->iconfile != NULL) + { + expand_env(menu->iconfile, buf, MAXPATHL); + if (vim_fexists(buf)) + { + GdkPixbuf *pixbuf = gdk_pixbuf_new_from_file_at_scale( + (const char *)buf, 24, 24, TRUE, NULL); + if (pixbuf != NULL) + { + GdkTexture *texture = + gdk_texture_new_for_pixbuf(pixbuf); + image = gtk_image_new_from_paintable( + GDK_PAINTABLE(texture)); + g_object_unref(texture); + g_object_unref(pixbuf); + } + } + } + + // Use themed icon + if (image == NULL) + { + const char *icon_name = "image-missing"; + int n = (int)ARRAY_LENGTH(toolbar_icon_names); + + if (menu->iconidx >= 0 && menu->iconidx < n) + icon_name = toolbar_icon_names[menu->iconidx]; + + image = gtk_image_new_from_icon_name(icon_name); + } + + return image; +} + +/* + * GTK4 Menu system using GMenu + GSimpleActionGroup + GtkPopoverMenuBar. + * + * Each menu/submenu has a GMenu stored in menu->submenu_id (cast to + * GtkWidget* to fit the struct field type). + * Actions are added to a GSimpleActionGroup attached to gui.mainwin. + */ + +static int menu_action_id = 0; + + static void +menu_action_cb(GSimpleAction *action UNUSED, GVariant *parameter UNUSED, + gpointer data) +{ + // Force-close any open popover menus in the menubar. + // GTK4 marks them as not-visible but Vim's custom main loop + // may not process the rendering update, so we flush explicitly. + if (gui.menubar != NULL) + { + GtkWidget *item; + + for (item = gtk_widget_get_first_child(gui.menubar); + item != NULL; + item = gtk_widget_get_next_sibling(item)) + { + GtkWidget *child; + + for (child = gtk_widget_get_first_child(item); + child != NULL; + child = gtk_widget_get_next_sibling(child)) + { + if (GTK_IS_POPOVER(child)) + gtk_popover_popdown(GTK_POPOVER(child)); + } + } + } + + gui_menu_cb((vimmenu_T *)data); + gui_mch_flush(); +} + + static char * +make_action_name(vimmenu_T *menu) +{ + // Create a unique action name from the menu pointer + static char buf[64]; + vim_snprintf(buf, sizeof(buf), "menu%d", menu_action_id++); + return buf; +} + + void +gui_mch_add_menu(vimmenu_T *menu, int idx UNUSED) +{ + GMenu *submenu; + + if (menu->name[0] == ']' || menu_is_popup(menu->name)) + { + // Popup menus - just create a GMenu, don't add to menubar + submenu = g_menu_new(); + menu->submenu_id = (GtkWidget *)(gpointer)submenu; + return; + } + + if (menu->parent != NULL && menu->parent->submenu_id == NULL) + return; + if (!menu_is_menubar(menu->name)) + return; + + // Create a submenu for this menu + submenu = g_menu_new(); + menu->submenu_id = (GtkWidget *)(gpointer)submenu; + + // Add to parent menu or menubar's model + { + GMenu *parent_menu; + char_u *label; + + label = CONVERT_TO_UTF8(menu->dname); + + if (menu->parent != NULL) + parent_menu = (GMenu *)(gpointer)menu->parent->submenu_id; + else + parent_menu = (GMenu *)(gpointer)g_object_get_data( + G_OBJECT(gui.menubar), "vim-gmenu"); + + if (parent_menu != NULL) + g_menu_append_submenu(parent_menu, (const char *)label, + G_MENU_MODEL(submenu)); + + CONVERT_TO_UTF8_FREE(label); + } +} + + void +gui_mch_add_menu_item(vimmenu_T *menu, int idx UNUSED) +{ + vimmenu_T *parent = menu->parent; + +#ifdef FEAT_TOOLBAR + if (parent != NULL && menu_is_toolbar(parent->name)) + { + if (menu_is_separator(menu->name)) + { + GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_VERTICAL); + gtk_box_append(GTK_BOX(gui.toolbar), sep); + menu->id = sep; + } + else + { + GtkWidget *btn; + GtkWidget *icon; + char_u *tooltip; + + icon = create_toolbar_icon(menu); + btn = gtk_button_new(); + gtk_button_set_child(GTK_BUTTON(btn), icon); + gtk_widget_set_focusable(btn, FALSE); + gtk_widget_add_css_class(btn, "flat"); + + tooltip = CONVERT_TO_UTF8(menu->strings[MENU_INDEX_TIP]); + if (tooltip != NULL && utf_valid_string(tooltip, NULL)) + gtk_widget_set_tooltip_text(btn, (const gchar *)tooltip); + CONVERT_TO_UTF8_FREE(tooltip); + + g_signal_connect(btn, "clicked", + G_CALLBACK(toolbar_button_clicked_cb), menu); + + gtk_box_append(GTK_BOX(gui.toolbar), btn); + menu->id = btn; + } + return; + } +#endif + + // Menu items (non-toolbar) + if (parent == NULL || parent->submenu_id == NULL) + return; + + { + GMenu *parent_menu = (GMenu *)(gpointer)parent->submenu_id; + + if (menu_is_separator(menu->name)) + { + // GMenu doesn't have real separators; use a section + GMenu *section = g_menu_new(); + g_menu_append_section(parent_menu, NULL, G_MENU_MODEL(section)); + g_object_unref(section); + menu->id = NULL; + } + else + { + char *action_name; + char detailed[80]; + char_u *label; + GSimpleAction *action; + + // Create a unique action + action_name = make_action_name(menu); + action = g_simple_action_new(action_name, NULL); + g_signal_connect(action, "activate", + G_CALLBACK(menu_action_cb), menu); + + if (menu_action_group == NULL) + { + menu_action_group = g_simple_action_group_new(); + gtk_widget_insert_action_group(gui.mainwin, "menu", + G_ACTION_GROUP(menu_action_group)); + } + g_action_map_add_action(G_ACTION_MAP(menu_action_group), + G_ACTION(action)); + g_object_unref(action); + + label = CONVERT_TO_UTF8(menu->dname); + vim_snprintf(detailed, sizeof(detailed), "menu.%s", action_name); + g_menu_append(parent_menu, (const char *)label, detailed); + CONVERT_TO_UTF8_FREE(label); + + menu->id = (GtkWidget *)1; // non-NULL marker + // Store action name for later use (grey/enable) + menu->label = (GtkWidget *)vim_strsave( + (char_u *)action_name); + } + } +} + + void +gui_mch_toggle_tearoffs(int enable UNUSED) +{ + // GTK4: tearoff menus don't exist. +} + + void +gui_mch_menu_set_tip(vimmenu_T *menu UNUSED) +{ +} + + void +gui_mch_destroy_menu(vimmenu_T *menu) +{ + // For toolbar buttons, remove from toolbar + if (menu->id != NULL && menu->id != (GtkWidget *)1) + { + GtkWidget *parent_widget = gtk_widget_get_parent(menu->id); + if (parent_widget != NULL) + gtk_box_remove(GTK_BOX(parent_widget), menu->id); + menu->id = NULL; + } + else + menu->id = NULL; + + // Free stored action name + vim_free(menu->label); + menu->label = NULL; + + // GMenu items cannot be individually removed easily. + // The submenu GMenu is unreffed if present. + if (menu->submenu_id != NULL) + { + // Don't unref - GMenu may be referenced by the model + menu->submenu_id = NULL; + } +} + + static void +popupmenu_closed_cb(GtkPopover *popover, gpointer data UNUSED) +{ + gtk_widget_unparent(GTK_WIDGET(popover)); +} + + void +gui_mch_show_popupmenu(vimmenu_T *menu) +{ + GMenu *gmenu; + GtkWidget *popover; + + if (menu == NULL || menu->submenu_id == NULL) + return; + + gmenu = (GMenu *)(gpointer)menu->submenu_id; + popover = gtk_popover_menu_new_from_model(G_MENU_MODEL(gmenu)); + gtk_widget_set_parent(popover, gui.drawarea); + g_signal_connect(popover, "closed", + G_CALLBACK(popupmenu_closed_cb), NULL); + gtk_popover_popup(GTK_POPOVER(popover)); +} + +/* + * ============================================================ + * Scrollbar functions + * ============================================================ + */ + + void +gui_mch_set_scrollbar_thumb(scrollbar_T *sb, long val, long size, long max) +{ + GtkAdjustment *adj; + + if (sb->id == NULL) + return; + if (!GTK_IS_WIDGET(sb->id) || !GTK_IS_RANGE(sb->id)) + return; + + adj = gtk_range_get_adjustment(GTK_RANGE(sb->id)); + gtk_adjustment_set_lower(adj, 0.0); + gtk_adjustment_set_upper(adj, (gdouble)max + 1); + gtk_adjustment_set_value(adj, (gdouble)val); + gtk_adjustment_set_step_increment(adj, 1.0); + gtk_adjustment_set_page_increment(adj, (gdouble)(size > 2 ? size - 2 : 1)); + gtk_adjustment_set_page_size(adj, (gdouble)size); +} + + void +gui_mch_set_scrollbar_pos(scrollbar_T *sb, int x, int y, int w, int h) +{ + if (sb->id != NULL) + { + gtk_widget_set_size_request(sb->id, w, h); + gui_gtk_form_move(GTK_FORM(gui.formwin), sb->id, x, y); + } +} + + int +gui_mch_get_scrollbar_xpadding(void) +{ + int formwin_w = gtk_widget_get_width(gui.formwin); + int sbar_w = 0; + int xpad; + + if (gui.which_scrollbars[SBAR_LEFT]) + sbar_w += gui.scrollbar_width; + if (gui.which_scrollbars[SBAR_RIGHT]) + sbar_w += gui.scrollbar_width; + + xpad = formwin_w - last_text_area_w - sbar_w; + return (xpad < 0) ? 0 : xpad; +} + + int +gui_mch_get_scrollbar_ypadding(void) +{ + int formwin_h = gtk_widget_get_height(gui.formwin); + int ypad; + + ypad = formwin_h - last_text_area_h; + if (gui.which_scrollbars[SBAR_BOTTOM]) + ypad -= gui.scrollbar_height; + + return (ypad < 0) ? 0 : ypad; +} + + static void +adjustment_value_changed(GtkAdjustment *adj, gpointer data UNUSED) +{ + scrollbar_T *sb = (scrollbar_T *)g_object_get_data(G_OBJECT(adj), "vim-sb"); + long value = (long)gtk_adjustment_get_value(adj); + + if (sb != NULL) + gui_drag_scrollbar(sb, value, FALSE); +} + + void +gui_mch_create_scrollbar(scrollbar_T *sb, int orient) +{ + if (orient == SBAR_HORIZ) + sb->id = gtk_scrollbar_new(GTK_ORIENTATION_HORIZONTAL, NULL); + else + sb->id = gtk_scrollbar_new(GTK_ORIENTATION_VERTICAL, NULL); + + if (sb->id != NULL && GTK_IS_RANGE(sb->id)) + { + GtkAdjustment *adj = gtk_range_get_adjustment(GTK_RANGE(sb->id)); + + gtk_widget_set_visible(sb->id, FALSE); + gui_gtk_form_put(GTK_FORM(gui.formwin), sb->id, 0, 0); + if (adj != NULL && G_IS_OBJECT(adj)) + { + g_object_set_data(G_OBJECT(adj), "vim-sb", (gpointer)sb); + g_signal_connect(G_OBJECT(adj), "value-changed", + G_CALLBACK(adjustment_value_changed), NULL); + } + } +} + + void +gui_mch_destroy_scrollbar(scrollbar_T *sb) +{ + if (sb->id != NULL) + { + gui_gtk_form_remove(GTK_FORM(gui.formwin), sb->id); + sb->id = NULL; + } +} + +/* + * ============================================================ + * Text area position + * ============================================================ + */ + + void +gui_mch_set_text_area_pos(int x, int y, int w, int h) +{ + last_text_area_w = w; + last_text_area_h = h; + // Don't use gui_gtk_form_move_resize for drawarea because its + // set_size_request would prevent the window from shrinking. + // Just update position; the actual allocation is handled by + // form_size_allocate which gives drawarea the formwin's full size. + gui_gtk_form_move(GTK_FORM(gui.formwin), gui.drawarea, x, y); + + // Update surface to match new text area size + if (w > 0 && h > 0) + { + if (gui.surface != NULL) + { + int sw = cairo_image_surface_get_width(gui.surface); + int sh = cairo_image_surface_get_height(gui.surface); + if (sw == w && sh == h) + return; + cairo_surface_destroy(gui.surface); + } + gui.surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); + } +} + +/* + * ============================================================ + * Browse dialogs + * ============================================================ + */ + +/* + * Blocking helper: run a GtkFileDialog and wait for result. + */ +typedef struct { + GFile *result; + gboolean done; +} FileDialogData; + + static void +file_dialog_open_cb(GObject *source, GAsyncResult *res, gpointer data) +{ + FileDialogData *fdd = (FileDialogData *)data; + fdd->result = gtk_file_dialog_open_finish( + GTK_FILE_DIALOG(source), res, NULL); + fdd->done = TRUE; +} + + static void +file_dialog_save_cb(GObject *source, GAsyncResult *res, gpointer data) +{ + FileDialogData *fdd = (FileDialogData *)data; + fdd->result = gtk_file_dialog_save_finish( + GTK_FILE_DIALOG(source), res, NULL); + fdd->done = TRUE; +} + + static void +file_dialog_folder_cb(GObject *source, GAsyncResult *res, gpointer data) +{ + FileDialogData *fdd = (FileDialogData *)data; + fdd->result = gtk_file_dialog_select_folder_finish( + GTK_FILE_DIALOG(source), res, NULL); + fdd->done = TRUE; +} + + char_u * +gui_mch_browse(int saving, + char_u *title, + char_u *dflt, + char_u *ext UNUSED, + char_u *initdir, + char_u *filter UNUSED) +{ + GtkFileDialog *dlg; + FileDialogData fdd; + char_u dirbuf[MAXPATHL]; + char_u *result = NULL; + + title = CONVERT_TO_UTF8(title); + + if (initdir == NULL || *initdir == NUL) + mch_dirname(dirbuf, MAXPATHL); + else if (vim_FullName(initdir, dirbuf, MAXPATHL - 2, FALSE) == FAIL) + dirbuf[0] = NUL; + add_pathsep(dirbuf); + + gui_mch_mousehide(FALSE); + + dlg = gtk_file_dialog_new(); + gtk_file_dialog_set_modal(dlg, TRUE); + if (title != NULL) + gtk_file_dialog_set_title(dlg, (const char *)title); + + { + GFile *dir = g_file_new_for_path((const char *)dirbuf); + gtk_file_dialog_set_initial_folder(dlg, dir); + g_object_unref(dir); + } + + if (saving && dflt != NULL && *dflt != NUL) + gtk_file_dialog_set_initial_name(dlg, (const char *)dflt); + + fdd.result = NULL; + fdd.done = FALSE; + + if (saving) + gtk_file_dialog_save(dlg, GTK_WINDOW(gui.mainwin), NULL, + file_dialog_save_cb, &fdd); + else + gtk_file_dialog_open(dlg, GTK_WINDOW(gui.mainwin), NULL, + file_dialog_open_cb, &fdd); + + while (!fdd.done) + g_main_context_iteration(NULL, TRUE); + + if (fdd.result != NULL) + { + char *path = g_file_get_path(fdd.result); + if (path != NULL) + { + result = vim_strsave((char_u *)path); + g_free(path); + } + g_object_unref(fdd.result); + } + + g_object_unref(dlg); + CONVERT_TO_UTF8_FREE(title); + + return result; +} + + char_u * +gui_mch_browsedir(char_u *title, char_u *initdir) +{ + GtkFileDialog *dlg; + FileDialogData fdd; + char_u *result = NULL; + + title = CONVERT_TO_UTF8(title); + gui_mch_mousehide(FALSE); + + dlg = gtk_file_dialog_new(); + gtk_file_dialog_set_modal(dlg, TRUE); + if (title != NULL) + gtk_file_dialog_set_title(dlg, (const char *)title); + + if (initdir != NULL && *initdir != NUL) + { + GFile *dir = g_file_new_for_path((const char *)initdir); + gtk_file_dialog_set_initial_folder(dlg, dir); + g_object_unref(dir); + } + + fdd.result = NULL; + fdd.done = FALSE; + + gtk_file_dialog_select_folder(dlg, GTK_WINDOW(gui.mainwin), NULL, + file_dialog_folder_cb, &fdd); + + while (!fdd.done) + g_main_context_iteration(NULL, TRUE); + + if (fdd.result != NULL) + { + char *path = g_file_get_path(fdd.result); + if (path != NULL) + { + result = vim_strsave((char_u *)path); + g_free(path); + } + g_object_unref(fdd.result); + } + + g_object_unref(dlg); + CONVERT_TO_UTF8_FREE(title); + + return result; +} + +/* + * ============================================================ + * Message dialog + * ============================================================ + */ + +typedef struct { + int response; + gboolean done; +} AlertDialogData; + + static void +alert_dialog_cb(GObject *source, GAsyncResult *res, gpointer data) +{ + AlertDialogData *add = (AlertDialogData *)data; + add->response = gtk_alert_dialog_choose_finish( + GTK_ALERT_DIALOG(source), res, NULL); + add->done = TRUE; +} + + int +gui_mch_dialog( + int type UNUSED, + char_u *title, + char_u *message, + char_u *buttons, + int dfltbutton, + char_u *textfield UNUSED, + int ex_cmd UNUSED) +{ + GtkAlertDialog *dlg; + AlertDialogData add; + char_u *p; + char_u *buf = NULL; + int butcount = 0; + int i; + const char *btn_labels[64]; + char_u *btn_conv[64]; + + title = CONVERT_TO_UTF8(title); + message = CONVERT_TO_UTF8(message); + + // Parse button labels from the "&Yes\n&No\n&Cancel" format + if (buttons != NULL) + { + buf = vim_strsave(buttons); + if (buf != NULL) + { + p = buf; + while (*p != NUL && butcount < 63) + { + char_u *start = p; + while (*p != NUL && *p != '\n') + ++p; + if (*p == '\n') + *p++ = NUL; + // Skip '&' mnemonic marker + if (*start == '&') + ++start; + btn_conv[butcount] = CONVERT_TO_UTF8(start); + btn_labels[butcount] = (const char *)btn_conv[butcount]; + butcount++; + } + } + } + btn_labels[butcount] = NULL; + + dlg = gtk_alert_dialog_new("%s", message ? (char *)message : ""); + if (title != NULL) + gtk_alert_dialog_set_detail(dlg, (const char *)title); + gtk_alert_dialog_set_buttons(dlg, btn_labels); + gtk_alert_dialog_set_modal(dlg, TRUE); + + if (dfltbutton > 0 && dfltbutton <= butcount) + gtk_alert_dialog_set_default_button(dlg, dfltbutton - 1); + if (butcount > 0) + gtk_alert_dialog_set_cancel_button(dlg, butcount - 1); + + add.response = -1; + add.done = FALSE; + + gtk_alert_dialog_choose(dlg, GTK_WINDOW(gui.mainwin), NULL, + alert_dialog_cb, &add); + + while (!add.done) + g_main_context_iteration(NULL, TRUE); + + g_object_unref(dlg); + + for (i = 0; i < butcount; i++) + CONVERT_TO_UTF8_FREE(btn_conv[i]); + vim_free(buf); + CONVERT_TO_UTF8_FREE(title); + CONVERT_TO_UTF8_FREE(message); + + // GTK returns 0-based index, Vim wants 1-based + return add.response >= 0 ? add.response + 1 : 0; +} + +/* + * ============================================================ + * Find/Replace dialogs + * ============================================================ + */ + +/* + * ============================================================ + * Find/Replace dialog + * ============================================================ + */ + +typedef struct +{ + GtkWidget *dialog; + GtkWidget *what; // Find what entry + GtkWidget *with; // Replace with entry + GtkWidget *wword; // Whole word check + GtkWidget *mcase; // Match case check + GtkWidget *up; // Direction up radio + GtkWidget *down; // Direction down radio +} SharedFindReplace; + +static SharedFindReplace find_widgets = {0}; +static SharedFindReplace repl_widgets = {0}; + + static void +find_replace_cb(GtkWidget *widget UNUSED, gpointer data) +{ + int flags; + char_u *find_text; + char_u *repl_text; + gboolean direction_down; + SharedFindReplace *sfr; + + flags = GPOINTER_TO_INT(data); + + if (flags == FRD_FINDNEXT) + { + repl_text = NULL; + sfr = &find_widgets; + } + else + { + repl_text = (char_u *)gtk_editable_get_text( + GTK_EDITABLE(repl_widgets.with)); + sfr = &repl_widgets; + } + + find_text = (char_u *)gtk_editable_get_text(GTK_EDITABLE(sfr->what)); + direction_down = gtk_check_button_get_active( + GTK_CHECK_BUTTON(sfr->down)); + + if (gtk_check_button_get_active(GTK_CHECK_BUTTON(sfr->wword))) + flags |= FRD_WHOLE_WORD; + if (gtk_check_button_get_active(GTK_CHECK_BUTTON(sfr->mcase))) + flags |= FRD_MATCH_CASE; + + repl_text = CONVERT_FROM_UTF8(repl_text); + find_text = CONVERT_FROM_UTF8(find_text); + gui_do_findrepl(flags, find_text, repl_text, direction_down); + CONVERT_FROM_UTF8_FREE(repl_text); + CONVERT_FROM_UTF8_FREE(find_text); +} + + static void +dialog_destroyed_cb(GtkWidget *widget UNUSED, gpointer data) +{ + *(GtkWidget **)data = NULL; +} + + static void +find_replace_dialog_create(char_u *arg, int do_replace) +{ + SharedFindReplace *frdp; + char_u *entry_text; + int wword = FALSE; + int mcase = !p_ic; + GtkWidget *vbox, *grid, *hbox, *tmp, *btn; + gboolean sensitive; + + frdp = do_replace ? &repl_widgets : &find_widgets; + entry_text = get_find_dialog_text(arg, &wword, &mcase); + + if (entry_text != NULL && output_conv.vc_type != CONV_NONE) + { + char_u *old = entry_text; + entry_text = string_convert(&output_conv, entry_text, NULL); + vim_free(old); + } + + // If the dialog already exists, just raise it. + if (frdp->dialog) + { + if (entry_text != NULL) + { + gtk_editable_set_text(GTK_EDITABLE(frdp->what), + (char *)entry_text); + gtk_check_button_set_active(GTK_CHECK_BUTTON(frdp->wword), + (gboolean)wword); + gtk_check_button_set_active(GTK_CHECK_BUTTON(frdp->mcase), + (gboolean)mcase); + } + gtk_window_present(GTK_WINDOW(frdp->dialog)); + gtk_widget_grab_focus(frdp->what); + vim_free(entry_text); + return; + } + + // Create a new dialog window. + frdp->dialog = gtk_window_new(); + gtk_window_set_transient_for(GTK_WINDOW(frdp->dialog), + GTK_WINDOW(gui.mainwin)); + gtk_window_set_destroy_with_parent(GTK_WINDOW(frdp->dialog), TRUE); + gtk_window_set_title(GTK_WINDOW(frdp->dialog), + do_replace ? _("VIM - Search and Replace...") + : _("VIM - Search...")); + gtk_window_set_resizable(GTK_WINDOW(frdp->dialog), FALSE); + + g_signal_connect(frdp->dialog, "destroy", + G_CALLBACK(dialog_destroyed_cb), &frdp->dialog); + + vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); + gtk_widget_set_margin_start(vbox, 12); + gtk_widget_set_margin_end(vbox, 12); + gtk_widget_set_margin_top(vbox, 12); + gtk_widget_set_margin_bottom(vbox, 12); + gtk_window_set_child(GTK_WINDOW(frdp->dialog), vbox); + + // Grid for labels + entries + grid = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(grid), 6); + gtk_grid_set_column_spacing(GTK_GRID(grid), 6); + gtk_box_append(GTK_BOX(vbox), grid); + + // "Find what:" label + entry + tmp = gtk_label_new(_("Find what:")); + gtk_label_set_xalign(GTK_LABEL(tmp), 0.0); + gtk_grid_attach(GTK_GRID(grid), tmp, 0, 0, 1, 1); + + frdp->what = gtk_entry_new(); + gtk_widget_set_hexpand(frdp->what, TRUE); + sensitive = (entry_text != NULL && entry_text[0] != NUL); + if (entry_text != NULL) + gtk_editable_set_text(GTK_EDITABLE(frdp->what), (char *)entry_text); + gtk_grid_attach(GTK_GRID(grid), frdp->what, 1, 0, 1, 1); + + if (do_replace) + { + // "Replace with:" label + entry + tmp = gtk_label_new(_("Replace with:")); + gtk_label_set_xalign(GTK_LABEL(tmp), 0.0); + gtk_grid_attach(GTK_GRID(grid), tmp, 0, 1, 1, 1); + + frdp->with = gtk_entry_new(); + gtk_widget_set_hexpand(frdp->with, TRUE); + gtk_grid_attach(GTK_GRID(grid), frdp->with, 1, 1, 1, 1); + } + + // Checkboxes + hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12); + gtk_box_append(GTK_BOX(vbox), hbox); + + frdp->wword = gtk_check_button_new_with_label(_("Match whole word only")); + gtk_check_button_set_active(GTK_CHECK_BUTTON(frdp->wword), + (gboolean)wword); + gtk_box_append(GTK_BOX(hbox), frdp->wword); + + frdp->mcase = gtk_check_button_new_with_label(_("Match case")); + gtk_check_button_set_active(GTK_CHECK_BUTTON(frdp->mcase), + (gboolean)mcase); + gtk_box_append(GTK_BOX(hbox), frdp->mcase); + + // Direction radio buttons + hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12); + gtk_box_append(GTK_BOX(vbox), hbox); + + tmp = gtk_label_new(_("Direction:")); + gtk_box_append(GTK_BOX(hbox), tmp); + + frdp->up = gtk_check_button_new_with_label(_("Up")); + gtk_box_append(GTK_BOX(hbox), frdp->up); + + frdp->down = gtk_check_button_new_with_label(_("Down")); + gtk_check_button_set_group(GTK_CHECK_BUTTON(frdp->down), + GTK_CHECK_BUTTON(frdp->up)); + gtk_check_button_set_active(GTK_CHECK_BUTTON(frdp->down), TRUE); + gtk_box_append(GTK_BOX(hbox), frdp->down); + + // Action buttons + hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + gtk_widget_set_halign(hbox, GTK_ALIGN_END); + gtk_box_append(GTK_BOX(vbox), hbox); + + btn = gtk_button_new_with_label(_("Find Next")); + gtk_widget_set_sensitive(btn, sensitive); + g_signal_connect(btn, "clicked", G_CALLBACK(find_replace_cb), + GINT_TO_POINTER(do_replace ? FRD_R_FINDNEXT : FRD_FINDNEXT)); + gtk_box_append(GTK_BOX(hbox), btn); + + if (do_replace) + { + btn = gtk_button_new_with_label(_("Replace")); + g_signal_connect(btn, "clicked", G_CALLBACK(find_replace_cb), + GINT_TO_POINTER(FRD_REPLACE)); + gtk_box_append(GTK_BOX(hbox), btn); + + btn = gtk_button_new_with_label(_("Replace All")); + g_signal_connect(btn, "clicked", G_CALLBACK(find_replace_cb), + GINT_TO_POINTER(FRD_REPLACEALL)); + gtk_box_append(GTK_BOX(hbox), btn); + } + + btn = gtk_button_new_with_label(_("Close")); + g_signal_connect_swapped(btn, "clicked", + G_CALLBACK(gtk_window_destroy), frdp->dialog); + gtk_box_append(GTK_BOX(hbox), btn); + + // Connect Enter key in entry to Find Next + g_signal_connect_swapped(frdp->what, "activate", + G_CALLBACK(find_replace_cb), + GINT_TO_POINTER(do_replace ? FRD_R_FINDNEXT : FRD_FINDNEXT)); + + gtk_window_present(GTK_WINDOW(frdp->dialog)); + gtk_widget_grab_focus(frdp->what); + if (do_replace && entry_text != NULL && entry_text[0] != NUL) + gtk_widget_grab_focus(frdp->with); + + vim_free(entry_text); +} + + void +gui_mch_find_dialog(exarg_T *eap) +{ + if (gui.in_use) + find_replace_dialog_create(eap->arg, FALSE); +} + + void +gui_mch_replace_dialog(exarg_T *eap) +{ + if (gui.in_use) + find_replace_dialog_create(eap->arg, TRUE); +} + +/* + * ============================================================ + * Help find (for :helpfind command) + * ============================================================ + */ + + void +ex_helpfind(exarg_T *eap UNUSED) +{ + do_cmdline_cmd((char_u *)"emenu ToolBar.FindHelp"); +} + + +/* + * ============================================================ + * Printing with GtkPrintOperation + * ============================================================ + */ +#ifdef FEAT_GUI_GTK_PRINT + +typedef struct +{ + linenr_T first_line; // first line to print (from range) + linenr_T last_line; // last line to print (from range) + int n_pages; // total number of pages + int lines_per_page; // lines that fit on one page + PangoFontDescription *font_desc; + int do_syntax; // whether to use syntax highlighting + double line_height; // line height in points + double char_width; // character width in points +} print_data_T; + +/* + * "begin-print" signal handler. + * Calculate pagination based on page size and font metrics. + */ + static void +print_begin_cb( + GtkPrintOperation *op, + GtkPrintContext *context, + gpointer user_data) +{ + print_data_T *pd = (print_data_T *)user_data; + PangoLayout *layout; + PangoFontMetrics *metrics; + double page_height; + int total_lines; + + page_height = gtk_print_context_get_height(context); + + // Create a PangoLayout to measure font metrics on the print surface. + layout = gtk_print_context_create_pango_layout(context); + pango_layout_set_font_description(layout, pd->font_desc); + + metrics = pango_context_get_metrics( + pango_layout_get_context(layout), + pd->font_desc, NULL); + + pd->line_height = (double)(pango_font_metrics_get_ascent(metrics) + + pango_font_metrics_get_descent(metrics)) / PANGO_SCALE; + pd->char_width = (double)pango_font_metrics_get_approximate_char_width( + metrics) / PANGO_SCALE; + + pango_font_metrics_unref(metrics); + g_object_unref(layout); + + if (pd->line_height <= 0) + pd->line_height = 12.0; + + pd->lines_per_page = (int)(page_height / pd->line_height); + if (pd->lines_per_page <= 0) + pd->lines_per_page = 1; + + total_lines = (int)(pd->last_line - pd->first_line + 1); + pd->n_pages = (total_lines + pd->lines_per_page - 1) / pd->lines_per_page; + if (pd->n_pages <= 0) + pd->n_pages = 1; + + gtk_print_operation_set_n_pages(op, pd->n_pages); +} + +/* + * "draw-page" signal handler. + * Render one page of buffer text with optional syntax highlighting. + */ + static void +print_draw_page_cb( + GtkPrintOperation *op UNUSED, + GtkPrintContext *context, + int page_nr, + gpointer user_data) +{ + print_data_T *pd = (print_data_T *)user_data; + cairo_t *cr; + linenr_T lnum; + linenr_T first; + linenr_T last; + int page_line; + double y; + + cr = gtk_print_context_get_cairo_context(context); + + first = pd->first_line + (linenr_T)page_nr * pd->lines_per_page; + last = first + pd->lines_per_page - 1; + if (last > pd->last_line) + last = pd->last_line; + + y = 0; + page_line = 0; + + for (lnum = first; lnum <= last; ++lnum, ++page_line) + { + char_u *line; + PangoLayout *layout; + PangoAttrList *attr_list; + + line = ml_get(lnum); + layout = gtk_print_context_create_pango_layout(context); + pango_layout_set_font_description(layout, pd->font_desc); + + attr_list = pango_attr_list_new(); + +# ifdef FEAT_SYN_HL + if (pd->do_syntax && syntax_present(curwin)) + { + colnr_T col; + int prev_syn_id = -1; + int attr_start = 0; + long_u prev_fg = 0; + int prev_bold = FALSE; + int prev_italic = FALSE; + int len = (int)STRLEN(line); + + for (col = 0; col < len; ) + { + int id; + int outputlen; + long_u fg_color; + int is_bold; + int is_italic; + + if (has_mbyte) + { + outputlen = (*mb_ptr2len)(line + col); + if (outputlen < 1) + outputlen = 1; + } + else + outputlen = 1; + + id = syn_get_id(curwin, lnum, col, 1, NULL, FALSE); + if (id > 0) + id = syn_get_final_id(id); + else + id = 0; + // syn_get_id may invalidate the line pointer. + line = ml_get(lnum); + + fg_color = highlight_gui_color_rgb(id, TRUE); + is_bold = (highlight_has_attr(id, HL_BOLD, 'g') != NULL); + is_italic = (highlight_has_attr(id, HL_ITALIC, 'g') != NULL); + + // When attributes change, flush the previous run. + if (id != prev_syn_id && col > 0) + { + if (prev_fg != 0 && prev_fg != (long_u)0xffffffL) + { + PangoAttribute *a = pango_attr_foreground_new( + (guint16)(((prev_fg >> 16) & 0xff) * 257), + (guint16)(((prev_fg >> 8) & 0xff) * 257), + (guint16)((prev_fg & 0xff) * 257)); + a->start_index = attr_start; + a->end_index = col; + pango_attr_list_insert(attr_list, a); + } + if (prev_bold) + { + PangoAttribute *a = pango_attr_weight_new( + PANGO_WEIGHT_BOLD); + a->start_index = attr_start; + a->end_index = col; + pango_attr_list_insert(attr_list, a); + } + if (prev_italic) + { + PangoAttribute *a = pango_attr_style_new( + PANGO_STYLE_ITALIC); + a->start_index = attr_start; + a->end_index = col; + pango_attr_list_insert(attr_list, a); + } + attr_start = col; + } + + prev_syn_id = id; + prev_fg = fg_color; + prev_bold = is_bold; + prev_italic = is_italic; + + col += outputlen; + } + + // Flush the last run. + if (attr_start < len) + { + if (prev_fg != 0 && prev_fg != (long_u)0xffffffL) + { + PangoAttribute *a = pango_attr_foreground_new( + (guint16)(((prev_fg >> 16) & 0xff) * 257), + (guint16)(((prev_fg >> 8) & 0xff) * 257), + (guint16)((prev_fg & 0xff) * 257)); + a->start_index = attr_start; + a->end_index = len; + pango_attr_list_insert(attr_list, a); + } + if (prev_bold) + { + PangoAttribute *a = pango_attr_weight_new( + PANGO_WEIGHT_BOLD); + a->start_index = attr_start; + a->end_index = len; + pango_attr_list_insert(attr_list, a); + } + if (prev_italic) + { + PangoAttribute *a = pango_attr_style_new( + PANGO_STYLE_ITALIC); + a->start_index = attr_start; + a->end_index = len; + pango_attr_list_insert(attr_list, a); + } + } + } +# endif // FEAT_SYN_HL + + pango_layout_set_attributes(layout, attr_list); + + // Expand tabs. Use a tab array matching Vim's tabstop. + { + PangoTabArray *tabs; + int tab_width = (int)(curbuf->b_p_ts * pd->char_width); + + if (tab_width <= 0) + tab_width = (int)(8 * pd->char_width); + tabs = pango_tab_array_new(1, TRUE); + pango_tab_array_set_tab(tabs, 0, PANGO_TAB_LEFT, tab_width); + pango_layout_set_tabs(layout, tabs); + pango_tab_array_free(tabs); + } + + pango_layout_set_text(layout, (const char *)line, -1); + + cairo_move_to(cr, 0, y); + pango_cairo_show_layout(cr, layout); + + pango_attr_list_unref(attr_list); + g_object_unref(layout); + + y += pd->line_height; + } +} + +/* + * Main entry point for GTK4 native printing. + * Called from ex_hardcopy() when running in a GTK4 GUI. + */ + void +gui_gtk4_hardcopy(exarg_T *eap) +{ + GtkPrintOperation *op; + GtkPrintOperationResult res; + print_data_T pd; + char_u *font_name; + + static GtkPrintSettings *settings = NULL; + + CLEAR_FIELD(pd); + pd.first_line = eap->line1; + pd.last_line = eap->line2; + + // Use 'printfont' if set, otherwise fall back to 'guifont'. + font_name = *p_pfn != NUL ? p_pfn : p_guifont; + if (font_name == NULL || *font_name == NUL) + font_name = (char_u *)"Monospace 10"; + + pd.font_desc = pango_font_description_from_string((const char *)font_name); + if (pd.font_desc == NULL) + { + semsg(_(e_unknown_font_str), font_name); + return; + } + + // Ensure the font description has a size (default 10pt if missing). + if (pango_font_description_get_size(pd.font_desc) == 0) + pango_font_description_set_size(pd.font_desc, 10 * PANGO_SCALE); + +# ifdef FEAT_SYN_HL + pd.do_syntax = syntax_present(curwin); +# endif + + op = gtk_print_operation_new(); + + if (settings != NULL) + gtk_print_operation_set_print_settings(op, settings); + + gtk_print_operation_set_job_name(op, + curbuf->b_fname != NULL + ? (const char *)curbuf->b_fname : "Vim"); + gtk_print_operation_set_show_progress(op, TRUE); + gtk_print_operation_set_unit(op, GTK_UNIT_POINTS); + + g_signal_connect(op, "begin-print", G_CALLBACK(print_begin_cb), &pd); + g_signal_connect(op, "draw-page", G_CALLBACK(print_draw_page_cb), &pd); + + res = gtk_print_operation_run(op, + GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG, + GTK_WINDOW(gui.mainwin), NULL); + + if (res == GTK_PRINT_OPERATION_RESULT_APPLY) + { + if (settings != NULL) + g_object_unref(settings); + settings = g_object_ref( + gtk_print_operation_get_print_settings(op)); + } + + g_object_unref(op); + pango_font_description_free(pd.font_desc); +} + +#endif // FEAT_GUI_GTK_PRINT + +#endif // FEAT_GUI_GTK diff --git a/src/gui_gtk4_f.c b/src/gui_gtk4_f.c new file mode 100644 index 0000000000..759f1065b9 --- /dev/null +++ b/src/gui_gtk4_f.c @@ -0,0 +1,324 @@ +/* vi:set ts=8 sts=4 sw=4 noet: + * + * VIM - Vi IMproved by Bram Moolenaar + * + * Do ":help uganda" in Vim to read copying and usage conditions. + * Do ":help credits" in Vim to see a list of people who contributed. + * See README.txt for an overview of the Vim source code. + * + * GTK4 GtkForm widget - a simple container for absolute child positioning. + * This is a clean rewrite of gui_gtk_f.c for GTK4. + * + * In GTK4, widgets no longer have their own GdkWindows (now GdkSurface), + * GtkContainer is removed, and child positioning uses GskTransform via + * gtk_widget_allocate(). This makes the form widget much simpler. + */ + +#include "vim.h" +#include +#include "gui_gtk4_f.h" + +typedef struct _GtkFormChild GtkFormChild; + +struct _GtkFormChild +{ + GtkWidget *widget; + gint x; + gint y; +}; + +// Forward declarations +static void gui_gtk_form_class_init(GtkFormClass *klass); +static void gui_gtk_form_init(GtkForm *form); +static void form_measure(GtkWidget *widget, GtkOrientation orientation, + int for_size, int *minimum, int *natural, + int *minimum_baseline, int *natural_baseline); +static void form_size_allocate(GtkWidget *widget, int width, int height, + int baseline); +static void form_snapshot(GtkWidget *widget, GtkSnapshot *snapshot); +static void form_dispose(GObject *object); +static void form_position_child(GtkForm *form, GtkFormChild *child, + gboolean force_allocate); + +G_DEFINE_TYPE(GtkForm, gui_gtk_form, GTK_TYPE_WIDGET) + +// Public interface + + GtkWidget * +gui_gtk_form_new(void) +{ + return GTK_WIDGET(g_object_new(GTK_TYPE_FORM, NULL)); +} + + void +gui_gtk_form_put( + GtkForm *form, + GtkWidget *child_widget, + gint x, + gint y) +{ + GtkFormChild *child; + + g_return_if_fail(GTK_IS_FORM(form)); + + child = g_new(GtkFormChild, 1); + if (child == NULL) + return; + + child->widget = child_widget; + child->x = x; + child->y = y; + + gtk_widget_set_size_request(child->widget, -1, -1); + + form->children = g_list_append(form->children, child); + + gtk_widget_set_parent(child_widget, GTK_WIDGET(form)); + form_position_child(form, child, TRUE); +} + + void +gui_gtk_form_move( + GtkForm *form, + GtkWidget *child_widget, + gint x, + gint y) +{ + GList *tmp_list; + + g_return_if_fail(GTK_IS_FORM(form)); + + for (tmp_list = form->children; tmp_list; tmp_list = tmp_list->next) + { + GtkFormChild *child = tmp_list->data; + if (child->widget == child_widget) + { + child->x = x; + child->y = y; + form_position_child(form, child, TRUE); + return; + } + } +} + + void +gui_gtk_form_move_resize( + GtkForm *form, + GtkWidget *widget, + gint x, + gint y, + gint w, + gint h) +{ + gtk_widget_set_size_request(widget, w, h); + gui_gtk_form_move(form, widget, x, y); +} + + void +gui_gtk_form_remove(GtkForm *form, GtkWidget *child_widget) +{ + GList *tmp_list; + + g_return_if_fail(GTK_IS_FORM(form)); + + for (tmp_list = form->children; tmp_list; tmp_list = tmp_list->next) + { + GtkFormChild *child = tmp_list->data; + if (child->widget == child_widget) + { + form->children = g_list_remove_link(form->children, tmp_list); + g_list_free_1(tmp_list); + gtk_widget_unparent(child_widget); + g_free(child); + return; + } + } +} + + void +gui_gtk_form_freeze(GtkForm *form) +{ + g_return_if_fail(GTK_IS_FORM(form)); + ++form->freeze_count; +} + + void +gui_gtk_form_thaw(GtkForm *form) +{ + g_return_if_fail(GTK_IS_FORM(form)); + + if (!form->freeze_count) + return; + + if (!(--form->freeze_count)) + { + GList *tmp_list; + + for (tmp_list = form->children; tmp_list; tmp_list = tmp_list->next) + form_position_child(form, tmp_list->data, FALSE); + gtk_widget_queue_draw(GTK_WIDGET(form)); + } +} + +// GObject/GtkWidget class implementation + + static void +gui_gtk_form_class_init(GtkFormClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + + gobject_class->dispose = form_dispose; + + widget_class->measure = form_measure; + widget_class->size_allocate = form_size_allocate; + widget_class->snapshot = form_snapshot; +} + + static void +gui_gtk_form_init(GtkForm *form) +{ + form->children = NULL; + form->freeze_count = 0; +} + + static void +form_measure( + GtkWidget *widget UNUSED, + GtkOrientation orientation UNUSED, + int for_size UNUSED, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + *minimum = 1; + *natural = 1; + *minimum_baseline = -1; + *natural_baseline = -1; +} + +static guint form_resize_idle_id = 0; +static int form_last_width = 0; +static int form_last_height = 0; + + static gboolean +form_resize_idle_cb(gpointer data UNUSED) +{ + int w, h; + + form_resize_idle_id = 0; + + // Use drawarea's actual allocation, not formwin's + if (gui.drawarea == NULL) + return FALSE; + w = gtk_widget_get_width(gui.drawarea); + h = gtk_widget_get_height(gui.drawarea); + + if (w > 1 && h > 1) + gui_resize_shell(w, h); + + return FALSE; +} + + static void +form_size_allocate(GtkWidget *widget, int width, int height, + int baseline UNUSED) +{ + GtkForm *form; + GList *tmp_list; + + g_return_if_fail(GTK_IS_FORM(widget)); + + form = GTK_FORM(widget); + + for (tmp_list = form->children; tmp_list; tmp_list = tmp_list->next) + form_position_child(form, tmp_list->data, TRUE); + + // Notify Vim about size change via idle callback + if (width != form_last_width || height != form_last_height) + { + form_last_width = width; + form_last_height = height; + if (form_resize_idle_id == 0) + form_resize_idle_id = g_idle_add(form_resize_idle_cb, NULL); + } +} + + static void +form_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) +{ + GtkForm *form; + GList *tmp_list; + + g_return_if_fail(GTK_IS_FORM(widget)); + + form = GTK_FORM(widget); + + for (tmp_list = form->children; tmp_list; tmp_list = tmp_list->next) + { + GtkFormChild *child = tmp_list->data; + if (child->widget != NULL + && GTK_IS_WIDGET(child->widget) + && gtk_widget_get_parent(child->widget) == widget) + gtk_widget_snapshot_child(widget, child->widget, snapshot); + } +} + + static void +form_dispose(GObject *object) +{ + GtkForm *form = GTK_FORM(object); + GList *tmp_list; + + tmp_list = form->children; + while (tmp_list) + { + GtkFormChild *child = tmp_list->data; + tmp_list = tmp_list->next; + + gtk_widget_unparent(child->widget); + g_free(child); + } + g_list_free(form->children); + form->children = NULL; + + G_OBJECT_CLASS(gui_gtk_form_parent_class)->dispose(object); +} + +// Child positioning using GskTransform + + static void +form_position_child( + GtkForm *form UNUSED, + GtkFormChild *child, + gboolean force_allocate) +{ + if (!force_allocate) + return; + + if (child->widget == NULL || !GTK_IS_WIDGET(child->widget)) + return; + + { + GtkRequisition requisition; + GskTransform *transform; + int w, h; + + gtk_widget_get_preferred_size(child->widget, &requisition, NULL); + w = requisition.width; + h = requisition.height; + + // If widget has no size request (e.g. drawarea), use parent size + if (w <= 0) + w = gtk_widget_get_width(GTK_WIDGET(form)); + if (h <= 0) + h = gtk_widget_get_height(GTK_WIDGET(form)); + if (w <= 0) w = 1; + if (h <= 0) h = 1; + + transform = gsk_transform_translate(NULL, + &GRAPHENE_POINT_INIT((float)child->x, (float)child->y)); + gtk_widget_allocate(child->widget, w, h, -1, transform); + } +} diff --git a/src/gui_gtk4_f.h b/src/gui_gtk4_f.h new file mode 100644 index 0000000000..a927061608 --- /dev/null +++ b/src/gui_gtk4_f.h @@ -0,0 +1,59 @@ +/* vi:set ts=8 sts=4 sw=4 noet: + * + * VIM - Vi IMproved by Bram Moolenaar + * + * Do ":help uganda" in Vim to read copying and usage conditions. + * Do ":help credits" in Vim to see a list of people who contributed. + * + * GTK4 GtkForm widget - a simple container for absolute positioning. + */ + +#ifndef GUI_GTK4_FORM_H +#define GUI_GTK4_FORM_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define GTK_TYPE_FORM (gui_gtk_form_get_type()) +#define GTK_FORM(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GTK_TYPE_FORM, GtkForm)) +#define GTK_FORM_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GTK_TYPE_FORM, GtkFormClass)) +#define GTK_IS_FORM(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GTK_TYPE_FORM)) + +typedef struct _GtkForm GtkForm; +typedef struct _GtkFormClass GtkFormClass; + +struct _GtkForm +{ + GtkWidget widget; + GList *children; + gint freeze_count; +}; + +struct _GtkFormClass +{ + GtkWidgetClass parent_class; +}; + +GType gui_gtk_form_get_type(void); + +GtkWidget *gui_gtk_form_new(void); + +void gui_gtk_form_put(GtkForm *form, GtkWidget *widget, gint x, gint y); + +void gui_gtk_form_move(GtkForm *form, GtkWidget *widget, gint x, gint y); + +void gui_gtk_form_move_resize(GtkForm *form, GtkWidget *widget, + gint x, gint y, gint w, gint h); + +void gui_gtk_form_remove(GtkForm *form, GtkWidget *widget); + +void gui_gtk_form_freeze(GtkForm *form); +void gui_gtk_form_thaw(GtkForm *form); + +#ifdef __cplusplus +} +#endif +#endif // GUI_GTK4_FORM_H diff --git a/src/gui_xim.c b/src/gui_xim.c index 26b125d919..acc6351936 100644 --- a/src/gui_xim.c +++ b/src/gui_xim.c @@ -18,15 +18,19 @@ #endif #if defined(FEAT_GUI_GTK) && defined(FEAT_XIM) -# if GTK_CHECK_VERSION(3,0,0) +# ifdef USE_GTK4 +# include +# elif GTK_CHECK_VERSION(3,0,0) # include # else # include # endif -# ifdef MSWIN -# include -# else -# include +# if !defined(USE_GTK4) +# ifdef MSWIN +# include +# else +# include +# endif # endif #endif @@ -185,7 +189,11 @@ static int im_preedit_cursor = 0; // cursor offset in characters static int im_preedit_trailing = 0; // number of characters after cursor static unsigned long im_commit_handler_id = 0; +# ifdef USE_GTK4 +static unsigned int im_activatekey_keyval = GDK_KEY_VoidSymbol; +# else static unsigned int im_activatekey_keyval = GDK_VoidSymbol; +# endif static unsigned int im_activatekey_state = 0; static GtkWidget *preedit_window = NULL; @@ -272,6 +280,19 @@ im_preedit_window_set_position(void) if (preedit_window == NULL) return; +# ifdef USE_GTK4 + // GTK4: positioning popup windows is limited. + // Use a simpler approach - just place near the cursor. + x = FILL_X(gui.col); + y = FILL_Y(gui.row) + gui.char_height; + width = 0; + height = 0; + screen_x = 0; + screen_y = 0; + screen_width = 0; + screen_height = 0; + // GTK4 doesn't have gtk_window_move; preedit is shown in-place. +# else gui_gtk_get_screen_geom_of_win(gui.drawarea, 0, 0, &screen_x, &screen_y, &screen_width, &screen_height); gdk_window_get_origin(gtk_widget_get_window(gui.drawarea), &x, &y); @@ -283,6 +304,7 @@ im_preedit_window_set_position(void) if (y + height > screen_y + screen_height) y = screen_y + screen_height - height; gtk_window_move(GTK_WINDOW(preedit_window), x, y); +# endif } static void @@ -305,18 +327,28 @@ im_preedit_window_open(void) if (preedit_window == NULL) { +# ifdef USE_GTK4 + preedit_window = gtk_window_new(); +# else preedit_window = gtk_window_new(GTK_WINDOW_POPUP); +# endif gtk_window_set_transient_for(GTK_WINDOW(preedit_window), GTK_WINDOW(gui.mainwin)); preedit_label = gtk_label_new(""); gtk_widget_set_name(preedit_label, "vim-gui-preedit-area"); +# ifdef USE_GTK4 + gtk_window_set_child(GTK_WINDOW(preedit_window), preedit_label); +# else gtk_container_add(GTK_CONTAINER(preedit_window), preedit_label); +# endif } # if GTK_CHECK_VERSION(3,16,0) { +# ifndef USE_GTK4 GtkStyleContext * const context = gtk_widget_get_style_context(preedit_label); +# endif GtkCssProvider * const provider = gtk_css_provider_new(); gchar *css = NULL; const char * const fontname @@ -329,10 +361,15 @@ im_preedit_window_open(void) { // fontsize was given in points. Convert it into that in pixels // to use with CSS. +# ifdef USE_GTK4 + // GTK4: assume 96 DPI as default + fontsize = 96 * fontsize / 72; +# else GdkScreen * const screen = gdk_window_get_screen(gtk_widget_get_window(gui.mainwin)); const gdouble dpi = gdk_screen_get_resolution(screen); fontsize = dpi * fontsize / 72; +# endif } if (fontsize > 0) fontsize_propval = g_strdup_printf("%dpx", fontsize); @@ -355,9 +392,16 @@ im_preedit_window_open(void) (gui.back_pixel >> 8) & 0xff, gui.back_pixel & 0xff); +# ifdef USE_GTK4 + gtk_css_provider_load_from_string(provider, css); + gtk_style_context_add_provider_for_display( + gdk_display_get_default(), + GTK_STYLE_PROVIDER(provider), G_MAXUINT); +# else gtk_css_provider_load_from_data(provider, css, -1, NULL); gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider), G_MAXUINT); +# endif g_free(css); g_free(fontsize_propval); @@ -396,9 +440,13 @@ im_preedit_window_open(void) layout = gtk_label_get_layout(GTK_LABEL(preedit_label)); pango_layout_get_pixel_size(layout, &w, &h); h = MAX(h, gui.char_height); +# ifdef USE_GTK4 + gtk_window_set_default_size(GTK_WINDOW(preedit_window), w, h); + gtk_widget_set_visible(preedit_window, TRUE); +# else gtk_window_resize(GTK_WINDOW(preedit_window), w, h); - gtk_widget_show_all(preedit_window); +# endif im_preedit_window_set_position(); } @@ -411,7 +459,11 @@ im_preedit_window_open(void) im_preedit_window_close(void) { if (preedit_window != NULL) +# ifdef USE_GTK4 + gtk_widget_set_visible(preedit_window, FALSE); +# else gtk_widget_hide(preedit_window); +# endif } static void @@ -874,7 +926,9 @@ xim_init(void) # endif g_return_if_fail(gui.drawarea != NULL); +# ifndef USE_GTK4 g_return_if_fail(gtk_widget_get_window(gui.drawarea) != NULL); +# endif xic = gtk_im_multicontext_new(); g_object_ref(xic); @@ -888,7 +942,11 @@ xim_init(void) g_signal_connect(G_OBJECT(xic), "preedit_end", G_CALLBACK(&im_preedit_end_cb), NULL); +# ifdef USE_GTK4 + gtk_im_context_set_client_widget(xic, gui.drawarea); +# else gtk_im_context_set_client_window(xic, gtk_widget_get_window(gui.drawarea)); +# endif } void @@ -911,6 +969,7 @@ im_shutdown(void) xim_has_preediting = FALSE; } +# ifndef USE_GTK4 /* * Convert the string argument to keyval and state for GdkEventKey. * If str is valid return TRUE, otherwise FALSE. @@ -980,7 +1039,9 @@ im_xim_isvalid_imactivate(void) &im_activatekey_keyval, &im_activatekey_state); } +# endif // !USE_GTK4 +# ifndef USE_GTK4 static void im_synthesize_keypress(unsigned int keyval, unsigned int state) { @@ -1008,6 +1069,7 @@ im_synthesize_keypress(unsigned int keyval, unsigned int state) gdk_event_free((GdkEvent *)event); } +# endif // !USE_GTK4 void xim_reset(void) @@ -1027,6 +1089,11 @@ xim_reset(void) { xim_set_focus(gui.in_focus); +# ifdef USE_GTK4 + im_shutdown(); + xim_init(); + xim_set_focus(gui.in_focus); +# else if (im_activatekey_keyval != GDK_VoidSymbol) { if (im_is_active) @@ -1043,6 +1110,7 @@ xim_reset(void) xim_init(); xim_set_focus(gui.in_focus); } +# endif } } @@ -1051,12 +1119,13 @@ xim_reset(void) xim_has_preediting = FALSE; } +# ifndef USE_GTK4 int xim_queue_key_press_event(GdkEventKey *event, int down) { -# ifdef FEAT_GUI_GTK +# ifdef FEAT_GUI_GTK if (event->state & GDK_SUPER_MASK) return FALSE; -# endif +# endif if (down) { // Workaround GTK2 XIM 'feature' that always converts keypad keys to @@ -1185,6 +1254,23 @@ xim_queue_key_press_event(GdkEventKey *event, int down) return FALSE; } +# else // USE_GTK4 +// GTK4: imactivatekey is not supported because GTK4's GtkIMContext +// does not allow synthesizing key events for IM activation. + int +im_xim_isvalid_imactivate(void) +{ + // Empty string is always valid (means no activation key). + // Any other value is not supported in GTK4. + return p_imak[0] == NUL; +} + + int +xim_queue_key_press_event(GdkEvent *event UNUSED, int down UNUSED) +{ + return FALSE; +} +# endif // !USE_GTK4 int im_get_status(void) diff --git a/src/hardcopy.c b/src/hardcopy.c index e87caf3020..2eb63ea588 100644 --- a/src/hardcopy.c +++ b/src/hardcopy.c @@ -560,6 +560,16 @@ ex_hardcopy(exarg_T *eap) CLEAR_FIELD(settings); settings.has_color = TRUE; +#ifdef FEAT_GUI_GTK_PRINT + // Use the native GTK print dialog only for interactive printing; + // ":hardcopy >file" must fall through to the PostScript writer. + if (gui.in_use && *eap->arg != '>') + { + gui_gtk4_hardcopy(eap); + return; + } +#endif + #ifdef FEAT_POSTSCRIPT if (*eap->arg == '>') { diff --git a/src/po/vim.pot b/src/po/vim.pot index f1369d68ae..2057c23a92 100644 --- a/src/po/vim.pot +++ b/src/po/vim.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Vim\n" "Report-Msgid-Bugs-To: vim-dev@vim.org\n" -"POT-Creation-Date: 2026-05-17 19:50+0000\n" +"POT-Creation-Date: 2026-05-19 18:20+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -921,9 +921,11 @@ msgstr "" msgid "VIM - Search..." msgstr "" +#. "Find what:" label + entry msgid "Find what:" msgstr "" +#. "Replace with:" label + entry msgid "Replace with:" msgstr "" @@ -957,6 +959,12 @@ msgstr "" msgid "_Close" msgstr "" +msgid "Direction:" +msgstr "" + +msgid "Close" +msgstr "" + msgid "Vim: Received \"die\" request from session manager\n" msgstr "" @@ -3454,6 +3462,9 @@ msgstr "" msgid "without GUI." msgstr "" +msgid "with GTK4 GUI." +msgstr "" + msgid "with GTK3 GUI." msgstr "" diff --git a/src/proto.h b/src/proto.h index e46b25624f..ed7a5a054e 100644 --- a/src/proto.h +++ b/src/proto.h @@ -302,8 +302,12 @@ extern char_u *vimpty_getenv(const char_u *string); // in misc2.c # include "gui_w32.pro" # endif # ifdef FEAT_GUI_GTK -# include "gui_gtk.pro" -# include "gui_gtk_x11.pro" +# ifdef USE_GTK4 +# include "gui_gtk4.pro" +# else +# include "gui_gtk.pro" +# include "gui_gtk_x11.pro" +# endif # endif # ifdef FEAT_GUI_MOTIF # include "gui_motif.pro" diff --git a/src/proto/gui_gtk4.pro b/src/proto/gui_gtk4.pro new file mode 100644 index 0000000000..1f13ab6ca6 --- /dev/null +++ b/src/proto/gui_gtk4.pro @@ -0,0 +1,110 @@ +/* gui_gtk4.c */ +void gui_mch_prepare(int *argc, char **argv); +void gui_mch_free_all(void); +int gui_mch_is_blinking(void); +int gui_mch_is_blink_off(void); +void gui_mch_set_blinking(long waittime, long on, long off); +void gui_mch_stop_blink(int may_call_gui_update_cursor); +void gui_mch_start_blink(void); +int gui_mch_early_init_check(int give_message); +int gui_mch_init_check(void); +int gui_mch_init(void); +void gui_mch_new_colors(void); +int gui_mch_open(void); +void gui_mch_exit(int rc); +int gui_mch_get_winpos(int *x, int *y); +void gui_mch_set_winpos(int x, int y); +int gui_mch_maximized(void); +void gui_mch_unmaximize(void); +void gui_mch_newfont(void); +void gui_mch_settitle(char_u *title, char_u *icon); +void gui_mch_set_shellsize(int width, int height, int min_width, int min_height, int base_width, int base_height, int direction); +void gui_mch_get_screen_dimensions(int *screen_w, int *screen_h); +void gui_mch_enable_menu(int showit); +void gui_mch_show_toolbar(int showit); +void gui_mch_set_dark_theme(int dark); +int gui_mch_adjust_charheight(void); +char_u *gui_mch_font_dialog(char_u *oldval); +int gui_mch_init_font(char_u *font_name, int fontset); +GuiFont gui_mch_get_font(char_u *name, int report_error); +char_u *gui_mch_get_fontname(GuiFont font, char_u *name); +void gui_mch_free_font(GuiFont font); +void gui_mch_expand_font(optexpand_T *args, void *param, int (*add_match)(char_u *val)); +guicolor_T gui_mch_get_color(char_u *name); +guicolor_T gui_mch_get_rgb_color(int r, int g, int b); +void gui_mch_set_fg_color(guicolor_T color); +void gui_mch_set_bg_color(guicolor_T color); +void gui_mch_set_sp_color(guicolor_T color); +guicolor_T gui_mch_get_rgb(guicolor_T pixel); +void gui_mch_clear_block(int row1, int col1, int row2, int col2); +void gui_mch_clear_all(void); +void gui_mch_delete_lines(int row, int num_lines); +void gui_mch_insert_lines(int row, int num_lines); +void gui_mch_draw_hollow_cursor(guicolor_T color); +void gui_mch_draw_part_cursor(int w, int h, guicolor_T color); +void gui_mch_flash(int msec); +void gui_mch_invert_rectangle(int r, int c, int nr, int nc); +void gui_mch_update(void); +int gui_mch_wait_for_chars(long wtime); +void gui_mch_flush(void); +void gui_mch_beep(void); +void *gui_mch_get_display(void); +void gui_mch_iconify(void); +void gui_mch_set_foreground(void); +void gui_mch_getmouse(int *x, int *y); +void gui_mch_setmouse(int x, int y); +void gui_mch_mousehide(int hide); +int gui_mch_haskey(char_u *name); +void gui_mch_forked(void); +void gui_mch_enable_scrollbar(scrollbar_T *sb, int flag); +void gui_mch_menu_grey(vimmenu_T *menu, int grey); +void gui_mch_menu_hidden(vimmenu_T *menu, int hidden); +void gui_mch_draw_menubar(void); +void gui_mch_show_tabline(int showit); +int gui_mch_showing_tabline(void); +void gui_mch_update_tabline(void); +void gui_mch_set_curtab(int nr); +void gui_mch_drawsign(int row, int col, int typenr); +void *gui_mch_register_sign(char_u *signfile); +void gui_mch_destroy_sign(void *sign); +int gui_gtk_draw_string_ext(int row, int col, char_u *s, int len, int flags, int force_pango); +int gui_gtk_draw_string(int row, int col, char_u *s, int len, int flags); +int gui_get_x11_windis(Window *win, Display **dis); +void gui_gtk_init_socket_server(void); +void gui_gtk_uninit_socket_server(void); +void gui_gtk_set_mnemonics(int enable); +void gui_make_popup(char_u *path_name, int mouse_pos); +int get_menu_tool_width(void); +int get_menu_tool_height(void); +void clip_mch_request_selection(Clipboard_T *cbd); +void clip_mch_set_selection(Clipboard_T *cbd); +int clip_mch_own_selection(Clipboard_T *cbd); +void clip_mch_lose_selection(Clipboard_T *cbd); +void gui_mch_post_balloon(BalloonEval *beval, char_u *mesg); +BalloonEval *gui_mch_create_beval_area(void *target, char_u *mesg, void (*mesgCB)(BalloonEval *, int), void *clientData); +void gui_mch_enable_beval_area(BalloonEval *beval); +void gui_mch_disable_beval_area(BalloonEval *beval); +guint gtk_main_level(void); +void gtk_main_quit(void); +void mch_set_mouse_shape(int shape); +void gui_mch_add_menu(vimmenu_T *menu, int idx); +void gui_mch_add_menu_item(vimmenu_T *menu, int idx); +void gui_mch_toggle_tearoffs(int enable); +void gui_mch_menu_set_tip(vimmenu_T *menu); +void gui_mch_destroy_menu(vimmenu_T *menu); +void gui_mch_show_popupmenu(vimmenu_T *menu); +void gui_mch_set_scrollbar_thumb(scrollbar_T *sb, long val, long size, long max); +void gui_mch_set_scrollbar_pos(scrollbar_T *sb, int x, int y, int w, int h); +int gui_mch_get_scrollbar_xpadding(void); +int gui_mch_get_scrollbar_ypadding(void); +void gui_mch_create_scrollbar(scrollbar_T *sb, int orient); +void gui_mch_destroy_scrollbar(scrollbar_T *sb); +void gui_mch_set_text_area_pos(int x, int y, int w, int h); +char_u *gui_mch_browse(int saving, char_u *title, char_u *dflt, char_u *ext, char_u *initdir, char_u *filter); +char_u *gui_mch_browsedir(char_u *title, char_u *initdir); +int gui_mch_dialog(int type, char_u *title, char_u *message, char_u *buttons, int dfltbutton, char_u *textfield, int ex_cmd); +void gui_mch_find_dialog(exarg_T *eap); +void gui_mch_replace_dialog(exarg_T *eap); +void ex_helpfind(exarg_T *eap); +void gui_gtk4_hardcopy(exarg_T *eap); +/* vim: set ft=c : */ diff --git a/src/version.c b/src/version.c index bb0b3d4f53..e6bc4886cc 100644 --- a/src/version.c +++ b/src/version.c @@ -729,6 +729,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 501, /**/ 500, /**/ @@ -2077,7 +2079,9 @@ list_version(void) #if !defined(FEAT_GUI) msg_puts(_("without GUI.")); #elif defined(FEAT_GUI_GTK) -# if defined(USE_GTK3) +# if defined(USE_GTK4) + msg_puts(_("with GTK4 GUI.")); +# elif defined(USE_GTK3) msg_puts(_("with GTK3 GUI.")); # elif defined(FEAT_GUI_GNOME) msg_puts(_("with GTK2-GNOME GUI.")); diff --git a/src/vim.h b/src/vim.h index 683aff2b75..6906547c4e 100644 --- a/src/vim.h +++ b/src/vim.h @@ -2365,7 +2365,7 @@ typedef struct Atom sel_atom; // PRIMARY/CLIPBOARD selection ID # endif -# ifdef FEAT_GUI_GTK +# if defined(FEAT_GUI_GTK) && !defined(USE_GTK4) GdkAtom gtk_sel_atom; // PRIMARY/CLIPBOARD selection ID # endif