From 267c6f2ac71f92999e969232431ba04678e7437e Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 15 Apr 2024 07:54:39 +0200 Subject: Adding upstream version 4:24.2.0. Signed-off-by: Daniel Baumann --- vcl/unx/generic/app/gendata.cxx | 69 + vcl/unx/generic/app/gendisp.cxx | 69 + vcl/unx/generic/app/geninst.cxx | 99 + vcl/unx/generic/app/gensys.cxx | 116 + vcl/unx/generic/app/i18n_cb.cxx | 494 + vcl/unx/generic/app/i18n_ic.cxx | 604 + vcl/unx/generic/app/i18n_im.cxx | 410 + vcl/unx/generic/app/i18n_keysym.cxx | 358 + vcl/unx/generic/app/i18n_xkb.cxx | 107 + vcl/unx/generic/app/keysymnames.cxx | 509 + vcl/unx/generic/app/randrwrapper.cxx | 181 + vcl/unx/generic/app/saldata.cxx | 775 + vcl/unx/generic/app/saldisp.cxx | 2489 ++ vcl/unx/generic/app/salinst.cxx | 253 + vcl/unx/generic/app/saltimer.cxx | 68 + vcl/unx/generic/app/sm.cxx | 857 + vcl/unx/generic/app/wmadaptor.cxx | 2196 ++ vcl/unx/generic/desktopdetect/desktopdetector.cxx | 264 + vcl/unx/generic/dtrans/X11_clipboard.cxx | 231 + vcl/unx/generic/dtrans/X11_clipboard.hxx | 111 + vcl/unx/generic/dtrans/X11_dndcontext.cxx | 118 + vcl/unx/generic/dtrans/X11_dndcontext.hxx | 80 + vcl/unx/generic/dtrans/X11_droptarget.cxx | 177 + vcl/unx/generic/dtrans/X11_selection.cxx | 4157 ++++ vcl/unx/generic/dtrans/X11_selection.hxx | 496 + vcl/unx/generic/dtrans/X11_service.cxx | 88 + vcl/unx/generic/dtrans/X11_transferable.cxx | 101 + vcl/unx/generic/dtrans/X11_transferable.hxx | 50 + vcl/unx/generic/dtrans/bmp.cxx | 786 + vcl/unx/generic/dtrans/bmp.hxx | 75 + vcl/unx/generic/dtrans/config.cxx | 123 + vcl/unx/generic/dtrans/copydata_curs.h | 36 + vcl/unx/generic/dtrans/copydata_mask.h | 36 + vcl/unx/generic/dtrans/linkdata_curs.h | 36 + vcl/unx/generic/dtrans/linkdata_mask.h | 36 + vcl/unx/generic/dtrans/movedata_curs.h | 36 + vcl/unx/generic/dtrans/movedata_mask.h | 36 + vcl/unx/generic/dtrans/nodrop_curs.h | 36 + vcl/unx/generic/dtrans/nodrop_mask.h | 36 + vcl/unx/generic/fontmanager/fontconfig.cxx | 1310 + vcl/unx/generic/fontmanager/fontmanager.cxx | 736 + vcl/unx/generic/fontmanager/fontsubst.cxx | 223 + vcl/unx/generic/fontmanager/helper.cxx | 261 + vcl/unx/generic/gdi/X11CairoSalGraphicsImpl.cxx | 243 + vcl/unx/generic/gdi/X11CairoSalGraphicsImpl.hxx | 188 + vcl/unx/generic/gdi/cairo_xlib_cairo.cxx | 277 + vcl/unx/generic/gdi/cairo_xlib_cairo.hxx | 93 + vcl/unx/generic/gdi/cairotextrender.cxx | 526 + vcl/unx/generic/gdi/font.cxx | 84 + vcl/unx/generic/gdi/freetypetextrender.cxx | 185 + vcl/unx/generic/gdi/salgdi.cxx | 298 + vcl/unx/generic/gdi/salvd.cxx | 241 + vcl/unx/generic/glyphs/freetype_glyphcache.cxx | 824 + vcl/unx/generic/glyphs/glyphcache.cxx | 86 + vcl/unx/generic/print/genprnpsp.cxx | 1097 + vcl/unx/generic/print/genpspgraphics.cxx | 195 + vcl/unx/generic/print/prtsetup.cxx | 465 + vcl/unx/generic/print/prtsetup.hxx | 135 + vcl/unx/generic/print/psheader.ps | 363 + vcl/unx/generic/printer/configuration/README | 5 + .../generic/printer/configuration/ppds/SGENPRT.PS | 582 + vcl/unx/generic/printer/configuration/psprint.conf | 94 + vcl/unx/generic/printer/cpdmgr.cxx | 756 + vcl/unx/generic/printer/cupsmgr.cxx | 992 + vcl/unx/generic/printer/jobdata.cxx | 238 + vcl/unx/generic/printer/ppdparser.cxx | 1951 ++ vcl/unx/generic/printer/printerinfomanager.cxx | 868 + vcl/unx/generic/window/salframe.cxx | 3871 +++ vcl/unx/generic/window/salobj.cxx | 506 + vcl/unx/generic/window/sessioninhibitor.cxx | 370 + vcl/unx/gtk3/a11y/TODO | 49 + vcl/unx/gtk3/a11y/atkaction.cxx | 269 + vcl/unx/gtk3/a11y/atkbridge.cxx | 32 + vcl/unx/gtk3/a11y/atkcomponent.cxx | 431 + vcl/unx/gtk3/a11y/atkeditabletext.cxx | 193 + vcl/unx/gtk3/a11y/atkfactory.cxx | 186 + vcl/unx/gtk3/a11y/atkfactory.hxx | 30 + vcl/unx/gtk3/a11y/atkhypertext.cxx | 277 + vcl/unx/gtk3/a11y/atkimage.cxx | 135 + vcl/unx/gtk3/a11y/atklistener.cxx | 783 + vcl/unx/gtk3/a11y/atklistener.hxx | 70 + vcl/unx/gtk3/a11y/atkregistry.cxx | 66 + vcl/unx/gtk3/a11y/atkregistry.hxx | 34 + vcl/unx/gtk3/a11y/atkselection.cxx | 206 + vcl/unx/gtk3/a11y/atktable.cxx | 647 + vcl/unx/gtk3/a11y/atktablecell.cxx | 269 + vcl/unx/gtk3/a11y/atktext.cxx | 881 + vcl/unx/gtk3/a11y/atktextattributes.cxx | 1388 ++ vcl/unx/gtk3/a11y/atktextattributes.hxx | 45 + vcl/unx/gtk3/a11y/atkutil.cxx | 626 + vcl/unx/gtk3/a11y/atkutil.hxx | 26 + vcl/unx/gtk3/a11y/atkvalue.cxx | 154 + vcl/unx/gtk3/a11y/atkwrapper.cxx | 1103 + vcl/unx/gtk3/a11y/atkwrapper.hxx | 131 + vcl/unx/gtk3/customcellrenderer.cxx | 316 + vcl/unx/gtk3/customcellrenderer.hxx | 49 + vcl/unx/gtk3/fpicker/SalGtkFilePicker.cxx | 2091 ++ vcl/unx/gtk3/fpicker/SalGtkFilePicker.hxx | 239 + vcl/unx/gtk3/fpicker/SalGtkFolderPicker.cxx | 205 + vcl/unx/gtk3/fpicker/SalGtkFolderPicker.hxx | 66 + vcl/unx/gtk3/fpicker/SalGtkPicker.cxx | 295 + vcl/unx/gtk3/fpicker/SalGtkPicker.hxx | 144 + vcl/unx/gtk3/fpicker/eventnotification.hxx | 41 + vcl/unx/gtk3/fpicker/resourceprovider.cxx | 80 + vcl/unx/gtk3/gloactiongroup.cxx | 405 + vcl/unx/gtk3/glomenu.cxx | 694 + vcl/unx/gtk3/gtkcairo.cxx | 129 + vcl/unx/gtk3/gtkcairo.hxx | 46 + vcl/unx/gtk3/gtkdata.cxx | 984 + vcl/unx/gtk3/gtkframe.cxx | 6416 +++++ vcl/unx/gtk3/gtkinst.cxx | 24855 +++++++++++++++++++ vcl/unx/gtk3/gtkobject.cxx | 611 + vcl/unx/gtk3/gtksalmenu.cxx | 1646 ++ vcl/unx/gtk3/gtksys.cxx | 330 + vcl/unx/gtk3/hudawareness.cxx | 110 + vcl/unx/gtk3/salnativewidgets-gtk.cxx | 3082 +++ vcl/unx/gtk3_kde5/FPServiceInfo.hxx | 28 + vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkaction.cxx | 12 + vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkbridge.cxx | 12 + vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkcomponent.cxx | 12 + .../gtk3_kde5/a11y/gtk3_kde5_atkeditabletext.cxx | 12 + vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkfactory.cxx | 12 + vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkhypertext.cxx | 12 + vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkimage.cxx | 12 + vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atklistener.cxx | 12 + vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkregistry.cxx | 12 + vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkselection.cxx | 12 + vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktable.cxx | 12 + vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktablecell.cxx | 12 + vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktext.cxx | 12 + .../gtk3_kde5/a11y/gtk3_kde5_atktextattributes.cxx | 12 + vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkutil.cxx | 12 + vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkvalue.cxx | 12 + vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkwrapper.cxx | 12 + vcl/unx/gtk3_kde5/filepicker_ipc_commands.hxx | 173 + vcl/unx/gtk3_kde5/gtk3_kde5_cairo.cxx | 22 + vcl/unx/gtk3_kde5/gtk3_kde5_customcellrenderer.cxx | 12 + vcl/unx/gtk3_kde5/gtk3_kde5_filepicker.cxx | 455 + vcl/unx/gtk3_kde5/gtk3_kde5_filepicker.hxx | 131 + vcl/unx/gtk3_kde5/gtk3_kde5_filepicker_ipc.cxx | 276 + vcl/unx/gtk3_kde5/gtk3_kde5_filepicker_ipc.hxx | 135 + vcl/unx/gtk3_kde5/gtk3_kde5_folderpicker.cxx | 85 + vcl/unx/gtk3_kde5/gtk3_kde5_folderpicker.hxx | 59 + vcl/unx/gtk3_kde5/gtk3_kde5_gloactiongroup.cxx | 22 + vcl/unx/gtk3_kde5/gtk3_kde5_glomenu.cxx | 22 + vcl/unx/gtk3_kde5/gtk3_kde5_gtkdata.cxx | 22 + vcl/unx/gtk3_kde5/gtk3_kde5_gtkframe.cxx | 22 + vcl/unx/gtk3_kde5/gtk3_kde5_gtkinst.cxx | 57 + vcl/unx/gtk3_kde5/gtk3_kde5_gtkobject.cxx | 22 + vcl/unx/gtk3_kde5/gtk3_kde5_gtksalmenu.cxx | 22 + vcl/unx/gtk3_kde5/gtk3_kde5_gtksys.cxx | 22 + vcl/unx/gtk3_kde5/gtk3_kde5_hudawareness.cxx | 22 + .../gtk3_kde5/gtk3_kde5_salnativewidgets-gtk.cxx | 22 + vcl/unx/gtk3_kde5/kde5_filepicker.cxx | 286 + vcl/unx/gtk3_kde5/kde5_filepicker.hxx | 110 + vcl/unx/gtk3_kde5/kde5_filepicker_ipc.cxx | 374 + vcl/unx/gtk3_kde5/kde5_filepicker_ipc.hxx | 51 + vcl/unx/gtk3_kde5/kde5_lo_filepicker_main.cxx | 53 + vcl/unx/gtk4/a11y.cxx | 851 + vcl/unx/gtk4/a11y.hxx | 24 + vcl/unx/gtk4/convert3to4.cxx | 1603 ++ vcl/unx/gtk4/convert3to4.hxx | 16 + vcl/unx/gtk4/customcellrenderer.cxx | 12 + vcl/unx/gtk4/fpicker/SalGtkFilePicker.cxx | 12 + vcl/unx/gtk4/fpicker/SalGtkFilePicker.hxx | 12 + vcl/unx/gtk4/fpicker/SalGtkFolderPicker.cxx | 12 + vcl/unx/gtk4/fpicker/SalGtkFolderPicker.hxx | 12 + vcl/unx/gtk4/fpicker/SalGtkPicker.cxx | 12 + vcl/unx/gtk4/fpicker/SalGtkPicker.hxx | 12 + vcl/unx/gtk4/fpicker/eventnotification.hxx | 12 + vcl/unx/gtk4/fpicker/resourceprovider.cxx | 12 + vcl/unx/gtk4/gloactiongroup.cxx | 12 + vcl/unx/gtk4/glomenu.cxx | 12 + vcl/unx/gtk4/gtkcairo.cxx | 12 + vcl/unx/gtk4/gtkcairo.hxx | 12 + vcl/unx/gtk4/gtkdata.cxx | 12 + vcl/unx/gtk4/gtkframe.cxx | 14 + vcl/unx/gtk4/gtkinst.cxx | 21 + vcl/unx/gtk4/gtkobject.cxx | 12 + vcl/unx/gtk4/gtksalmenu.cxx | 12 + vcl/unx/gtk4/gtksys.cxx | 12 + vcl/unx/gtk4/hudawareness.cxx | 12 + vcl/unx/gtk4/notifyinglayout.cxx | 88 + vcl/unx/gtk4/notifyinglayout.hxx | 36 + vcl/unx/gtk4/salnativewidgets-gtk.cxx | 12 + vcl/unx/gtk4/surfacecellrenderer.cxx | 241 + vcl/unx/gtk4/surfacecellrenderer.hxx | 42 + vcl/unx/gtk4/surfacepaintable.cxx | 100 + vcl/unx/gtk4/surfacepaintable.hxx | 32 + vcl/unx/gtk4/transferableprovider.cxx | 118 + vcl/unx/gtk4/transferableprovider.hxx | 51 + vcl/unx/kf5/KFFilePicker.cxx | 179 + vcl/unx/kf5/KFFilePicker.hxx | 63 + vcl/unx/kf5/KFSalInstance.cxx | 116 + vcl/unx/kf5/KFSalInstance.hxx | 37 + vcl/unx/kf6/KFFilePicker.cxx | 12 + vcl/unx/kf6/KFFilePicker.hxx | 12 + vcl/unx/kf6/KFSalInstance.cxx | 12 + vcl/unx/kf6/KFSalInstance.hxx | 12 + vcl/unx/x11/x11sys.cxx | 98 + vcl/unx/x11/xlimits.cxx | 29 + 201 files changed, 92471 insertions(+) create mode 100644 vcl/unx/generic/app/gendata.cxx create mode 100644 vcl/unx/generic/app/gendisp.cxx create mode 100644 vcl/unx/generic/app/geninst.cxx create mode 100644 vcl/unx/generic/app/gensys.cxx create mode 100644 vcl/unx/generic/app/i18n_cb.cxx create mode 100644 vcl/unx/generic/app/i18n_ic.cxx create mode 100644 vcl/unx/generic/app/i18n_im.cxx create mode 100644 vcl/unx/generic/app/i18n_keysym.cxx create mode 100644 vcl/unx/generic/app/i18n_xkb.cxx create mode 100644 vcl/unx/generic/app/keysymnames.cxx create mode 100644 vcl/unx/generic/app/randrwrapper.cxx create mode 100644 vcl/unx/generic/app/saldata.cxx create mode 100644 vcl/unx/generic/app/saldisp.cxx create mode 100644 vcl/unx/generic/app/salinst.cxx create mode 100644 vcl/unx/generic/app/saltimer.cxx create mode 100644 vcl/unx/generic/app/sm.cxx create mode 100644 vcl/unx/generic/app/wmadaptor.cxx create mode 100644 vcl/unx/generic/desktopdetect/desktopdetector.cxx create mode 100644 vcl/unx/generic/dtrans/X11_clipboard.cxx create mode 100644 vcl/unx/generic/dtrans/X11_clipboard.hxx create mode 100644 vcl/unx/generic/dtrans/X11_dndcontext.cxx create mode 100644 vcl/unx/generic/dtrans/X11_dndcontext.hxx create mode 100644 vcl/unx/generic/dtrans/X11_droptarget.cxx create mode 100644 vcl/unx/generic/dtrans/X11_selection.cxx create mode 100644 vcl/unx/generic/dtrans/X11_selection.hxx create mode 100644 vcl/unx/generic/dtrans/X11_service.cxx create mode 100644 vcl/unx/generic/dtrans/X11_transferable.cxx create mode 100644 vcl/unx/generic/dtrans/X11_transferable.hxx create mode 100644 vcl/unx/generic/dtrans/bmp.cxx create mode 100644 vcl/unx/generic/dtrans/bmp.hxx create mode 100644 vcl/unx/generic/dtrans/config.cxx create mode 100644 vcl/unx/generic/dtrans/copydata_curs.h create mode 100644 vcl/unx/generic/dtrans/copydata_mask.h create mode 100644 vcl/unx/generic/dtrans/linkdata_curs.h create mode 100644 vcl/unx/generic/dtrans/linkdata_mask.h create mode 100644 vcl/unx/generic/dtrans/movedata_curs.h create mode 100644 vcl/unx/generic/dtrans/movedata_mask.h create mode 100644 vcl/unx/generic/dtrans/nodrop_curs.h create mode 100644 vcl/unx/generic/dtrans/nodrop_mask.h create mode 100644 vcl/unx/generic/fontmanager/fontconfig.cxx create mode 100644 vcl/unx/generic/fontmanager/fontmanager.cxx create mode 100644 vcl/unx/generic/fontmanager/fontsubst.cxx create mode 100644 vcl/unx/generic/fontmanager/helper.cxx create mode 100644 vcl/unx/generic/gdi/X11CairoSalGraphicsImpl.cxx create mode 100644 vcl/unx/generic/gdi/X11CairoSalGraphicsImpl.hxx create mode 100644 vcl/unx/generic/gdi/cairo_xlib_cairo.cxx create mode 100644 vcl/unx/generic/gdi/cairo_xlib_cairo.hxx create mode 100644 vcl/unx/generic/gdi/cairotextrender.cxx create mode 100644 vcl/unx/generic/gdi/font.cxx create mode 100644 vcl/unx/generic/gdi/freetypetextrender.cxx create mode 100644 vcl/unx/generic/gdi/salgdi.cxx create mode 100644 vcl/unx/generic/gdi/salvd.cxx create mode 100644 vcl/unx/generic/glyphs/freetype_glyphcache.cxx create mode 100644 vcl/unx/generic/glyphs/glyphcache.cxx create mode 100644 vcl/unx/generic/print/genprnpsp.cxx create mode 100644 vcl/unx/generic/print/genpspgraphics.cxx create mode 100644 vcl/unx/generic/print/prtsetup.cxx create mode 100644 vcl/unx/generic/print/prtsetup.hxx create mode 100644 vcl/unx/generic/print/psheader.ps create mode 100644 vcl/unx/generic/printer/configuration/README create mode 100644 vcl/unx/generic/printer/configuration/ppds/SGENPRT.PS create mode 100644 vcl/unx/generic/printer/configuration/psprint.conf create mode 100644 vcl/unx/generic/printer/cpdmgr.cxx create mode 100644 vcl/unx/generic/printer/cupsmgr.cxx create mode 100644 vcl/unx/generic/printer/jobdata.cxx create mode 100644 vcl/unx/generic/printer/ppdparser.cxx create mode 100644 vcl/unx/generic/printer/printerinfomanager.cxx create mode 100644 vcl/unx/generic/window/salframe.cxx create mode 100644 vcl/unx/generic/window/salobj.cxx create mode 100644 vcl/unx/generic/window/sessioninhibitor.cxx create mode 100644 vcl/unx/gtk3/a11y/TODO create mode 100644 vcl/unx/gtk3/a11y/atkaction.cxx create mode 100644 vcl/unx/gtk3/a11y/atkbridge.cxx create mode 100644 vcl/unx/gtk3/a11y/atkcomponent.cxx create mode 100644 vcl/unx/gtk3/a11y/atkeditabletext.cxx create mode 100644 vcl/unx/gtk3/a11y/atkfactory.cxx create mode 100644 vcl/unx/gtk3/a11y/atkfactory.hxx create mode 100644 vcl/unx/gtk3/a11y/atkhypertext.cxx create mode 100644 vcl/unx/gtk3/a11y/atkimage.cxx create mode 100644 vcl/unx/gtk3/a11y/atklistener.cxx create mode 100644 vcl/unx/gtk3/a11y/atklistener.hxx create mode 100644 vcl/unx/gtk3/a11y/atkregistry.cxx create mode 100644 vcl/unx/gtk3/a11y/atkregistry.hxx create mode 100644 vcl/unx/gtk3/a11y/atkselection.cxx create mode 100644 vcl/unx/gtk3/a11y/atktable.cxx create mode 100644 vcl/unx/gtk3/a11y/atktablecell.cxx create mode 100644 vcl/unx/gtk3/a11y/atktext.cxx create mode 100644 vcl/unx/gtk3/a11y/atktextattributes.cxx create mode 100644 vcl/unx/gtk3/a11y/atktextattributes.hxx create mode 100644 vcl/unx/gtk3/a11y/atkutil.cxx create mode 100644 vcl/unx/gtk3/a11y/atkutil.hxx create mode 100644 vcl/unx/gtk3/a11y/atkvalue.cxx create mode 100644 vcl/unx/gtk3/a11y/atkwrapper.cxx create mode 100644 vcl/unx/gtk3/a11y/atkwrapper.hxx create mode 100644 vcl/unx/gtk3/customcellrenderer.cxx create mode 100644 vcl/unx/gtk3/customcellrenderer.hxx create mode 100644 vcl/unx/gtk3/fpicker/SalGtkFilePicker.cxx create mode 100644 vcl/unx/gtk3/fpicker/SalGtkFilePicker.hxx create mode 100644 vcl/unx/gtk3/fpicker/SalGtkFolderPicker.cxx create mode 100644 vcl/unx/gtk3/fpicker/SalGtkFolderPicker.hxx create mode 100644 vcl/unx/gtk3/fpicker/SalGtkPicker.cxx create mode 100644 vcl/unx/gtk3/fpicker/SalGtkPicker.hxx create mode 100644 vcl/unx/gtk3/fpicker/eventnotification.hxx create mode 100644 vcl/unx/gtk3/fpicker/resourceprovider.cxx create mode 100644 vcl/unx/gtk3/gloactiongroup.cxx create mode 100644 vcl/unx/gtk3/glomenu.cxx create mode 100644 vcl/unx/gtk3/gtkcairo.cxx create mode 100644 vcl/unx/gtk3/gtkcairo.hxx create mode 100644 vcl/unx/gtk3/gtkdata.cxx create mode 100644 vcl/unx/gtk3/gtkframe.cxx create mode 100644 vcl/unx/gtk3/gtkinst.cxx create mode 100644 vcl/unx/gtk3/gtkobject.cxx create mode 100644 vcl/unx/gtk3/gtksalmenu.cxx create mode 100644 vcl/unx/gtk3/gtksys.cxx create mode 100644 vcl/unx/gtk3/hudawareness.cxx create mode 100644 vcl/unx/gtk3/salnativewidgets-gtk.cxx create mode 100644 vcl/unx/gtk3_kde5/FPServiceInfo.hxx create mode 100644 vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkaction.cxx create mode 100644 vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkbridge.cxx create mode 100644 vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkcomponent.cxx create mode 100644 vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkeditabletext.cxx create mode 100644 vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkfactory.cxx create mode 100644 vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkhypertext.cxx create mode 100644 vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkimage.cxx create mode 100644 vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atklistener.cxx create mode 100644 vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkregistry.cxx create mode 100644 vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkselection.cxx create mode 100644 vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktable.cxx create mode 100644 vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktablecell.cxx create mode 100644 vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktext.cxx create mode 100644 vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktextattributes.cxx create mode 100644 vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkutil.cxx create mode 100644 vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkvalue.cxx create mode 100644 vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkwrapper.cxx create mode 100644 vcl/unx/gtk3_kde5/filepicker_ipc_commands.hxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_cairo.cxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_customcellrenderer.cxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_filepicker.cxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_filepicker.hxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_filepicker_ipc.cxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_filepicker_ipc.hxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_folderpicker.cxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_folderpicker.hxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_gloactiongroup.cxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_glomenu.cxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_gtkdata.cxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_gtkframe.cxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_gtkinst.cxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_gtkobject.cxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_gtksalmenu.cxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_gtksys.cxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_hudawareness.cxx create mode 100644 vcl/unx/gtk3_kde5/gtk3_kde5_salnativewidgets-gtk.cxx create mode 100644 vcl/unx/gtk3_kde5/kde5_filepicker.cxx create mode 100644 vcl/unx/gtk3_kde5/kde5_filepicker.hxx create mode 100644 vcl/unx/gtk3_kde5/kde5_filepicker_ipc.cxx create mode 100644 vcl/unx/gtk3_kde5/kde5_filepicker_ipc.hxx create mode 100644 vcl/unx/gtk3_kde5/kde5_lo_filepicker_main.cxx create mode 100644 vcl/unx/gtk4/a11y.cxx create mode 100644 vcl/unx/gtk4/a11y.hxx create mode 100644 vcl/unx/gtk4/convert3to4.cxx create mode 100644 vcl/unx/gtk4/convert3to4.hxx create mode 100644 vcl/unx/gtk4/customcellrenderer.cxx create mode 100644 vcl/unx/gtk4/fpicker/SalGtkFilePicker.cxx create mode 100644 vcl/unx/gtk4/fpicker/SalGtkFilePicker.hxx create mode 100644 vcl/unx/gtk4/fpicker/SalGtkFolderPicker.cxx create mode 100644 vcl/unx/gtk4/fpicker/SalGtkFolderPicker.hxx create mode 100644 vcl/unx/gtk4/fpicker/SalGtkPicker.cxx create mode 100644 vcl/unx/gtk4/fpicker/SalGtkPicker.hxx create mode 100644 vcl/unx/gtk4/fpicker/eventnotification.hxx create mode 100644 vcl/unx/gtk4/fpicker/resourceprovider.cxx create mode 100644 vcl/unx/gtk4/gloactiongroup.cxx create mode 100644 vcl/unx/gtk4/glomenu.cxx create mode 100644 vcl/unx/gtk4/gtkcairo.cxx create mode 100644 vcl/unx/gtk4/gtkcairo.hxx create mode 100644 vcl/unx/gtk4/gtkdata.cxx create mode 100644 vcl/unx/gtk4/gtkframe.cxx create mode 100644 vcl/unx/gtk4/gtkinst.cxx create mode 100644 vcl/unx/gtk4/gtkobject.cxx create mode 100644 vcl/unx/gtk4/gtksalmenu.cxx create mode 100644 vcl/unx/gtk4/gtksys.cxx create mode 100644 vcl/unx/gtk4/hudawareness.cxx create mode 100644 vcl/unx/gtk4/notifyinglayout.cxx create mode 100644 vcl/unx/gtk4/notifyinglayout.hxx create mode 100644 vcl/unx/gtk4/salnativewidgets-gtk.cxx create mode 100644 vcl/unx/gtk4/surfacecellrenderer.cxx create mode 100644 vcl/unx/gtk4/surfacecellrenderer.hxx create mode 100644 vcl/unx/gtk4/surfacepaintable.cxx create mode 100644 vcl/unx/gtk4/surfacepaintable.hxx create mode 100644 vcl/unx/gtk4/transferableprovider.cxx create mode 100644 vcl/unx/gtk4/transferableprovider.hxx create mode 100644 vcl/unx/kf5/KFFilePicker.cxx create mode 100644 vcl/unx/kf5/KFFilePicker.hxx create mode 100644 vcl/unx/kf5/KFSalInstance.cxx create mode 100644 vcl/unx/kf5/KFSalInstance.hxx create mode 100644 vcl/unx/kf6/KFFilePicker.cxx create mode 100644 vcl/unx/kf6/KFFilePicker.hxx create mode 100644 vcl/unx/kf6/KFSalInstance.cxx create mode 100644 vcl/unx/kf6/KFSalInstance.hxx create mode 100644 vcl/unx/x11/x11sys.cxx create mode 100644 vcl/unx/x11/xlimits.cxx (limited to 'vcl/unx') diff --git a/vcl/unx/generic/app/gendata.cxx b/vcl/unx/generic/app/gendata.cxx new file mode 100644 index 0000000000..79175217c8 --- /dev/null +++ b/vcl/unx/generic/app/gendata.cxx @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifdef IOS +#include +#endif + +#include + +#include + +#ifndef IOS + +#include +#include + +SalData::SalData() { SetSalData(this); } + +SalData::~SalData() {} + +#endif + +GenericUnixSalData::GenericUnixSalData() + : m_pDisplay(nullptr) +{ +} + +GenericUnixSalData::~GenericUnixSalData() +{ +#ifndef IOS + // at least for InitPrintFontManager the sequence is important + m_pPrintFontManager.reset(); + m_pFreetypeManager.reset(); + m_pPrinterInfoManager.reset(); +#endif +} + +void GenericUnixSalData::Dispose() {} + +#ifndef IOS +void GenericUnixSalData::InitFreetypeManager() { m_pFreetypeManager.reset(new FreetypeManager); } +#endif + +void GenericUnixSalData::InitPrintFontManager() +{ +#ifndef IOS + GetFreetypeManager(); + m_pPrintFontManager.reset(new psp::PrintFontManager); + m_pPrintFontManager->initialize(); +#endif +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/app/gendisp.cxx b/vcl/unx/generic/app/gendisp.cxx new file mode 100644 index 0000000000..b1dbef3f5f --- /dev/null +++ b/vcl/unx/generic/app/gendisp.cxx @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include + +SalGenericDisplay::SalGenericDisplay() +{ + m_pCapture = nullptr; +} + +SalGenericDisplay::~SalGenericDisplay() +{ +} + +void SalGenericDisplay::registerFrame( SalFrame* pFrame ) +{ + insertFrame( pFrame ); +} + +void SalGenericDisplay::deregisterFrame( SalFrame* pFrame ) +{ + eraseFrame( pFrame ); +} + +void SalGenericDisplay::emitDisplayChanged() +{ + SalFrame *pAnyFrame = anyFrame(); + if( pAnyFrame ) + pAnyFrame->CallCallback( SalEvent::DisplayChanged, nullptr ); +} + +bool SalGenericDisplay::DispatchInternalEvent( bool bHandleAllCurrentEvent ) +{ + return DispatchUserEvents( bHandleAllCurrentEvent ); +} + +void SalGenericDisplay::SendInternalEvent( SalFrame* pFrame, void* pData, SalEvent nEvent ) +{ + PostEvent( pFrame, pData, nEvent ); +} + +void SalGenericDisplay::CancelInternalEvent( SalFrame* pFrame, void* pData, SalEvent nEvent ) +{ + RemoveEvent( pFrame, pData, nEvent ); +} + +void SalGenericDisplay::ProcessEvent( SalUserEvent aEvent ) +{ + aEvent.m_pFrame->CallCallback( aEvent.m_nEvent, aEvent.m_pData ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/app/geninst.cxx b/vcl/unx/generic/app/geninst.cxx new file mode 100644 index 0000000000..191d87ca76 --- /dev/null +++ b/vcl/unx/generic/app/geninst.cxx @@ -0,0 +1,99 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#if defined(LINUX) +# include +#endif +#if defined(__FreeBSD__) +# include +#endif + +#include +#if HAVE_FEATURE_OPENGL +#include +#endif +#include +#include + +// SalYieldMutex + +SalYieldMutex::SalYieldMutex() +{ +#if HAVE_FEATURE_OPENGL + SetBeforeReleaseHandler( &OpenGLContext::prepareForYield ); +#endif +} + +SalYieldMutex::~SalYieldMutex() +{ +} + +SalGenericInstance::~SalGenericInstance() +{ +} + +OUString SalGenericInstance::getOSVersion() +{ + OUString aKernelVer = "unknown"; +#if defined(LINUX) + FILE* pVersion = fopen( "/proc/version", "r" ); + if ( pVersion ) + { + char aVerBuffer[512]; + if ( fgets ( aVerBuffer, 511, pVersion ) ) + { + aKernelVer = OUString::createFromAscii( aVerBuffer ); + // "Linux version 3.16.7-29-desktop ..." + std::u16string_view aVers = o3tl::getToken(aKernelVer, 2, ' ' ); + // "3.16.7-29-desktop ..." + size_t nTooDetailed = aVers.find( '.', 2); + if (nTooDetailed < 1 || nTooDetailed > 8) + aKernelVer = "Linux (misparsed version)"; + else // "3.16.7-29-desktop ..." + aKernelVer = OUString::Concat("Linux ") + aVers.substr(0, nTooDetailed); + } + fclose( pVersion ); + } +#elif defined(__FreeBSD__) + struct utsname stName; + if ( uname( &stName ) != 0 ) + return aKernelVer; + + sal_Int32 nDots = 0; + sal_Int32 nIndex = 0; + aKernelVer = OUString::createFromAscii( stName.release ); + while ( nIndex++ < aKernelVer.getLength() ) + { + const char c = stName.release[ nIndex ]; + if ( c == ' ' || c == '-' || ( c == '.' && nDots++ > 0 ) ) + break; + } + aKernelVer = OUString::createFromAscii(stName.sysname) + " " + aKernelVer.copy(0, nIndex); +#elif defined(EMSCRIPTEN) +#define str(s) #s +#define xstr(s) str(s) + aKernelVer = "Emscripten " xstr(__EMSCRIPTEN_major__) + "." xstr(__EMSCRIPTEN_minor__) "." xstr(__EMSCRIPTEN_tiny__); +#endif + return aKernelVer; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/app/gensys.cxx b/vcl/unx/generic/app/gensys.cxx new file mode 100644 index 0000000000..98371c5484 --- /dev/null +++ b/vcl/unx/generic/app/gensys.cxx @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include + +#include + +#include +#include +#include +#include +#include + +using namespace com::sun::star; + +SalGenericSystem::SalGenericSystem() +{ +} + +SalGenericSystem::~SalGenericSystem() +{ +} + +int SalGenericSystem::ShowNativeMessageBox( const OUString& rTitle, const OUString& rMessage ) +{ + std::vector< OUString > aButtons; + int nButtonIds[5] = {0}, nBut = 0; + + ImplHideSplash(); + + aButtons.push_back( "OK" ); + nButtonIds[nBut++] = SALSYSTEM_SHOWNATIVEMSGBOX_BTN_OK; + int nResult = ShowNativeDialog( rTitle, rMessage, aButtons ); + + return nResult != -1 ? nButtonIds[ nResult ] : 0; +} + +#if !defined(ANDROID) && !defined(IOS) + +// X11-specific + +const char* SalGenericSystem::getFrameResName() +{ + /* according to ICCCM: + * first search command line for -name parameter + * then try RESOURCE_NAME environment variable + * then use argv[0] stripped by directories + */ + static OStringBuffer aResName; + if( aResName.isEmpty() ) + { + int nArgs = osl_getCommandArgCount(); + for( int n = 0; n < nArgs-1; n++ ) + { + OUString aArg; + osl_getCommandArg( n, &aArg.pData ); + if( aArg.equalsIgnoreAsciiCase("-name") ) + { + osl_getCommandArg( n+1, &aArg.pData ); + aResName.append( OUStringToOString( aArg, osl_getThreadTextEncoding() ) ); + break; + } + } + if( aResName.isEmpty() ) + { + const char* pEnv = getenv( "RESOURCE_NAME" ); + if( pEnv && *pEnv ) + aResName.append( pEnv ); + } + if( aResName.isEmpty() ) + aResName.append( OUStringToOString( utl::ConfigManager::getProductName().toAsciiLowerCase(), + osl_getThreadTextEncoding())); + } + return aResName.getStr(); +} + +const char* SalGenericSystem::getFrameClassName() +{ + static OStringBuffer aClassName; + if( aClassName.isEmpty() ) + { + OUString aIni, aProduct; + rtl::Bootstrap::get( "BRAND_BASE_DIR", aIni ); + aIni += "/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE( "bootstrap" ); + rtl::Bootstrap aBootstrap( aIni ); + aBootstrap.getFrom( "ProductKey", aProduct ); + + if( !aProduct.isEmpty() ) + aClassName.append( OUStringToOString( aProduct, osl_getThreadTextEncoding() ) ); + else + aClassName.append( OUStringToOString( utl::ConfigManager::getProductName(), osl_getThreadTextEncoding())); + } + return aClassName.getStr(); +} + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/app/i18n_cb.cxx b/vcl/unx/generic/app/i18n_cb.cxx new file mode 100644 index 0000000000..c17c01a4d2 --- /dev/null +++ b/vcl/unx/generic/app/i18n_cb.cxx @@ -0,0 +1,494 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +// i. preedit start callback + +int +PreeditStartCallback ( XIC, XPointer client_data, XPointer ) +{ + preedit_data_t* pPreeditData = reinterpret_cast(client_data); + if ( pPreeditData->eState == PreeditStatus::ActivationRequired ) + { + pPreeditData->eState = PreeditStatus::Active; + pPreeditData->aText.nLength = 0; + } + + return -1; +} + +// ii. preedit done callback + +void +PreeditDoneCallback ( XIC, XPointer client_data, XPointer ) +{ + preedit_data_t* pPreeditData = reinterpret_cast(client_data); + if (pPreeditData->eState == PreeditStatus::Active ) + { + if( pPreeditData->pFrame ) + pPreeditData->pFrame->CallCallback( SalEvent::EndExtTextInput, nullptr ); + } + pPreeditData->eState = PreeditStatus::StartPending; +} + +// iii. preedit draw callback + +// Handle deletion of text in a preedit_draw_callback +// from and howmuch are guaranteed to be nonnegative + +static void +Preedit_DeleteText(preedit_text_t *ptext, int from, int howmuch) +{ + // If we've been asked to delete no text then just set + // nLength correctly and return + if (ptext->nLength == 0) + { + ptext->nLength = from; + return; + } + + int to = from + howmuch; + + if (to == static_cast(ptext->nLength)) + { + // delete from the end of the text + ptext->nLength = from; + } + else if (to < static_cast(ptext->nLength)) + { + // cut out of the middle of the text + memmove( static_cast(ptext->pUnicodeBuffer + from), + static_cast(ptext->pUnicodeBuffer + to), + (ptext->nLength - to) * sizeof(sal_Unicode)); + memmove( static_cast(ptext->pCharStyle + from), + static_cast(ptext->pCharStyle + to), + (ptext->nLength - to) * sizeof(XIMFeedback)); + ptext->nLength -= howmuch; + } + else + { + // XXX this indicates an error, are we out of sync ? + SAL_INFO("vcl.app", "Preedit_DeleteText( from=" << from + << " to=" << to + << " length=" << ptext->nLength + << " )."); + fprintf (stderr, "\t XXX internal error, out of sync XXX\n"); + + ptext->nLength = from; + } + + // NULL-terminate the string + ptext->pUnicodeBuffer[ptext->nLength] = u'\0'; +} + +// reallocate the textbuffer with sufficiently large size 2^x +// nnewlimit is presupposed to be larger than ptext->size +static void +enlarge_buffer ( preedit_text_t *ptext, int nnewlimit ) +{ + size_t nnewsize = ptext->nSize; + + while ( nnewsize <= o3tl::make_unsigned(nnewlimit) ) + nnewsize *= 2; + + ptext->nSize = nnewsize; + ptext->pUnicodeBuffer = static_cast(realloc(static_cast(ptext->pUnicodeBuffer), + nnewsize * sizeof(sal_Unicode))); + ptext->pCharStyle = static_cast(realloc(static_cast(ptext->pCharStyle), + nnewsize * sizeof(XIMFeedback))); +} + +// Handle insertion of text in a preedit_draw_callback +// string field of XIMText struct is guaranteed to be != NULL + +static void +Preedit_InsertText(preedit_text_t *pText, XIMText *pInsertText, int where) +{ + sal_Unicode *pInsertTextString; + int nInsertTextLength = 0; + XIMFeedback *pInsertTextCharStyle = pInsertText->feedback; + + nInsertTextLength = pInsertText->length; + + // can't handle wchar_t strings, so convert to multibyte chars first + char *pMBString; + size_t nMBLength; + if (pInsertText->encoding_is_wchar) + { + wchar_t *pWCString = pInsertText->string.wide_char; + size_t nBytes = wcstombs ( nullptr, pWCString, 0 /* don't care */); + pMBString = static_cast(alloca( nBytes + 1 )); + nMBLength = wcstombs ( pMBString, pWCString, nBytes + 1); + } + else + { + pMBString = pInsertText->string.multi_byte; + nMBLength = strlen(pMBString); // xxx + } + + // convert multibyte chars to unicode + rtl_TextEncoding nEncoding = osl_getThreadTextEncoding(); + + if (nEncoding != RTL_TEXTENCODING_UNICODE) + { + rtl_TextToUnicodeConverter aConverter = + rtl_createTextToUnicodeConverter( nEncoding ); + rtl_TextToUnicodeContext aContext = + rtl_createTextToUnicodeContext(aConverter); + + sal_Size nBufferSize = nInsertTextLength * 2; + + pInsertTextString = static_cast(alloca(nBufferSize)); + + sal_uInt32 nConversionInfo; + sal_Size nConvertedChars; + + rtl_convertTextToUnicode( aConverter, aContext, + pMBString, nMBLength, + pInsertTextString, nBufferSize, + RTL_TEXTTOUNICODE_FLAGS_UNDEFINED_IGNORE + | RTL_TEXTTOUNICODE_FLAGS_INVALID_IGNORE, + &nConversionInfo, &nConvertedChars ); + + rtl_destroyTextToUnicodeContext(aConverter, aContext); + rtl_destroyTextToUnicodeConverter(aConverter); + + } + else + { + pInsertTextString = reinterpret_cast(pMBString); + } + + // enlarge target text-buffer if necessary + if (pText->nSize <= (pText->nLength + nInsertTextLength)) + enlarge_buffer(pText, pText->nLength + nInsertTextLength); + + // insert text: displace old mem and put new bytes in + int from = where; + int to = where + nInsertTextLength; + int howmany = pText->nLength - where; + + memmove(static_cast(pText->pUnicodeBuffer + to), + static_cast(pText->pUnicodeBuffer + from), + howmany * sizeof(sal_Unicode)); + memmove(static_cast(pText->pCharStyle + to), + static_cast(pText->pCharStyle + from), + howmany * sizeof(XIMFeedback)); + + to = from; + howmany = nInsertTextLength; + + memcpy(static_cast(pText->pUnicodeBuffer + to), static_cast(pInsertTextString), + howmany * sizeof(sal_Unicode)); + memcpy(static_cast(pText->pCharStyle + to), static_cast(pInsertTextCharStyle), + howmany * sizeof(XIMFeedback)); + + pText->nLength += howmany; + + // NULL-terminate the string + pText->pUnicodeBuffer[pText->nLength] = u'\0'; +} + +// Handle the change of attributes in a preedit_draw_callback + +static void +Preedit_UpdateAttributes ( preedit_text_t* ptext, XIMFeedback const * feedback, + int from, int amount ) +{ + if ( (from + amount) > static_cast(ptext->nLength) ) + { + // XXX this indicates an error, are we out of sync ? + SAL_INFO("vcl.app", "Preedit_UpdateAttributes( " + << from << " + " << amount << " > " << ptext->nLength + << " )."); + fprintf (stderr, "\t XXX internal error, out of sync XXX\n"); + + return; + } + + memcpy ( ptext->pCharStyle + from, + feedback, amount * sizeof(XIMFeedback) ); +} + +// Convert the XIM feedback values into appropriate VCL +// EXTTEXTINPUT_ATTR values +// returns an allocate list of attributes, which must be freed by caller +static ExtTextInputAttr* +Preedit_FeedbackToSAL ( const XIMFeedback* pfeedback, int nlength, std::vector& rSalAttr ) +{ + ExtTextInputAttr *psalattr; + ExtTextInputAttr nval; + ExtTextInputAttr noldval = ExtTextInputAttr::NONE; + XIMFeedback nfeedback; + + // only work with reasonable length + if (nlength > 0 && nlength > sal::static_int_cast(rSalAttr.size()) ) + { + rSalAttr.reserve( nlength ); + psalattr = rSalAttr.data(); + } + else + return nullptr; + + for (int npos = 0; npos < nlength; npos++) + { + nval = ExtTextInputAttr::NONE; + nfeedback = pfeedback[npos]; + + // means to use the feedback of the previous char + if (nfeedback == 0) + { + nval = noldval; + } + // convert feedback to attributes + else + { + if (nfeedback & XIMReverse) + nval |= ExtTextInputAttr::Highlight; + if (nfeedback & XIMUnderline) + nval |= ExtTextInputAttr::Underline; + if (nfeedback & XIMHighlight) + nval |= ExtTextInputAttr::Highlight; + if (nfeedback & XIMPrimary) + nval |= ExtTextInputAttr::DottedUnderline; + if (nfeedback & XIMSecondary) + nval |= ExtTextInputAttr::DashDotUnderline; + if (nfeedback & XIMTertiary) // same as 2ery + nval |= ExtTextInputAttr::DashDotUnderline; + + } + // copy in list + psalattr[npos] = nval; + noldval = nval; + } + // return list of sal attributes + return psalattr; +} + +void +PreeditDrawCallback(XIC ic, XPointer client_data, + XIMPreeditDrawCallbackStruct *call_data) +{ + preedit_data_t* pPreeditData = reinterpret_cast(client_data); + + // if there's nothing to change then change nothing + if ( ( (call_data->text == nullptr) && (call_data->chg_length == 0) ) + || pPreeditData->pFrame == nullptr ) + return; + + // Solaris 7 deletes the preedit buffer after commit + // since the next call to preeditstart will have the same effect just skip this. + // if (pPreeditData->eState == ePreeditStatusStartPending && call_data->text == NULL) + // return; + + if ( pPreeditData->eState == PreeditStatus::StartPending ) + pPreeditData->eState = PreeditStatus::ActivationRequired; + PreeditStartCallback( ic, client_data, nullptr ); + + // Edit the internal textbuffer as indicated by the call_data, + // chg_first and chg_length are guaranteed to be nonnegative + + // handle text deletion + if (call_data->text == nullptr) + { + Preedit_DeleteText(&(pPreeditData->aText), + call_data->chg_first, call_data->chg_length ); + } + else + { + // handle text insertion + if ( (call_data->chg_length == 0) + && (call_data->text->string.wide_char != nullptr)) + { + Preedit_InsertText(&(pPreeditData->aText), call_data->text, + call_data->chg_first); + } + else if ( (call_data->chg_length != 0) + && (call_data->text->string.wide_char != nullptr)) + { + // handle text replacement by deletion and insertion of text, + // not smart, just good enough + + Preedit_DeleteText(&(pPreeditData->aText), + call_data->chg_first, call_data->chg_length); + Preedit_InsertText(&(pPreeditData->aText), call_data->text, + call_data->chg_first); + } + else if ( (call_data->chg_length != 0) + && (call_data->text->string.wide_char == nullptr)) + { + // not really a text update, only attributes are concerned + Preedit_UpdateAttributes(&(pPreeditData->aText), + call_data->text->feedback, + call_data->chg_first, call_data->chg_length); + } + } + + // build the SalExtTextInputEvent and send it up + + pPreeditData->aInputEv.mpTextAttr = Preedit_FeedbackToSAL( + pPreeditData->aText.pCharStyle, pPreeditData->aText.nLength, pPreeditData->aInputFlags); + pPreeditData->aInputEv.mnCursorPos = call_data->caret; + pPreeditData->aInputEv.maText = OUString(pPreeditData->aText.pUnicodeBuffer, + pPreeditData->aText.nLength); + pPreeditData->aInputEv.mnCursorFlags = 0; // default: make cursor visible + + if ( pPreeditData->eState == PreeditStatus::Active && pPreeditData->pFrame ) + pPreeditData->pFrame->CallCallback(SalEvent::ExtTextInput, static_cast(&pPreeditData->aInputEv)); + if (pPreeditData->aText.nLength == 0 && pPreeditData->pFrame ) + pPreeditData->pFrame->CallCallback( SalEvent::EndExtTextInput, nullptr ); + + if (pPreeditData->aText.nLength == 0) + pPreeditData->eState = PreeditStatus::StartPending; + + GetPreeditSpotLocation(ic, reinterpret_cast(pPreeditData)); +} + +void +GetPreeditSpotLocation(XIC ic, XPointer client_data) +{ + + // Send SalEventExtTextInputPos event to get spotlocation + + SalExtTextInputPosEvent aPosEvent; + preedit_data_t* pPreeditData = reinterpret_cast(client_data); + + if( pPreeditData->pFrame ) + pPreeditData->pFrame->CallCallback(SalEvent::ExtTextInputPos, static_cast(&aPosEvent)); + + XPoint point; + point.x = aPosEvent.mnX + aPosEvent.mnWidth; + point.y = aPosEvent.mnY + aPosEvent.mnHeight; + + XVaNestedList preedit_attr; + preedit_attr = XVaCreateNestedList(0, XNSpotLocation, &point, nullptr); + XSetICValues(ic, XNPreeditAttributes, preedit_attr, nullptr); + XFree(preedit_attr); +} + +// iv. preedit caret callback + +#if OSL_DEBUG_LEVEL > 1 +void +PreeditCaretCallback ( XIC ic, XPointer client_data, + XIMPreeditCaretCallbackStruct *call_data ) +{ + // XXX PreeditCaretCallback is pure debug code for now + const char *direction = "?"; + const char *style = "?"; + + switch ( call_data->style ) + { + case XIMIsInvisible: style = "Invisible"; break; + case XIMIsPrimary: style = "Primary"; break; + case XIMIsSecondary: style = "Secondary"; break; + } + switch ( call_data->direction ) + { + case XIMForwardChar: direction = "Forward char"; break; + case XIMBackwardChar: direction = "Backward char"; break; + case XIMForwardWord: direction = "Forward word"; break; + case XIMBackwardWord: direction = "Backward word"; break; + case XIMCaretUp: direction = "Caret up"; break; + case XIMCaretDown: direction = "Caret down"; break; + case XIMNextLine: direction = "Next line"; break; + case XIMPreviousLine: direction = "Previous line"; break; + case XIMLineStart: direction = "Line start"; break; + case XIMLineEnd: direction = "Line end"; break; + case XIMAbsolutePosition: direction = "Absolute"; break; + case XIMDontChange: direction = "Don't change"; break; + } + + SAL_INFO("vcl.app", "PreeditCaretCallback( ic=" << ic + << ", client=" << client_data + << ","); + SAL_INFO("vcl.app", "\t position=" << call_data->position + << ", direction=\"" << direction + << "\", style=\"" << style + << "\" )."); +} +#else +void +PreeditCaretCallback ( XIC, XPointer, XIMPreeditCaretCallbackStruct* ) +{ +} +#endif + +// v. commit string callback: convert an extended text input (iiimp ... ) +// into an ordinary key-event + +Bool +IsControlCode(sal_Unicode nChar) +{ + if ( nChar <= 0x1F /* C0 controls */ ) + return True; + else + return False; +} + +// vi. status callbacks: for now these are empty, they are just needed for turbo linux + +void +StatusStartCallback (XIC, XPointer, XPointer) +{ +} + +void +StatusDoneCallback (XIC, XPointer, XPointer) +{ +} + +void +StatusDrawCallback (XIC, XPointer, XIMStatusDrawCallbackStruct *) +{ +} + +// vii. destroy callbacks: internally disable all IC/IM calls + +void +IC_IMDestroyCallback (XIM, XPointer client_data, XPointer) +{ + SalI18N_InputContext *pContext = reinterpret_cast(client_data); + if (pContext != nullptr) + pContext->HandleDestroyIM(); +} + +void +IM_IMDestroyCallback (XIM, XPointer client_data, XPointer) +{ + SalI18N_InputMethod *pMethod = reinterpret_cast(client_data); + if (pMethod != nullptr) + pMethod->HandleDestroyIM(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/app/i18n_ic.cxx b/vcl/unx/generic/app/i18n_ic.cxx new file mode 100644 index 0000000000..32390a8888 --- /dev/null +++ b/vcl/unx/generic/app/i18n_ic.cxx @@ -0,0 +1,604 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include + +#include +#include + +#include + +using namespace vcl; + +static void sendEmptyCommit( SalFrame* pFrame ) +{ + vcl::DeletionListener aDel( pFrame ); + + SalExtTextInputEvent aEmptyEv; + aEmptyEv.mpTextAttr = nullptr; + aEmptyEv.maText.clear(); + aEmptyEv.mnCursorPos = 0; + aEmptyEv.mnCursorFlags = 0; + pFrame->CallCallback( SalEvent::ExtTextInput, static_cast(&aEmptyEv) ); + if( ! aDel.isDeleted() ) + pFrame->CallCallback( SalEvent::EndExtTextInput, nullptr ); +} + +// Constructor / Destructor, the InputContext is bound to the SalFrame, as it +// needs the shell window as a focus window + +SalI18N_InputContext::~SalI18N_InputContext() +{ + if ( maContext != nullptr ) + XDestroyIC( maContext ); + if ( mpAttributes != nullptr ) + XFree( mpAttributes ); + if ( mpStatusAttributes != nullptr ) + XFree( mpStatusAttributes ); + if ( mpPreeditAttributes != nullptr ) + XFree( mpPreeditAttributes ); + + if (maClientData.aText.pUnicodeBuffer != nullptr) + free(maClientData.aText.pUnicodeBuffer); + if (maClientData.aText.pCharStyle != nullptr) + free(maClientData.aText.pCharStyle); +} + +// convenience routine to add items to a XVaNestedList + +static XVaNestedList +XVaAddToNestedList( XVaNestedList a_srclist, char* name, XPointer value ) +{ + XVaNestedList a_dstlist; + + // if ( value == NULL ) + // return a_srclist; + + if ( a_srclist == nullptr ) + { + a_dstlist = XVaCreateNestedList( + 0, + name, value, + nullptr ); + } + else + { + a_dstlist = XVaCreateNestedList( + 0, + XNVaNestedList, a_srclist, + name, value, + nullptr ); + } + + return a_dstlist != nullptr ? a_dstlist : a_srclist ; +} + +// convenience routine to create a fontset + +static XFontSet +get_font_set( Display *p_display ) +{ + static XFontSet p_font_set = nullptr; + + if (p_font_set == nullptr) + { + char **pp_missing_list; + int n_missing_count; + char *p_default_string; + + p_font_set = XCreateFontSet(p_display, "-*", + &pp_missing_list, &n_missing_count, &p_default_string); + } + + return p_font_set; +} + +const XIMStyle g_nSupportedStatusStyle( + XIMStatusCallbacks | + XIMStatusNothing | + XIMStatusNone + ); + +// Constructor for an InputContext (IC) + +SalI18N_InputContext::SalI18N_InputContext ( SalFrame *pFrame ) : + mbUseable( True ), + maContext( nullptr ), + mnSupportedPreeditStyle( + XIMPreeditCallbacks | + XIMPreeditNothing | + XIMPreeditNone + ), + mnStatusStyle( 0 ), + mnPreeditStyle( 0 ), + mpAttributes( nullptr ), + mpStatusAttributes( nullptr ), + mpPreeditAttributes( nullptr ) +{ +#ifdef __sun + static const char* pIIIMPEnable = getenv( "SAL_DISABLE_OWN_IM_STATUS" ); + if( pIIIMPEnable && *pIIIMPEnable ) + mnSupportedStatusStyle &= ~XIMStatusCallbacks; +#endif + + memset(&maPreeditStartCallback, 0, sizeof(maPreeditStartCallback)); + memset(&maPreeditDoneCallback, 0, sizeof(maPreeditDoneCallback)); + memset(&maPreeditDrawCallback, 0, sizeof(maPreeditDrawCallback)); + memset(&maPreeditCaretCallback, 0, sizeof(maPreeditCaretCallback)); + memset(&maCommitStringCallback, 0, sizeof(maCommitStringCallback)); + memset(&maSwitchIMCallback, 0, sizeof(maSwitchIMCallback)); + memset(&maDestroyCallback, 0, sizeof(maDestroyCallback)); + + maClientData.aText.pUnicodeBuffer = nullptr; + maClientData.aText.pCharStyle = nullptr; + maClientData.aInputEv.mpTextAttr = nullptr; + maClientData.aInputEv.mnCursorPos = 0; + maClientData.aInputEv.mnCursorFlags = 0; + + SalI18N_InputMethod *pInputMethod; + pInputMethod = vcl_sal::getSalDisplay(GetGenericUnixSalData())->GetInputMethod(); + + mnSupportedPreeditStyle = XIMPreeditCallbacks | XIMPreeditPosition + | XIMPreeditNothing | XIMPreeditNone; + if (pInputMethod->UseMethod() + && SupportInputMethodStyle( pInputMethod->GetSupportedStyles() ) ) + { + const SystemEnvData* pEnv = pFrame->GetSystemData(); + ::Window aClientWindow = pEnv->aShellWindow; + ::Window aFocusWindow = pEnv->GetWindowHandle(pFrame); + + // for status callbacks and commit string callbacks +#define PREEDIT_BUFSZ 16 + maClientData.eState = PreeditStatus::StartPending; + maClientData.pFrame = pFrame; + maClientData.aText.pUnicodeBuffer = + static_cast(malloc(PREEDIT_BUFSZ * sizeof(sal_Unicode))); + maClientData.aText.pCharStyle = + static_cast(malloc(PREEDIT_BUFSZ * sizeof(XIMFeedback))); + maClientData.aText.nSize = PREEDIT_BUFSZ; + maClientData.aText.nLength = 0; + + // Status attributes + + switch ( mnStatusStyle ) + { + case XIMStatusCallbacks: + { + static XIMCallback aStatusStartCallback; + static XIMCallback aStatusDoneCallback; + static XIMCallback aStatusDrawCallback; + + aStatusStartCallback.callback = reinterpret_cast(StatusStartCallback); + aStatusStartCallback.client_data = reinterpret_cast(&maClientData); + aStatusDoneCallback.callback = reinterpret_cast(StatusDoneCallback); + aStatusDoneCallback.client_data = reinterpret_cast(&maClientData); + aStatusDrawCallback.callback = reinterpret_cast(StatusDrawCallback); + aStatusDrawCallback.client_data = reinterpret_cast(&maClientData); + + mpStatusAttributes = XVaCreateNestedList ( + 0, + XNStatusStartCallback, &aStatusStartCallback, + XNStatusDoneCallback, &aStatusDoneCallback, + XNStatusDrawCallback, &aStatusDrawCallback, + nullptr ); + + break; + } + + case XIMStatusArea: + /* not supported */ + break; + + case XIMStatusNone: + case XIMStatusNothing: + default: + /* no arguments needed */ + break; + } + + // set preedit attributes + + switch ( mnPreeditStyle ) + { + case XIMPreeditCallbacks: + + maPreeditCaretCallback.callback = reinterpret_cast(PreeditCaretCallback); + maPreeditStartCallback.callback = reinterpret_cast(PreeditStartCallback); + maPreeditDoneCallback.callback = reinterpret_cast(PreeditDoneCallback); + maPreeditDrawCallback.callback = reinterpret_cast(PreeditDrawCallback); + maPreeditCaretCallback.client_data = reinterpret_cast(&maClientData); + maPreeditStartCallback.client_data = reinterpret_cast(&maClientData); + maPreeditDoneCallback.client_data = reinterpret_cast(&maClientData); + maPreeditDrawCallback.client_data = reinterpret_cast(&maClientData); + + mpPreeditAttributes = XVaCreateNestedList ( + 0, + XNPreeditStartCallback, &maPreeditStartCallback, + XNPreeditDoneCallback, &maPreeditDoneCallback, + XNPreeditDrawCallback, &maPreeditDrawCallback, + XNPreeditCaretCallback, &maPreeditCaretCallback, + nullptr ); + + break; + + case XIMPreeditArea: + /* not supported */ + break; + + case XIMPreeditPosition: + { + // spot location + SalExtTextInputPosEvent aPosEvent; + pFrame->CallCallback(SalEvent::ExtTextInputPos, static_cast(&aPosEvent)); + + static XPoint aSpot; + aSpot.x = aPosEvent.mnX + aPosEvent.mnWidth; + aSpot.y = aPosEvent.mnY + aPosEvent.mnHeight; + + // create attributes for preedit position style + mpPreeditAttributes = XVaCreateNestedList ( + 0, + XNSpotLocation, &aSpot, + nullptr ); + + // XCreateIC() fails on Redflag Linux 2.0 if there is no + // fontset though the data itself is not evaluated nor is + // it required according to the X specs. + Display* pDisplay = vcl_sal::getSalDisplay(GetGenericUnixSalData())->GetDisplay(); + XFontSet pFontSet = get_font_set(pDisplay); + + if (pFontSet != nullptr) + { + mpPreeditAttributes = XVaAddToNestedList( mpPreeditAttributes, + const_cast(XNFontSet), reinterpret_cast(pFontSet)); + } + + break; + } + + case XIMPreeditNone: + case XIMPreeditNothing: + default: + /* no arguments needed */ + break; + } + + // Create the InputContext by giving it exactly the information it + // deserves, because inappropriate attributes + // let XCreateIC fail on Solaris (eg. for C locale) + + mpAttributes = XVaCreateNestedList( + 0, + XNFocusWindow, aFocusWindow, + XNClientWindow, aClientWindow, + XNInputStyle, mnPreeditStyle | mnStatusStyle, + nullptr ); + + if ( mnPreeditStyle != XIMPreeditNone ) + { +#if defined LINUX || defined FREEBSD || defined NETBSD || defined OPENBSD || defined DRAGONFLY + if ( mpPreeditAttributes != nullptr ) +#endif + mpAttributes = XVaAddToNestedList( mpAttributes, + const_cast(XNPreeditAttributes), static_cast(mpPreeditAttributes) ); + } + if ( mnStatusStyle != XIMStatusNone ) + { +#if defined LINUX || defined FREEBSD || defined NETBSD || defined OPENBSD || defined DRAGONFLY + if ( mpStatusAttributes != nullptr ) +#endif + mpAttributes = XVaAddToNestedList( mpAttributes, + const_cast(XNStatusAttributes), static_cast(mpStatusAttributes) ); + } + maContext = XCreateIC( pInputMethod->GetMethod(), + XNVaNestedList, mpAttributes, + nullptr ); + } + + if ( maContext == nullptr ) + { +#if OSL_DEBUG_LEVEL > 1 + SAL_WARN("vcl.app", "input context creation failed."); +#endif + + mbUseable = False; + + if ( mpAttributes != nullptr ) + XFree( mpAttributes ); + if ( mpStatusAttributes != nullptr ) + XFree( mpStatusAttributes ); + if ( mpPreeditAttributes != nullptr ) + XFree( mpPreeditAttributes ); + if ( maClientData.aText.pUnicodeBuffer != nullptr ) + free ( maClientData.aText.pUnicodeBuffer ); + if ( maClientData.aText.pCharStyle != nullptr ) + free ( maClientData.aText.pCharStyle ); + + mpAttributes = nullptr; + mpStatusAttributes = nullptr; + mpPreeditAttributes = nullptr; + maClientData.aText.pUnicodeBuffer = nullptr; + maClientData.aText.pCharStyle = nullptr; + } + + if ( maContext != nullptr) + { + maDestroyCallback.callback = IC_IMDestroyCallback; + maDestroyCallback.client_data = reinterpret_cast(this); + XSetICValues( maContext, + XNDestroyCallback, &maDestroyCallback, + nullptr ); + } +} + +// In Solaris 8 the status window does not unmap if the frame unmapps, so +// unmap it the hard way + +void +SalI18N_InputContext::Unmap() +{ + UnsetICFocus(); + maClientData.pFrame = nullptr; +} + +void +SalI18N_InputContext::Map( SalFrame *pFrame ) +{ + if( !mbUseable ) + return; + + if( !pFrame ) + return; + + if ( maContext == nullptr ) + { + SalI18N_InputMethod *pInputMethod; + pInputMethod = vcl_sal::getSalDisplay(GetGenericUnixSalData())->GetInputMethod(); + + maContext = XCreateIC( pInputMethod->GetMethod(), + XNVaNestedList, mpAttributes, + nullptr ); + } + if( maClientData.pFrame != pFrame ) + SetICFocus( pFrame ); +} + +// Handle DestroyCallbacks +// in fact this is a callback called from the XNDestroyCallback + +void +SalI18N_InputContext::HandleDestroyIM() +{ + maContext = nullptr; // don't change + mbUseable = False; +} + +// make sure, the input method gets all the X-Events it needs, this is only +// called once on each frame, it relies on a valid maContext + +void +SalI18N_InputContext::ExtendEventMask( ::Window aFocusWindow ) +{ + unsigned long nIMEventMask; + XWindowAttributes aWindowAttributes; + + if ( mbUseable ) + { + Display *pDisplay = XDisplayOfIM( XIMOfIC(maContext) ); + + XGetWindowAttributes( pDisplay, aFocusWindow, + &aWindowAttributes ); + XGetICValues ( maContext, + XNFilterEvents, &nIMEventMask, + nullptr); + nIMEventMask |= aWindowAttributes.your_event_mask; + XSelectInput ( pDisplay, aFocusWindow, nIMEventMask ); + } +} + +// tune the styles provided by the input method with the supported one + +unsigned int +SalI18N_InputContext::GetWeightingOfIMStyle( XIMStyle nStyle ) +{ + struct StyleWeightingT { + const XIMStyle nStyle; + const unsigned int nWeight; + }; + + StyleWeightingT const *pWeightPtr; + static const StyleWeightingT pWeight[] = { + { XIMPreeditCallbacks, 0x10000000 }, + { XIMPreeditPosition, 0x02000000 }, + { XIMPreeditArea, 0x01000000 }, + { XIMPreeditNothing, 0x00100000 }, + { XIMPreeditNone, 0x00010000 }, + { XIMStatusCallbacks, 0x1000 }, + { XIMStatusArea, 0x0100 }, + { XIMStatusNothing, 0x0010 }, + { XIMStatusNone, 0x0001 }, + { 0, 0x0 } + }; + + int nWeight = 0; + for ( pWeightPtr = pWeight; pWeightPtr->nStyle != 0; pWeightPtr++ ) + { + if ( (pWeightPtr->nStyle & nStyle) != 0 ) + nWeight += pWeightPtr->nWeight; + } + return nWeight; +} + +bool +SalI18N_InputContext::IsSupportedIMStyle( XIMStyle nStyle ) const +{ + return (nStyle & mnSupportedPreeditStyle) + && (nStyle & g_nSupportedStatusStyle); +} + +bool +SalI18N_InputContext::SupportInputMethodStyle( XIMStyles const *pIMStyles ) +{ + mnPreeditStyle = 0; + mnStatusStyle = 0; + + if ( pIMStyles != nullptr ) + { + int nBestScore = 0; + int nActualScore = 0; + + // check whether the XIM supports one of the desired styles + // only a single preedit and a single status style must occur + // in an input method style. Hideki said so, so i trust him + for ( int nStyle = 0; nStyle < pIMStyles->count_styles; nStyle++ ) + { + XIMStyle nProvidedStyle = pIMStyles->supported_styles[ nStyle ]; + if ( IsSupportedIMStyle(nProvidedStyle) ) + { + nActualScore = GetWeightingOfIMStyle( nProvidedStyle ); + if ( nActualScore >= nBestScore ) + { + nBestScore = nActualScore; + mnPreeditStyle = nProvidedStyle & mnSupportedPreeditStyle; + mnStatusStyle = nProvidedStyle & g_nSupportedStatusStyle; + } + } + } + } + + return (mnPreeditStyle != 0) && (mnStatusStyle != 0) ; +} + +// handle extended and normal key input + +void +SalI18N_InputContext::CommitKeyEvent(sal_Unicode const * pText, std::size_t nLength) +{ + if (nLength == 1 && IsControlCode(pText[0])) + return; + + if( maClientData.pFrame ) + { + SalExtTextInputEvent aTextEvent; + + aTextEvent.mpTextAttr = nullptr; + aTextEvent.mnCursorPos = nLength; + aTextEvent.maText = OUString(pText, nLength); + aTextEvent.mnCursorFlags = 0; + + maClientData.pFrame->CallCallback(SalEvent::ExtTextInput, static_cast(&aTextEvent)); + maClientData.pFrame->CallCallback(SalEvent::EndExtTextInput, nullptr); + } +#if OSL_DEBUG_LEVEL > 1 + else + SAL_WARN("vcl.app", "CommitKeyEvent without frame."); +#endif +} + +int +SalI18N_InputContext::UpdateSpotLocation() +{ + if (maContext == nullptr || maClientData.pFrame == nullptr) + return -1; + + SalExtTextInputPosEvent aPosEvent; + maClientData.pFrame->CallCallback(SalEvent::ExtTextInputPos, static_cast(&aPosEvent)); + + XPoint aSpot; + aSpot.x = aPosEvent.mnX + aPosEvent.mnWidth; + aSpot.y = aPosEvent.mnY + aPosEvent.mnHeight; + + XVaNestedList preedit_attr = XVaCreateNestedList(0, XNSpotLocation, &aSpot, nullptr); + XSetICValues(maContext, XNPreeditAttributes, preedit_attr, nullptr); + XFree(preedit_attr); + + return 0; +} + +// set and unset the focus for the Input Context +// the context may be NULL despite it is usable if the framewindow is +// in unmapped state + +void +SalI18N_InputContext::SetICFocus( SalFrame* pFocusFrame ) +{ + if ( !(mbUseable && (maContext != nullptr)) ) + return; + + maClientData.pFrame = pFocusFrame; + + const SystemEnvData* pEnv = pFocusFrame->GetSystemData(); + ::Window aClientWindow = pEnv->aShellWindow; + ::Window aFocusWindow = pEnv->GetWindowHandle(pFocusFrame); + + XSetICValues( maContext, + XNFocusWindow, aFocusWindow, + XNClientWindow, aClientWindow, + nullptr ); + + if( maClientData.aInputEv.mpTextAttr ) + { + sendEmptyCommit(pFocusFrame); + // begin preedit again + vcl_sal::getSalDisplay(GetGenericUnixSalData())->SendInternalEvent( pFocusFrame, &maClientData.aInputEv, SalEvent::ExtTextInput ); + } + + XSetICFocus( maContext ); +} + +void +SalI18N_InputContext::UnsetICFocus() +{ + + if ( mbUseable && (maContext != nullptr) ) + { + // cancel an eventual event posted to begin preedit again + vcl_sal::getSalDisplay(GetGenericUnixSalData())->CancelInternalEvent( maClientData.pFrame, &maClientData.aInputEv, SalEvent::ExtTextInput ); + maClientData.pFrame = nullptr; + XUnsetICFocus( maContext ); + } +} + +// multi byte input method only + +void +SalI18N_InputContext::EndExtTextInput() +{ + if ( !mbUseable || (maContext == nullptr) || !maClientData.pFrame ) + return; + + vcl::DeletionListener aDel( maClientData.pFrame ); + // delete preedit in sal (commit an empty string) + sendEmptyCommit( maClientData.pFrame ); + if( ! aDel.isDeleted() ) + { + // mark previous preedit state again (will e.g. be sent at focus gain) + maClientData.aInputEv.mpTextAttr = maClientData.aInputFlags.data(); + if( static_cast(maClientData.pFrame)->hasFocus() ) + { + // begin preedit again + vcl_sal::getSalDisplay(GetGenericUnixSalData())->SendInternalEvent( maClientData.pFrame, &maClientData.aInputEv, SalEvent::ExtTextInput ); + } + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/app/i18n_im.cxx b/vcl/unx/generic/app/i18n_im.cxx new file mode 100644 index 0000000000..6a655ca39e --- /dev/null +++ b/vcl/unx/generic/app/i18n_im.cxx @@ -0,0 +1,410 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include + +#ifdef LINUX +# ifndef __USE_XOPEN +# define __USE_XOPEN +# endif +#endif + +#include + +#include + +#include +#include +#include + +#include + +using namespace vcl; + +// kinput2 IME needs special key handling since key release events are filtered in +// preeditmode and XmbResetIC does not work + +namespace { + +class XKeyEventOp : public XKeyEvent +{ + private: + void init(); + + public: + XKeyEventOp(); + + XKeyEventOp& operator= (const XKeyEvent &rEvent); + void erase (); + bool match (const XKeyEvent &rEvent) const; +}; + +} + +void +XKeyEventOp::init() +{ + type = 0; /* serial = 0; */ + send_event = 0; display = nullptr; + window = 0; root = 0; + subwindow = 0; /* time = 0; */ + /* x = 0; y = 0; */ + /* x_root = 0; y_root = 0; */ + state = 0; keycode = 0; + same_screen = 0; +} + +XKeyEventOp::XKeyEventOp() +{ + init(); +} + +XKeyEventOp& +XKeyEventOp::operator= (const XKeyEvent &rEvent) +{ + type = rEvent.type; /* serial = rEvent.serial; */ + send_event = rEvent.send_event; display = rEvent.display; + window = rEvent.window; root = rEvent.root; + subwindow = rEvent.subwindow;/* time = rEvent.time; */ + /* x = rEvent.x, y = rEvent.y; */ + /* x_root = rEvent.x_root, y_root = rEvent.y_root; */ + state = rEvent.state; keycode = rEvent.keycode; + same_screen = rEvent.same_screen; + + return *this; +} + +void +XKeyEventOp::erase () +{ + init(); +} + +bool +XKeyEventOp::match (const XKeyEvent &rEvent) const +{ + return ( (type == KeyPress && rEvent.type == KeyRelease) + || (type == KeyRelease && rEvent.type == KeyPress )) + /* && serial == rEvent.serial */ + && send_event == rEvent.send_event + && display == rEvent.display + && window == rEvent.window + && root == rEvent.root + && subwindow == rEvent.subwindow + /* && time == rEvent.time + && x == rEvent.x + && y == rEvent.y + && x_root == rEvent.x_root + && y_root == rEvent.y_root */ + && state == rEvent.state + && keycode == rEvent.keycode + && same_screen == rEvent.same_screen; +} + +// locale handling + +// Locale handling of the operating system layer + +static char* +SetSystemLocale( const char* p_inlocale ) +{ + char *p_outlocale = setlocale(LC_ALL, p_inlocale); + + SAL_WARN_IF(p_outlocale == nullptr, "vcl.app", + "I18N: Operating system doesn't support locale \"" + << p_inlocale << "\"."); + + return p_outlocale; +} + +#ifdef __sun +static void +SetSystemEnvironment( const OUString& rLocale ) +{ + OUString LC_ALL_Var("LC_ALL"); + osl_setEnvironment(LC_ALL_Var.pData, rLocale.pData); + + OUString LANG_Var("LANG"); + osl_setEnvironment(LANG_Var.pData, rLocale.pData); +} +#endif + +static Bool +IsPosixLocale( const char* p_locale ) +{ + if ( p_locale == nullptr ) + return False; + if ( (p_locale[ 0 ] == 'C') && (p_locale[ 1 ] == '\0') ) + return True; + if ( strncmp(p_locale, "POSIX", sizeof("POSIX")) == 0 ) + return True; + + return False; +} + +// Locale handling of the X Window System layer + +static Bool +IsXWindowCompatibleLocale( const char* p_locale ) +{ + if ( p_locale == nullptr ) + return False; + + if ( !XSupportsLocale() ) + { + SAL_WARN("vcl.app", + "I18N: X Window System doesn't support locale \"" + << p_locale << "\"."); + return False; + } + return True; +} + +// Set the operating system locale prior to trying to open an +// XIM InputMethod. +// Handle the cases where the current locale is either not supported by the +// operating system (LANG=gaga) or by the XWindow system (LANG=aa_ER@saaho) +// by providing a fallback. +// Upgrade "C" or "POSIX" to "en_US" locale to allow umlauts and accents +// see i8988, i9188, i8930, i16318 +// on Solaris the environment needs to be set equivalent to the locale (#i37047#) + +void +SalI18N_InputMethod::SetLocale() +{ + // check whether we want an Input Method engine, if we don't we + // do not need to set the locale + if ( !mbUseable ) + return; + + char *locale = SetSystemLocale( "" ); + if ( (!IsXWindowCompatibleLocale(locale)) || IsPosixLocale(locale) ) + { + osl_setThreadTextEncoding (RTL_TEXTENCODING_ISO_8859_1); + locale = SetSystemLocale( "en_US" ); +#ifdef __sun + SetSystemEnvironment( "en_US" ); +#endif + if (! IsXWindowCompatibleLocale(locale)) + { + locale = SetSystemLocale( "C" ); +#ifdef __sun + SetSystemEnvironment( "C" ); +#endif + if (! IsXWindowCompatibleLocale(locale)) + mbUseable = False; + } + } + + // must not fail if mbUseable since XSupportsLocale() asserts success + if ( mbUseable && XSetLocaleModifiers("") == nullptr ) + { + SAL_WARN("vcl.app", + "I18N: Can't set X modifiers for locale \"" + << locale << "\"."); + mbUseable = False; + } +} + +Bool +SalI18N_InputMethod::PosixLocale() +{ + if (maMethod) + return IsPosixLocale (XLocaleOfIM (maMethod)); + return False; +} + +// Constructor / Destructor / Initialisation + +SalI18N_InputMethod::SalI18N_InputMethod( ) + : mbUseable( bUseInputMethodDefault ) + , maMethod( nullptr ) + , mpStyles( nullptr ) +{ + maDestroyCallback.callback = nullptr; + maDestroyCallback.client_data = nullptr; + const char *pUseInputMethod = getenv( "SAL_USEINPUTMETHOD" ); + if ( pUseInputMethod != nullptr ) + mbUseable = pUseInputMethod[0] != '\0' ; +} + +SalI18N_InputMethod::~SalI18N_InputMethod() +{ + if ( mpStyles != nullptr ) + XFree( mpStyles ); + if ( maMethod != nullptr ) + XCloseIM ( maMethod ); +} + +// XXX +// debug routine: lets have a look at the provided method styles + +#if OSL_DEBUG_LEVEL > 1 + +extern "C" char* +GetMethodName( XIMStyle nStyle, char *pBuf, int nBufSize) +{ + struct StyleName { + const XIMStyle nStyle; + const char *pName; + const int nNameLen; + }; + + StyleName *pDescPtr; + static const StyleName pDescription[] = { + { XIMPreeditArea, "PreeditArea ", sizeof("PreeditArea ") }, + { XIMPreeditCallbacks, "PreeditCallbacks ",sizeof("PreeditCallbacks ")}, + { XIMPreeditPosition, "PreeditPosition ", sizeof("PreeditPosition ") }, + { XIMPreeditNothing, "PreeditNothing ", sizeof("PreeditNothing ") }, + { XIMPreeditNone, "PreeditNone ", sizeof("PreeditNone ") }, + { XIMStatusArea, "StatusArea ", sizeof("StatusArea ") }, + { XIMStatusCallbacks, "StatusCallbacks ", sizeof("StatusCallbacks ") }, + { XIMStatusNothing, "StatusNothing ", sizeof("StatusNothing ") }, + { XIMStatusNone, "StatusNone ", sizeof("StatusNone ") }, + { 0, "NULL", 0 } + }; + + if ( nBufSize > 0 ) + pBuf[0] = '\0'; + + char *pBufPtr = pBuf; + for ( pDescPtr = const_cast(pDescription); pDescPtr->nStyle != 0; pDescPtr++ ) + { + int nSize = pDescPtr->nNameLen - 1; + if ( (nStyle & pDescPtr->nStyle) && (nBufSize > nSize) ) + { + strncpy( pBufPtr, pDescPtr->pName, nSize + 1); + pBufPtr += nSize; + nBufSize -= nSize; + } + } + + return pBuf; +} + +extern "C" void +PrintInputStyle( XIMStyles *pStyle ) +{ + char pBuf[ 128 ]; + int nBuf = sizeof( pBuf ); + + if ( pStyle == NULL ) + SAL_INFO("vcl.app", "no input method styles."); + else + for ( int nStyle = 0; nStyle < pStyle->count_styles; nStyle++ ) + { + SAL_INFO("vcl.app", "style #" + << nStyle + << " = " + << GetMethodName(pStyle->supported_styles[nStyle], pBuf, nBuf)); + } +} + +#endif + +// this is the real constructing routine, since locale setting has to be done +// prior to xopendisplay, the xopenim call has to be delayed + +void +SalI18N_InputMethod::CreateMethod ( Display *pDisplay ) +{ + if ( mbUseable ) + { + maMethod = XOpenIM(pDisplay, nullptr, nullptr, nullptr); + + if ((maMethod == nullptr) && (getenv("XMODIFIERS") != nullptr)) + { + OUString envVar("XMODIFIERS"); + osl_clearEnvironment(envVar.pData); + XSetLocaleModifiers(""); + maMethod = XOpenIM(pDisplay, nullptr, nullptr, nullptr); + } + + if ( maMethod != nullptr ) + { + if ( XGetIMValues(maMethod, XNQueryInputStyle, &mpStyles, nullptr) + != nullptr) + mbUseable = False; +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.app", "Creating Mono-Lingual InputMethod."); + PrintInputStyle( mpStyles ); +#endif + } + else + { + mbUseable = False; + } + } + +#if OSL_DEBUG_LEVEL > 1 + SAL_WARN_IF(!mbUseable, "vcl.app", "input method creation failed."); +#endif + + maDestroyCallback.callback = IM_IMDestroyCallback; + maDestroyCallback.client_data = reinterpret_cast(this); + if (mbUseable && maMethod != nullptr) + XSetIMValues(maMethod, XNDestroyCallback, &maDestroyCallback, nullptr); +} + +// give IM the opportunity to look at the event, and possibly hide it + +bool +SalI18N_InputMethod::FilterEvent( XEvent *pEvent, ::Window window ) +{ + if (!mbUseable) + return False; + + bool bFilterEvent = XFilterEvent (pEvent, window); + + if (pEvent->type != KeyPress && pEvent->type != KeyRelease) + return bFilterEvent; + + /* + * fix broken key release handling of some IMs + */ + XKeyEvent* pKeyEvent = &(pEvent->xkey); + static XKeyEventOp s_aLastKeyPress; + + if (bFilterEvent) + { + if (pKeyEvent->type == KeyRelease) + bFilterEvent = !s_aLastKeyPress.match (*pKeyEvent); + s_aLastKeyPress.erase(); + } + else /* (!bFilterEvent) */ + { + if (pKeyEvent->type == KeyPress) + s_aLastKeyPress = *pKeyEvent; + else + s_aLastKeyPress.erase(); + } + + return bFilterEvent; +} + +void +SalI18N_InputMethod::HandleDestroyIM() +{ + mbUseable = False; + maMethod = nullptr; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/app/i18n_keysym.cxx b/vcl/unx/generic/app/i18n_keysym.cxx new file mode 100644 index 0000000000..a77632a3e7 --- /dev/null +++ b/vcl/unx/generic/app/i18n_keysym.cxx @@ -0,0 +1,358 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include + +#include + +// convert keysyms to unicode +// for all keysyms with byte1 and byte2 equal zero, and of course only for +// keysyms that have a unicode counterpart + +namespace { + +struct keymap_t { + const int first; const int last; + const sal_Unicode *map; +}; + +} + +// Latin-1 Byte 3 = 0x00 +const sal_Unicode keymap00_map[] = { + 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027, + 0x0028, 0x0029, 0x002a, 0x002b, 0x002c, 0x002d, 0x002e, 0x002f, + 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, + 0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, 0x003f, + 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, + 0x0048, 0x0049, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f, + 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, + 0x0058, 0x0059, 0x005a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f, + 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, + 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f, + 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, + 0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, 0x007e, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x00a0, 0x00a1, 0x00a2, 0x00a3, 0x00a4, 0x00a5, 0x00a6, 0x00a7, + 0x00a8, 0x00a9, 0x00aa, 0x00ab, 0x00ac, 0x00ad, 0x00ae, 0x00af, + 0x00b0, 0x00b1, 0x00b2, 0x00b3, 0x00b4, 0x00b5, 0x00b6, 0x00b7, + 0x00b8, 0x00b9, 0x00ba, 0x00bb, 0x00bc, 0x00bd, 0x00be, 0x00bf, + 0x00c0, 0x00c1, 0x00c2, 0x00c3, 0x00c4, 0x00c5, 0x00c6, 0x00c7, + 0x00c8, 0x00c9, 0x00ca, 0x00cb, 0x00cc, 0x00cd, 0x00ce, 0x00cf, + 0x00d0, 0x00d1, 0x00d2, 0x00d3, 0x00d4, 0x00d5, 0x00d6, 0x00d7, + 0x00d8, 0x00d9, 0x00da, 0x00db, 0x00dc, 0x00dd, 0x00de, 0x00df, + 0x00e0, 0x00e1, 0x00e2, 0x00e3, 0x00e4, 0x00e5, 0x00e6, 0x00e7, + 0x00e8, 0x00e9, 0x00ea, 0x00eb, 0x00ec, 0x00ed, 0x00ee, 0x00ef, + 0x00f0, 0x00f1, 0x00f2, 0x00f3, 0x00f4, 0x00f5, 0x00f6, 0x00f7, + 0x00f8, 0x00f9, 0x00fa, 0x00fb, 0x00fc, 0x00fd, 0x00fe, 0x00ff }; +const keymap_t keymap00 = { 32, 255, keymap00_map }; + +// Latin-2 Byte 3 = 0x01 +const sal_Unicode keymap01_map[] = { + 0x0104, 0x02d8, 0x0141, 0x0000, 0x013d, 0x015a, 0x0000, 0x0000, + 0x0160, 0x015e, 0x0164, 0x0179, 0x0000, 0x017d, 0x017b, 0x0000, + 0x0105, 0x02db, 0x0142, 0x0000, 0x013e, 0x015b, 0x02c7, 0x0000, + 0x0161, 0x015f, 0x0165, 0x017a, 0x02dd, 0x017e, 0x017c, 0x0154, + 0x0000, 0x0000, 0x0102, 0x0000, 0x0139, 0x0106, 0x0000, 0x010c, + 0x0000, 0x0118, 0x0000, 0x011a, 0x0000, 0x0000, 0x010e, 0x0110, + 0x0143, 0x0147, 0x0000, 0x0000, 0x0150, 0x0000, 0x0000, 0x0158, + 0x016e, 0x0000, 0x0170, 0x0000, 0x0000, 0x0162, 0x0000, 0x0155, + 0x0000, 0x0000, 0x0103, 0x0000, 0x013a, 0x0107, 0x0000, 0x010d, + 0x0000, 0x0119, 0x0000, 0x011b, 0x0000, 0x0000, 0x010f, 0x0111, + 0x0144, 0x0148, 0x0000, 0x0000, 0x0151, 0x0000, 0x0000, 0x0159, + 0x016f, 0x0000, 0x0171, 0x0000, 0x0000, 0x0163, 0x02d9 }; +const keymap_t keymap01 = { 161, 255, keymap01_map }; + +// Latin-3 Byte 3 = 0x02 +const sal_Unicode keymap02_map[] = { + 0x0126, 0x0000, 0x0000, 0x0000, 0x0000, 0x0124, 0x0000, 0x0000, + 0x0130, 0x0000, 0x011e, 0x0134, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0127, 0x0000, 0x0000, 0x0000, 0x0000, 0x0125, 0x0000, 0x0000, + 0x0131, 0x0000, 0x011f, 0x0135, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x010a, 0x0108, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0120, 0x0000, 0x0000, 0x011c, + 0x0000, 0x0000, 0x0000, 0x0000, 0x016c, 0x015c, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x010b, 0x0109, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0121, 0x0000, 0x0000, 0x011d, + 0x0000, 0x0000, 0x0000, 0x0000, 0x016d, 0x015d }; +const keymap_t keymap02 = { 161, 254, keymap02_map }; + +// Latin-4 Byte 3 = 0x03 +const sal_Unicode keymap03_map[] = { + 0x0138, 0x0156, 0x0000, 0x0128, 0x013b, 0x0000, 0x0000, 0x0000, + 0x0112, 0x0122, 0x0166, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0157, 0x0000, 0x0129, 0x013c, 0x0000, 0x0000, 0x0000, + 0x0113, 0x0123, 0x0167, 0x014a, 0x0000, 0x014b, 0x0100, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x012e, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0116, 0x0000, 0x0000, 0x012a, 0x0000, 0x0145, + 0x014c, 0x0136, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0172, + 0x0000, 0x0000, 0x0000, 0x0168, 0x016a, 0x0000, 0x0101, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x012f, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0117, 0x0000, 0x0000, 0x012b, 0x0000, 0x0146, + 0x014d, 0x0137, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0173, + 0x0000, 0x0000, 0x0000, 0x0169, 0x016b }; +const keymap_t keymap03 = { 162, 254, keymap03_map }; + +// Kana Byte 3 = 0x04 +const sal_Unicode keymap04_map[] = { + 0x203e, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x3002, 0x300c, 0x300d, 0x3001, 0x30fb, + 0x30f2, 0x30a1, 0x30a3, 0x30a5, 0x30a7, 0x30a9, 0x30e3, 0x30e5, + 0x30e7, 0x30c3, 0x30fc, 0x30a2, 0x30a4, 0x30a6, 0x30a8, 0x30aa, + 0x30ab, 0x30ad, 0x30af, 0x30b1, 0x30b3, 0x30b5, 0x30b7, 0x30b9, + 0x30bb, 0x30bd, 0x30bf, 0x30c1, 0x30c4, 0x30c6, 0x30c8, 0x30ca, + 0x30cb, 0x30cc, 0x30cd, 0x30ce, 0x30cf, 0x30d2, 0x30d5, 0x30d8, + 0x30db, 0x30de, 0x30df, 0x30e0, 0x30e1, 0x30e2, 0x30e4, 0x30e6, + 0x30e8, 0x30e9, 0x30ea, 0x30eb, 0x30ec, 0x30ed, 0x30ef, 0x30f3, + 0x309b, 0x309c }; +const keymap_t keymap04 = { 126, 223, keymap04_map }; + +// Arabic Byte 3 = 0x05 +const sal_Unicode keymap05_map[] = { + 0x060c, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x061b, + 0x0000, 0x0000, 0x0000, 0x061f, 0x0000, 0x0621, 0x0622, 0x0623, + 0x0624, 0x0625, 0x0626, 0x0627, 0x0628, 0x0629, 0x062a, 0x062b, + 0x062c, 0x062d, 0x062e, 0x062f, 0x0630, 0x0631, 0x0632, 0x0633, + 0x0634, 0x0635, 0x0636, 0x0637, 0x0638, 0x0639, 0x063a, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0640, 0x0641, 0x0642, 0x0643, + 0x0644, 0x0645, 0x0646, 0x0647, 0x0648, 0x0649, 0x064a, 0x064b, + 0x064c, 0x064d, 0x064e, 0x064f, 0x0650, 0x0651, 0x0652 }; +const keymap_t keymap05 = { 172, 242, keymap05_map }; + +// Cyrillic Byte 3 = 0x06 +const sal_Unicode keymap06_map[] = { + 0x0452, 0x0453, 0x0451, 0x0454, 0x0455, 0x0456, 0x0457, 0x0458, + 0x0459, 0x045a, 0x045b, 0x045c, 0x0000, 0x045e, 0x045f, 0x2116, + 0x0402, 0x0403, 0x0401, 0x0404, 0x0405, 0x0406, 0x0407, 0x0408, + 0x0409, 0x040a, 0x040b, 0x040c, 0x0000, 0x040e, 0x040f, 0x044e, + 0x0430, 0x0431, 0x0446, 0x0434, 0x0435, 0x0444, 0x0433, 0x0445, + 0x0438, 0x0439, 0x043a, 0x043b, 0x043c, 0x043d, 0x043e, 0x043f, + 0x044f, 0x0440, 0x0441, 0x0442, 0x0443, 0x0436, 0x0432, 0x044c, + 0x044b, 0x0437, 0x0448, 0x044d, 0x0449, 0x0447, 0x044a, 0x042e, + 0x0410, 0x0411, 0x0426, 0x0414, 0x0415, 0x0424, 0x0413, 0x0425, + 0x0418, 0x0419, 0x041a, 0x041b, 0x041c, 0x041d, 0x041e, 0x041f, + 0x042f, 0x0420, 0x0421, 0x0422, 0x0423, 0x0416, 0x0412, 0x042c, + 0x042b, 0x0417, 0x0428, 0x042d, 0x0429, 0x0427, 0x042a }; +const keymap_t keymap06 = { 161, 255, keymap06_map }; + +// Greek Byte 3 = 0x07 +const sal_Unicode keymap07_map[] = { + 0x0386, 0x0388, 0x0389, 0x038a, 0x03aa, 0x0000, 0x038c, 0x038e, + 0x03ab, 0x0000, 0x038f, 0x0000, 0x0000, 0x0385, 0x2015, 0x0000, + 0x03ac, 0x03ad, 0x03ae, 0x03af, 0x03ca, 0x0390, 0x03cc, 0x03cd, + 0x03cb, 0x03b0, 0x03ce, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, + 0x0399, 0x039a, 0x039b, 0x039c, 0x039d, 0x039e, 0x039f, 0x03a0, + 0x03a1, 0x03a3, 0x0000, 0x03a4, 0x03a5, 0x03a6, 0x03a7, 0x03a8, + 0x03a9, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x03b1, 0x03b2, 0x03b3, 0x03b4, 0x03b5, 0x03b6, 0x03b7, 0x03b8, + 0x03b9, 0x03ba, 0x03bb, 0x03bc, 0x03bd, 0x03be, 0x03bf, 0x03c0, + 0x03c1, 0x03c3, 0x03c2, 0x03c4, 0x03c5, 0x03c6, 0x03c7, 0x03c8, + 0x03c9 }; +const keymap_t keymap07 = { 161, 249, keymap07_map }; + +// Technical Byte 3 = 0x08 +const sal_Unicode keymap08_map[] = { + 0x23b7, 0x250c, 0x2500, 0x2320, 0x2321, 0x2502, 0x23a1, 0x23a3, + 0x23a4, 0x23a6, 0x239b, 0x239d, 0x239e, 0x23a0, 0x23a8, 0x23ac, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x2264, 0x2260, 0x2265, 0x222b, 0x2234, + 0x221d, 0x221e, 0x0000, 0x0000, 0x2207, 0x0000, 0x0000, 0x223c, + 0x2243, 0x0000, 0x0000, 0x0000, 0x21d4, 0x21d2, 0x2261, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x221a, 0x0000, 0x0000, + 0x0000, 0x2282, 0x2283, 0x2229, 0x222a, 0x2227, 0x2228, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x2202, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0192, 0x0000, 0x0000, + 0x0000, 0x0000, 0x2190, 0x2191, 0x2192, 0x2193 }; +const keymap_t keymap08 = { 161, 254, keymap08_map }; + +// Special Byte 3 = 0x09 +const sal_Unicode keymap09_map[] = { + 0x25c6, 0x2592, 0x2409, 0x240c, 0x240d, 0x240a, 0x0000, 0x0000, + 0x2424, 0x240b, 0x2518, 0x2510, 0x250c, 0x2514, 0x253c, 0x23ba, + 0x23bb, 0x2500, 0x23bc, 0x23bd, 0x251c, 0x2524, 0x2534, 0x252c, + 0x2502 }; +const keymap_t keymap09 = { 224, 248, keymap09_map }; + +// Publishing Byte 3 = 0x0a = 10 +const sal_Unicode keymap10_map[] = { + 0x2003, 0x2002, 0x2004, 0x2005, 0x2007, 0x2008, 0x2009, 0x200a, + 0x2014, 0x2013, 0x0000, 0x0000, 0x0000, 0x2026, 0x2025, 0x2153, + 0x2154, 0x2155, 0x2156, 0x2157, 0x2158, 0x2159, 0x215a, 0x2105, + 0x0000, 0x0000, 0x2012, 0x2329, 0x0000, 0x232a, 0x0000, 0x0000, + 0x0000, 0x0000, 0x215b, 0x215c, 0x215d, 0x215e, 0x0000, 0x0000, + 0x2122, 0x2613, 0x0000, 0x25c1, 0x25b7, 0x25cb, 0x25af, 0x2018, + 0x2019, 0x201c, 0x201d, 0x211e, 0x0000, 0x2032, 0x2033, 0x0000, + 0x271d, 0x0000, 0x25ac, 0x25c0, 0x25b6, 0x25cf, 0x25ae, 0x25e6, + 0x25ab, 0x25ad, 0x25b3, 0x25bd, 0x2606, 0x2022, 0x25aa, 0x25b2, + 0x25bc, 0x261c, 0x261e, 0x2663, 0x2666, 0x2665, 0x0000, 0x2720, + 0x2020, 0x2021, 0x2713, 0x2717, 0x266f, 0x266d, 0x2642, 0x2640, + 0x260e, 0x2315, 0x2117, 0x2038, 0x201a, 0x201e }; +const keymap_t keymap10 = { 161, 254, keymap10_map }; + +// APL Byte 3 = 0x0b = 11 +const sal_Unicode keymap11_map[] = { + 0x003c, 0x0000, 0x0000, 0x003e, 0x0000, 0x2228, 0x2227, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x00af, 0x0000, 0x22a5, + 0x2229, 0x230a, 0x0000, 0x005f, 0x0000, 0x0000, 0x0000, 0x2218, + 0x0000, 0x2395, 0x0000, 0x22a4, 0x25cb, 0x0000, 0x0000, 0x0000, + 0x2308, 0x0000, 0x0000, 0x222a, 0x0000, 0x2283, 0x0000, 0x2282, + 0x0000, 0x22a2, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x22a3 }; +const keymap_t keymap11 = { 163, 252, keymap11_map }; + +// Hebrew Byte 3 = 0x0c = 12 +const sal_Unicode keymap12_map[] = { + 0x2017, 0x05d0, 0x05d1, 0x05d2, 0x05d3, 0x05d4, 0x05d5, 0x05d6, + 0x05d7, 0x05d8, 0x05d9, 0x05da, 0x05db, 0x05dc, 0x05dd, 0x05de, + 0x05df, 0x05e0, 0x05e1, 0x05e2, 0x05e3, 0x05e4, 0x05e5, 0x05e6, + 0x05e7, 0x05e8, 0x05e9, 0x05ea }; +const keymap_t keymap12 = { 223, 250, keymap12_map }; + +// Thai Byte 3 = 0x0d = 13 +const sal_Unicode keymap13_map[] = { + 0x0e01, 0x0e02, 0x0e03, 0x0e04, 0x0e05, 0x0e06, 0x0e07, 0x0e08, + 0x0e09, 0x0e0a, 0x0e0b, 0x0e0c, 0x0e0d, 0x0e0e, 0x0e0f, 0x0e10, + 0x0e11, 0x0e12, 0x0e13, 0x0e14, 0x0e15, 0x0e16, 0x0e17, 0x0e18, + 0x0e19, 0x0e1a, 0x0e1b, 0x0e1c, 0x0e1d, 0x0e1e, 0x0e1f, 0x0e20, + 0x0e21, 0x0e22, 0x0e23, 0x0e24, 0x0e25, 0x0e26, 0x0e27, 0x0e28, + 0x0e29, 0x0e2a, 0x0e2b, 0x0e2c, 0x0e2d, 0x0e2e, 0x0e2f, 0x0e30, + 0x0e31, 0x0e32, 0x0e33, 0x0e34, 0x0e35, 0x0e36, 0x0e37, 0x0e38, + 0x0e39, 0x0e3a, 0x0000, 0x0000, 0x0000, 0x0000, 0x0e3f, 0x0e40, + 0x0e41, 0x0e42, 0x0e43, 0x0e44, 0x0e45, 0x0e46, 0x0e47, 0x0e48, + 0x0e49, 0x0e4a, 0x0e4b, 0x0e4c, 0x0e4d, 0x0000, 0x0000, 0x0e50, + 0x0e51, 0x0e52, 0x0e53, 0x0e54, 0x0e55, 0x0e56, 0x0e57, 0x0e58, + 0x0e59 }; +const keymap_t keymap13 = { 161, 249, keymap13_map }; + +// Korean Byte 3 = 0x0e = 14 +const sal_Unicode keymap14_map[] = { + 0x3131, 0x3132, 0x3133, 0x3134, 0x3135, 0x3136, 0x3137, 0x3138, + 0x3139, 0x313a, 0x313b, 0x313c, 0x313d, 0x313e, 0x313f, 0x3140, + 0x3141, 0x3142, 0x3143, 0x3144, 0x3145, 0x3146, 0x3147, 0x3148, + 0x3149, 0x314a, 0x314b, 0x314c, 0x314d, 0x314e, 0x314f, 0x3150, + 0x3151, 0x3152, 0x3153, 0x3154, 0x3155, 0x3156, 0x3157, 0x3158, + 0x3159, 0x315a, 0x315b, 0x315c, 0x315d, 0x315e, 0x315f, 0x3160, + 0x3161, 0x3162, 0x3163, 0x11a8, 0x11a9, 0x11aa, 0x11ab, 0x11ac, + 0x11ad, 0x11ae, 0x11af, 0x11b0, 0x11b1, 0x11b2, 0x11b3, 0x11b4, + 0x11b5, 0x11b6, 0x11b7, 0x11b8, 0x11b9, 0x11ba, 0x11bb, 0x11bc, + 0x11bd, 0x11be, 0x11bf, 0x11c0, 0x11c1, 0x11c2, 0x316d, 0x3171, + 0x3178, 0x317f, 0x3181, 0x3184, 0x3186, 0x318d, 0x318e, 0x11eb, + 0x11f0, 0x11f9, 0x0000, 0x0000, 0x0000, 0x0000, 0x20a9 }; +const keymap_t keymap14 = { 161, 255, keymap14_map }; + +// missing: +// Latin-8 Byte 3 = 0x12 = 18 + +// Latin-9 Byte 3 = 0x13 = 19 +const sal_Unicode keymap19_map[] = { + 0x0152, 0x0153, 0x0178 }; +const keymap_t keymap19 = { 188, 190, keymap19_map }; + +// missing: +// Armenian Byte 3 = 0x14 = 20 +// Georgian Byte 3 = 0x15 = 21 +// Azeri Byte 3 = 0x16 = 22 +// Vietnamese Byte 3 = 0x1e = 30 + +// Currency Byte 3 = 0x20 = 32 +const sal_Unicode keymap32_map[] = { + 0x20a0, 0x20a1, 0x20a2, 0x20a3, 0x20a4, 0x20a5, 0x20a6, 0x20a7, + 0x20a8, 0x0000, 0x20aa, 0x20ab, 0x20ac }; +const keymap_t keymap32 = { 160, 172, keymap32_map }; + +// Keyboard (Keypad mappings) Byte 3 = 0xff = 255 +const sal_Unicode keymap255_map[] = { + 0x0020, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x002a, 0x002b, 0x002c, 0x002d, 0x002e, 0x002f, + 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, + 0x0038, 0x0039, 0x0000, 0x0000, 0x0000, 0x003d }; +const keymap_t keymap255 = { 128, 189, keymap255_map }; + +#define INITIAL_KEYMAPS 33 +const keymap_t* const p_keymap[INITIAL_KEYMAPS] = { + &keymap00, &keymap01, &keymap02, &keymap03, /* 00 -- 03 */ + &keymap04, &keymap05, &keymap06, &keymap07, /* 04 -- 07 */ + &keymap08, &keymap09, &keymap10, &keymap11, /* 08 -- 11 */ + &keymap12, &keymap13, &keymap14, nullptr, /* 12 -- 15 */ + nullptr, nullptr, nullptr, &keymap19, /* 16 -- 19 */ + nullptr, nullptr, nullptr, nullptr, /* 20 -- 23 */ + nullptr, nullptr, nullptr, nullptr, /* 24 -- 27 */ + nullptr, nullptr, nullptr, nullptr, /* 28 -- 31 */ + &keymap32 /* 32 */ +}; + +sal_Unicode +KeysymToUnicode (KeySym nKeySym) +{ + // keysym is already unicode + if ((nKeySym & 0xff000000) == 0x01000000) + { + // strip off group indicator and iso10646 plane + // FIXME can't handle chars from surrogate area. + if (! (nKeySym & 0x00ff0000) ) + return static_cast(nKeySym & 0x0000ffff); + } + // legacy keysyms, switch to appropriate codeset + else + { + unsigned char n_byte1 = (nKeySym & 0xff000000) >> 24; + unsigned char n_byte2 = (nKeySym & 0x00ff0000) >> 16; + unsigned char n_byte3 = (nKeySym & 0x0000ff00) >> 8; + unsigned char n_byte4 = (nKeySym & 0x000000ff); + + if (n_byte1 != 0) + return 0; + if (n_byte2 != 0) + return 0; + + keymap_t const* p_map = nullptr; + if (n_byte3 < INITIAL_KEYMAPS) + p_map = p_keymap[n_byte3]; + else if (n_byte3 == 255) + p_map = &keymap255; + + if ((p_map != nullptr) && (n_byte4 >= p_map->first) && (n_byte4 <= p_map->last) ) + return p_map->map[n_byte4 - p_map->first]; + } + + return 0; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/app/i18n_xkb.cxx b/vcl/unx/generic/app/i18n_xkb.cxx new file mode 100644 index 0000000000..0fc4d7933f --- /dev/null +++ b/vcl/unx/generic/app/i18n_xkb.cxx @@ -0,0 +1,107 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include + +#include + +#include +#include + +#include + +SalI18N_KeyboardExtension::SalI18N_KeyboardExtension( Display* pDisplay ) + : mbUseExtension(true) + , mnEventBase(0) +{ + // allow user to set the default keyboard group idx or to disable the usage + // of x keyboard extension at all: + // setenv SAL_XKEYBOARDGROUP disables keyboard extension + // setenv SAL_XKEYBOARDGROUP 2 sets the keyboard group index to 2 + // keyboard group index must be in [1,4], may be specified in hex or decimal + static char *pUseKeyboardExtension = getenv( "SAL_XKEYBOARDGROUP" ); + if ( pUseKeyboardExtension != nullptr ) + { + mbUseExtension = pUseKeyboardExtension[0] != '\0' ; + } + + // query XServer support for XKB Extension, + // do not call XQueryExtension() / XInitExtension() due to possible version + // clashes ! + if ( mbUseExtension ) + { + int nMajorExtOpcode; + int nExtMajorVersion = XkbMajorVersion; + int nExtMinorVersion = XkbMinorVersion; + int nErrorBase = 0; + + mbUseExtension = XkbQueryExtension( pDisplay, + &nMajorExtOpcode, &mnEventBase, &nErrorBase, + &nExtMajorVersion, &nExtMinorVersion ) != 0; + } + + // query notification for changes of the keyboard group + if ( mbUseExtension ) + { + constexpr auto XkbGroupMask = XkbGroupStateMask | XkbGroupBaseMask + | XkbGroupLatchMask | XkbGroupLockMask; + + mbUseExtension = XkbSelectEventDetails( pDisplay, + XkbUseCoreKbd, XkbStateNotify, XkbGroupMask, XkbGroupMask ); + } + + // query initial keyboard group + if ( mbUseExtension ) + { + XkbStateRec aStateRecord; + XkbGetState( pDisplay, XkbUseCoreKbd, &aStateRecord ); + } +} + +void +SalI18N_KeyboardExtension::Dispatch( XEvent* pEvent ) +{ + // must the event be handled? + if ( !mbUseExtension + || (pEvent->type != mnEventBase) ) + return; + + // only handle state notify events for now, and only interested + // in group details + sal_uInt32 nXKBType = reinterpret_cast(pEvent)->xkb_type; + switch ( nXKBType ) + { + case XkbStateNotify: + break; + + default: +#if OSL_DEBUG_LEVEL > 1 + SAL_WARN("vcl.app", "Got unrequested XkbAnyEvent " + << std::hex << std::showbase + << static_cast(nXKBType) + << "/" << std::dec + << static_cast(nXKBType)); +#endif + break; + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/app/keysymnames.cxx b/vcl/unx/generic/app/keysymnames.cxx new file mode 100644 index 0000000000..d4842df955 --- /dev/null +++ b/vcl/unx/generic/app/keysymnames.cxx @@ -0,0 +1,509 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include +#include + +#if !defined (SunXK_Undo) +#define SunXK_Undo 0x0000FF65 // XK_Undo +#define SunXK_Again 0x0000FF66 // XK_Redo +#define SunXK_Find 0x0000FF68 // XK_Find +#define SunXK_Stop 0x0000FF69 // XK_Cancel +#define SunXK_Props 0x1005FF70 +#define SunXK_Front 0x1005FF71 +#define SunXK_Copy 0x1005FF72 +#define SunXK_Open 0x1005FF73 +#define SunXK_Paste 0x1005FF74 +#define SunXK_Cut 0x1005FF75 +#endif + +#include +#include + +namespace vcl_sal { + + namespace { + + struct KeysymNameReplacement + { + KeySym aSymbol; + const char* pName; + }; + + struct KeyboardReplacements + { + const char* pLangName; + const KeysymNameReplacement* pReplacements; + int nReplacements; + }; + + } + + // CAUTION CAUTION CAUTION + // every string value in the replacements tables must be in UTF8 + // be careful with your editor ! + + const struct KeysymNameReplacement aImplReplacements_English[] = + { + { XK_Control_L, "Ctrl" }, + { XK_Control_R, "Ctrl" }, + { XK_Escape, "Esc" }, + { XK_space, "Space" }, + { XK_Page_Up, "PgUp"}, + { XK_Page_Down, "PgDn"}, + { XK_grave, "`"} + }; + + const struct KeysymNameReplacement aImplReplacements_Turkish[] = + { + { XK_Control_L, "Ctrl" }, + { XK_Control_R, "Ctrl" }, + { XK_Right, "Sa\304\237" }, + { XK_Left, "Sol" }, + { XK_Up, "Yukar\304\261" }, + { XK_Down, "A\305\237a\304\237\304\261" }, + { XK_space, "Bo\305\237luk" } + }; + + const struct KeysymNameReplacement aImplReplacements_Russian[] = + { + { XK_Right, "\320\222\320\277\321\200\320\260\320\262\320\276" }, + { XK_Left, "\320\222\320\273\320\265\320\262\320\276" }, + { XK_Up, "\320\222\320\262\320\265\321\200\321\205" }, + { XK_Down, "\320\222\320\275\320\270\320\267" }, + { XK_space, "\320\237\321\200\320\276\320\261\320\265\320\273" } + }; + + const struct KeysymNameReplacement aImplReplacements_German[] = + { + { XK_Control_L, "Strg" }, + { XK_Control_R, "Strg" }, + { XK_Shift_L, "Umschalt" }, + { XK_Shift_R, "Umschalt" }, + { XK_Alt_L, "Alt" }, + { XK_Alt_R, "Alt Gr" }, + { XK_Page_Up, "Bild auf" }, + { XK_Page_Down, "Bild ab" }, + { XK_End, "Ende" }, + { XK_Home, "Pos 1" }, + { XK_Insert, "Einfg" }, + { XK_Delete, "Entf" }, + { XK_Escape, "Esc" }, + { XK_Right, "Rechts" }, + { XK_Left, "Links" }, + { XK_Up, "Oben" }, + { XK_Down, "Unten" }, + { XK_BackSpace, "R\303\274ckschritt" }, + { XK_Return, "Eingabe" }, + { XK_slash, "Schr\303\244gstrich" }, + { XK_space, "Leertaste" }, + { SunXK_Stop, "Stop" }, + { SunXK_Again, "Wiederholen" }, + { SunXK_Props, "Eigenschaften" }, + { SunXK_Undo, "Zur\303\274cknehmen" }, + { SunXK_Front, "Vordergrund" }, + { SunXK_Copy, "Kopieren" }, + { SunXK_Open, "\303\226ffnen" }, + { SunXK_Paste, "Einsetzen" }, + { SunXK_Find, "Suchen" }, + { SunXK_Cut, "Ausschneiden" }, + }; + + const struct KeysymNameReplacement aImplReplacements_French[] = + { + { XK_Shift_L, "Maj" }, + { XK_Shift_R, "Maj" }, + { XK_Page_Up, "Pg. Pr\303\251c" }, + { XK_Page_Down, "Pg. Suiv" }, + { XK_End, "Fin" }, + { XK_Home, "Origine" }, + { XK_Insert, "Ins\303\251rer" }, + { XK_Delete, "Suppr" }, + { XK_Escape, "Esc" }, + { XK_Right, "Droite" }, + { XK_Left, "Gauche" }, + { XK_Up, "Haut" }, + { XK_Down, "Bas" }, + { XK_BackSpace, "Ret. Arr" }, + { XK_Return, "Retour" }, + { XK_space, "Espace" }, + { XK_KP_Enter, "Entr\303\251e" }, + { SunXK_Stop, "Stop" }, + { SunXK_Again, "Encore" }, + { SunXK_Props, "Props" }, + { SunXK_Undo, "Annuler" }, + { SunXK_Front, "Devant" }, + { SunXK_Copy, "Copy" }, + { SunXK_Open, "Ouvrir" }, + { SunXK_Paste, "Coller" }, + { SunXK_Find, "Cher." }, + { SunXK_Cut, "Couper" }, + }; + + const struct KeysymNameReplacement aImplReplacements_Italian[] = + { + { XK_Shift_L, "Maiusc" }, + { XK_Shift_R, "Maiusc" }, + { XK_Page_Up, "PgSu" }, + { XK_Page_Down, "PgGiu" }, + { XK_End, "Fine" }, + { XK_Insert, "Ins" }, + { XK_Delete, "Canc" }, + { XK_Escape, "Esc" }, + { XK_Right, "A destra" }, + { XK_Left, "A sinistra" }, + { XK_Up, "Sposta verso l'alto" }, + { XK_Down, "Sposta verso il basso" }, + { XK_BackSpace, "Backspace" }, + { XK_Return, "Invio" }, + { XK_space, "Spazio" }, + { SunXK_Stop, "Stop" }, + { SunXK_Again, "Ancora" }, + { SunXK_Props, "Propriet\303\240" }, + { SunXK_Undo, "Annulla" }, + { SunXK_Front, "Davanti" }, + { SunXK_Copy, "Copia" }, + { SunXK_Open, "Apri" }, + { SunXK_Paste, "Incolla" }, + { SunXK_Find, "Trova" }, + { SunXK_Cut, "Taglia" }, + }; + + const struct KeysymNameReplacement aImplReplacements_Dutch[] = + { + { XK_Page_Up, "PageUp" }, + { XK_Page_Down, "PageDown" }, + { XK_Escape, "Esc" }, + { XK_Right, "Rechts" }, + { XK_Left, "Links" }, + { XK_Up, "Boven" }, + { XK_Down, "Onder" }, + { XK_BackSpace, "Backspace" }, + { XK_Return, "Return" }, + { XK_space, "Spatiebalk" }, + { SunXK_Stop, "Stop" }, + { SunXK_Again, "Again" }, + { SunXK_Props, "Props" }, + { SunXK_Undo, "Undo" }, + { SunXK_Front, "Front" }, + { SunXK_Copy, "Copy" }, + { SunXK_Open, "Open" }, + { SunXK_Paste, "Paste" }, + { SunXK_Find, "Find" }, + { SunXK_Cut, "Cut" }, + }; + + const struct KeysymNameReplacement aImplReplacements_Norwegian[] = + { + { XK_Shift_L, "Skift" }, + { XK_Shift_R, "Skift" }, + { XK_Page_Up, "PageUp" }, + { XK_Page_Down, "PageDown" }, + { XK_Escape, "Esc" }, + { XK_Right, "H\303\270yre" }, + { XK_Left, "Venstre" }, + { XK_Up, "Opp" }, + { XK_Down, "Ned" }, + { XK_BackSpace, "Tilbake" }, + { XK_Return, "Enter" }, + { SunXK_Stop, "Avbryt" }, + { SunXK_Again, "Gjenta" }, + { SunXK_Props, "Egenskaper" }, + { SunXK_Undo, "Angre" }, + { SunXK_Front, "Front" }, + { SunXK_Copy, "Kopi" }, + { SunXK_Open, "\303\205pne" }, + { SunXK_Paste, "Lim" }, + { SunXK_Find, "S\303\270k" }, + { SunXK_Cut, "Klipp" }, + }; + + const struct KeysymNameReplacement aImplReplacements_Swedish[] = + { + { XK_Shift_L, "Skift" }, + { XK_Shift_R, "Skift" }, + { XK_Page_Up, "PageUp" }, + { XK_Page_Down, "PageDown" }, + { XK_Escape, "Esc" }, + { XK_Right, "H\303\266ger" }, + { XK_Left, "V\303\244nster" }, + { XK_Up, "Up" }, + { XK_Down, "Ned" }, + { XK_BackSpace, "Backsteg" }, + { XK_Return, "Retur" }, + { XK_space, "Blank" }, + { SunXK_Stop, "Avbryt" }, + { SunXK_Again, "Upprepa" }, + { SunXK_Props, "Egenskaper" }, + { SunXK_Undo, "\303\205ngra" }, + { SunXK_Front, "Fram" }, + { SunXK_Copy, "Kopiera" }, + { SunXK_Open, "\303\226ppna" }, + { SunXK_Paste, "Klistra in" }, + { SunXK_Find, "S\303\266k" }, + { SunXK_Cut, "Klipp ut" }, + }; + + const struct KeysymNameReplacement aImplReplacements_Portuguese[] = + { + { XK_Page_Up, "PageUp" }, + { XK_Page_Down, "PageDown" }, + { XK_Escape, "Esc" }, + { XK_Right, "Direita" }, + { XK_Left, "Esquerda" }, + { XK_Up, "Acima" }, + { XK_Down, "Abaixo" }, + { XK_BackSpace, "Backspace" }, + { XK_Return, "Enter" }, + { XK_slash, "Barra" }, + { SunXK_Stop, "Stop" }, + { SunXK_Again, "Again" }, + { SunXK_Props, "Props" }, + { SunXK_Undo, "Undo" }, + { SunXK_Front, "Front" }, + { SunXK_Copy, "Copy" }, + { SunXK_Open, "Open" }, + { SunXK_Paste, "Paste" }, + { SunXK_Find, "Find" }, + { SunXK_Cut, "Cut" }, + }; + + const struct KeysymNameReplacement aImplReplacements_Slovenian[] = + { + { XK_Control_L, "Krmilka" }, + { XK_Control_R, "Krmilka" }, + { XK_Shift_L, "Dvigalka" }, + { XK_Shift_R, "Dvigalka" }, + { XK_Alt_L, "Izmenjalka" }, + { XK_Alt_R, "Desna izmenjalka" }, + { XK_Page_Up, "Prej\305\241nja stranf" }, + { XK_Page_Down, "Naslednja stran" }, + { XK_End, "Konec" }, + { XK_Home, "Za\304\215etek" }, + { XK_Insert, "Vstavljalka" }, + { XK_Delete, "Brisalka" }, + { XK_Escape, "Ube\305\276nica" }, + { XK_Right, "Desno" }, + { XK_Left, "Levo" }, + { XK_Up, "Navzgor" }, + { XK_Down, "Navzdol" }, + { XK_BackSpace, "Vra\304\215alka" }, + { XK_Return, "Vna\305\241alka" }, + { XK_slash, "Po\305\241evnica" }, + { XK_space, "Preslednica" }, + { SunXK_Stop, "Ustavi" }, + { SunXK_Again, "Ponovi" }, + { SunXK_Props, "Lastnosti" }, + { SunXK_Undo, "Razveljavi" }, + { SunXK_Front, "Ospredje" }, + { SunXK_Copy, "Kopiraj" }, + { SunXK_Open, "Odpri" }, + { SunXK_Paste, "Prilepi" }, + { SunXK_Find, "Najdi" }, + { SunXK_Cut, "Izre\305\276i" }, + }; + + const struct KeysymNameReplacement aImplReplacements_Spanish[] = + { + { XK_Shift_L, "May\303\272s" }, + { XK_Shift_R, "May\303\272s" }, + { XK_Page_Up, "ReP\303\241g" }, + { XK_Page_Down, "AvP\303\241g" }, + { XK_End, "Fin" }, + { XK_Home, "Inicio" }, + { XK_Delete, "Supr" }, + { XK_Escape, "Esc" }, + { XK_Right, "Derecha" }, + { XK_Left, "Izquierda" }, + { XK_Up, "Arriba" }, + { XK_Down, "Abajo" }, + { XK_BackSpace, "Ret" }, + { XK_Return, "Entrada" }, + { XK_space, "Espacio" }, + { XK_KP_Enter, "Intro" }, + { SunXK_Stop, "Detener" }, + { SunXK_Again, "Repetir" }, + { SunXK_Props, "Props" }, + { SunXK_Undo, "Anular" }, + { SunXK_Front, "Delante" }, + { SunXK_Copy, "Copiar" }, + { SunXK_Open, "Abrir" }, + { SunXK_Paste, "Pegar" }, + { SunXK_Find, "Buscar" }, + { SunXK_Cut, "Cortar" }, + }; + + const struct KeysymNameReplacement aImplReplacements_Estonian[] = + { + { XK_Page_Up, "PgUp" }, + { XK_Page_Down, "PgDown" }, + { XK_End, "End" }, + { XK_Home, "Home" }, + { XK_Insert, "Ins" }, + { XK_Delete, "Del" }, + { XK_Escape, "Esc" }, + { XK_Right, "Nool paremale" }, + { XK_Left, "Nool vasakule" }, + { XK_Up, "Nool \303\274les" }, + { XK_Down, "Nool alla" }, + { XK_BackSpace, "Tagasil\303\274ke" }, + { XK_Return, "Enter" }, + { XK_slash, "Kaldkriips" }, + { XK_space, "T\303\274hik" }, + { XK_asterisk, "T\303\244rn" }, + { SunXK_Stop, "Peata" }, + { SunXK_Again, "Korda" }, + { SunXK_Props, "Omadused" }, + { SunXK_Undo, "V\303\265ta tagasi" }, + { SunXK_Front, "Esiplaanile" }, + { SunXK_Copy, "Kopeeri" }, + { SunXK_Open, "Ava" }, + { SunXK_Paste, "Aseta" }, + { SunXK_Find, "Otsi" }, + { SunXK_Cut, "L\303\265ika" }, + }; + + const struct KeysymNameReplacement aImplReplacements_Catalan[] = + { + { XK_Shift_L, "Maj" }, + { XK_Shift_R, "Maj" }, + { XK_Page_Up, "Re P\303\240g" }, + { XK_Page_Down, "Av P\303\240g" }, + { XK_End, "Fi" }, + { XK_Home, "Inici" }, + { XK_Delete, "Supr" }, + { XK_Escape, "Esc" }, + { XK_Right, "Dreta" }, + { XK_Left, "Esquerra" }, + { XK_Up, "Amunt" }, + { XK_Down, "Avall" }, + { XK_BackSpace, "Retroc\303\251s" }, + { XK_Return, "Retorn" }, + { XK_space, "Espai" }, + { XK_KP_Enter, "Retorn" }, + { SunXK_Stop, "Atura" }, + { SunXK_Again, "Repeteix" }, + { SunXK_Props, "Props" }, + { SunXK_Undo, "Desf\303\251s" }, + { SunXK_Front, "Davant" }, + { SunXK_Copy, "C\303\262pia" }, + { SunXK_Open, "Obre" }, + { SunXK_Paste, "Enganxa" }, + { SunXK_Find, "Cerca" }, + { SunXK_Cut, "Retalla" }, + }; + + const struct KeysymNameReplacement aImplReplacements_Lithuanian[] = + { + { XK_Control_L, "Vald" }, + { XK_Control_R, "Vald" }, + { XK_Shift_L, "Lyg2" }, + { XK_Shift_R, "Lyg2" }, + { XK_Alt_L, "Alt" }, + { XK_Alt_R, "Lyg3" }, + { XK_Page_Up, "Psl\342\206\221" }, + { XK_Page_Down, "Psl\342\206\223" }, + { XK_End, "Pab" }, + { XK_Home, "Prad" }, + { XK_Insert, "\304\256terpti" }, + { XK_Delete, "\305\240al" }, + { XK_Escape, "Gr" }, + { XK_Right, "De\305\241in\304\227n" }, + { XK_Left, "Kair\304\227n" }, + { XK_Up, "Auk\305\241tyn" }, + { XK_Down, "\305\275emyn" }, + { XK_BackSpace, "Naikinti" }, + { XK_Return, "\304\256vesti" }, + { XK_asterisk, "\305\275vaig\305\276dut\304\227" }, + { XK_slash, "De\305\241ininis br\305\253k\305\241nys" }, + { XK_space, "Tarpas" }, + { SunXK_Stop, "Stabdyti" }, + { SunXK_Again, "Kartoti" }, + { SunXK_Props, "Savyb\304\227s" }, + { SunXK_Undo, "At\305\241aukti" }, + { SunXK_Front, "Priekinis planas" }, + { SunXK_Copy, "Kopijuoti" }, + { SunXK_Open, "Atverti" }, + { SunXK_Paste, "\304\256d\304\227ti" }, + { SunXK_Find, "Ie\305\241koti" }, + { SunXK_Cut, "I\305\241kirpti" }, + }; + + const struct KeysymNameReplacement aImplReplacements_Hungarian[] = + { + { XK_Right, "Jobbra" }, + { XK_Left, "Balra" }, + { XK_Up, "Fel" }, + { XK_Down, "Le" }, + { XK_Return, "Enter" }, + { XK_space, "Sz\303\263k\303\266z" }, + { XK_asterisk, "Csillag" }, + { XK_slash, "Oszt\303\241sjel" }, + }; + + const struct KeyboardReplacements aKeyboards[] = + { + { "ca", aImplReplacements_Catalan, std::size(aImplReplacements_Catalan) }, + { "de", aImplReplacements_German, std::size(aImplReplacements_German) }, + { "sl", aImplReplacements_Slovenian, std::size(aImplReplacements_Slovenian) }, + { "es", aImplReplacements_Spanish, std::size(aImplReplacements_Spanish) }, + { "et", aImplReplacements_Estonian, std::size(aImplReplacements_Estonian) }, + { "fr", aImplReplacements_French, std::size(aImplReplacements_French) }, + { "hu", aImplReplacements_Hungarian, std::size(aImplReplacements_Hungarian) }, + { "it", aImplReplacements_Italian, std::size(aImplReplacements_Italian) }, + { "lt", aImplReplacements_Lithuanian, std::size(aImplReplacements_Lithuanian) }, + { "nl", aImplReplacements_Dutch, std::size(aImplReplacements_Dutch) }, + { "no", aImplReplacements_Norwegian, std::size(aImplReplacements_Norwegian) }, + { "pt", aImplReplacements_Portuguese, std::size(aImplReplacements_Portuguese) }, + { "ru", aImplReplacements_Russian, std::size(aImplReplacements_Russian) }, + { "sv", aImplReplacements_Swedish, std::size(aImplReplacements_Swedish) }, + { "tr", aImplReplacements_Turkish, std::size(aImplReplacements_Turkish) }, + }; + + // translate keycodes, used within the displayed menu shortcuts + OUString getKeysymReplacementName( std::u16string_view pLang, KeySym nSymbol ) + { + for(const auto & rKeyboard : aKeyboards) + { + if( o3tl::equalsAscii( pLang, rKeyboard.pLangName ) ) + { + const struct KeysymNameReplacement* pRepl = rKeyboard.pReplacements; + for( int m = rKeyboard.nReplacements ; m ; ) + { + if( nSymbol == pRepl[--m].aSymbol ) + return OUString( pRepl[m].pName, strlen(pRepl[m].pName), RTL_TEXTENCODING_UTF8 ); + } + } + } + + // try english fallbacks + const struct KeysymNameReplacement* pRepl = aImplReplacements_English; + for( int m = SAL_N_ELEMENTS(aImplReplacements_English); m ; ) + { + if( nSymbol == pRepl[--m].aSymbol ) + return OUString( pRepl[m].pName, strlen(pRepl[m].pName), RTL_TEXTENCODING_UTF8 ); + } + + return OUString(); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/app/randrwrapper.cxx b/vcl/unx/generic/app/randrwrapper.cxx new file mode 100644 index 0000000000..1ef474c347 --- /dev/null +++ b/vcl/unx/generic/app/randrwrapper.cxx @@ -0,0 +1,181 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifdef USE_RANDR + +#include +#include + +#include + +namespace +{ + +class RandRWrapper +{ + bool m_bValid; + + explicit RandRWrapper(Display*); +public: + static RandRWrapper& get(Display*); + static void releaseWrapper(); + + Bool XRRQueryExtension(Display* i_pDisp, int* o_event_base, int* o_error_base ) + { + Bool bRet = False; + if( m_bValid ) + bRet = ::XRRQueryExtension( i_pDisp, o_event_base, o_error_base ); + return bRet; + } + XRRScreenConfiguration* XRRGetScreenInfo( Display* i_pDisp, Drawable i_aDrawable ) + { + return m_bValid ? ::XRRGetScreenInfo( i_pDisp, i_aDrawable ) : nullptr; + } + void XRRFreeScreenConfigInfo( XRRScreenConfiguration* i_pConfig ) + { + if( m_bValid ) + ::XRRFreeScreenConfigInfo( i_pConfig ); + } + void XRRSelectInput( Display* i_pDisp, ::Window i_window, int i_nMask ) + { + if( m_bValid ) + ::XRRSelectInput( i_pDisp, i_window, i_nMask ); + } + int XRRUpdateConfiguration( XEvent* i_pEvent ) + { + return m_bValid ? ::XRRUpdateConfiguration( i_pEvent ) : 0; + } + XRRScreenSize* XRRConfigSizes( XRRScreenConfiguration* i_pConfig, int* o_nSizes ) + { + return m_bValid ? ::XRRConfigSizes( i_pConfig, o_nSizes ) : nullptr; + } + SizeID XRRConfigCurrentConfiguration( XRRScreenConfiguration* i_pConfig, Rotation* o_pRot ) + { + return m_bValid ? ::XRRConfigCurrentConfiguration( i_pConfig, o_pRot ) : 0; + } + int XRRRootToScreen( Display *dpy, ::Window root ) + { + return m_bValid ? ::XRRRootToScreen( dpy, root ) : -1; + } +}; + +RandRWrapper::RandRWrapper( Display* pDisplay ) : + m_bValid( true ) +{ + int nEventBase = 0, nErrorBase = 0; + if( !XRRQueryExtension( pDisplay, &nEventBase, &nErrorBase ) ) + m_bValid = false; +} + +RandRWrapper* pWrapper = nullptr; + +RandRWrapper& RandRWrapper::get( Display* i_pDisplay ) +{ + if( ! pWrapper ) + pWrapper = new RandRWrapper( i_pDisplay ); + return *pWrapper; +} + +void RandRWrapper::releaseWrapper() +{ + delete pWrapper; + pWrapper = nullptr; +} + +} // namespace + +#endif + +#include +#if OSL_DEBUG_LEVEL > 1 +#include +#endif + +void SalDisplay::InitRandR( ::Window aRoot ) const +{ + #ifdef USE_RANDR + RandRWrapper::get( GetDisplay() ).XRRSelectInput( GetDisplay(), aRoot, RRScreenChangeNotifyMask ); + #else + (void)this; + (void)aRoot; + #endif +} + +void SalDisplay::DeInitRandR() +{ + #ifdef USE_RANDR + RandRWrapper::releaseWrapper(); +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.app", "SalDisplay::DeInitRandR()."); +#endif + #endif +} + +void SalDisplay::processRandREvent( XEvent* pEvent ) +{ +#ifdef USE_RANDR + XConfigureEvent* pCnfEvent=reinterpret_cast(pEvent); + if( !pWrapper || pWrapper->XRRRootToScreen(GetDisplay(),pCnfEvent->window) == -1 ) + return; + + int nRet = pWrapper->XRRUpdateConfiguration( pEvent ); + if( nRet != 1 || pEvent->type == ConfigureNotify) // this should then be a XRRScreenChangeNotifyEvent + return; + + // update screens + bool bNotify = false; + for(ScreenData & rScreen : m_aScreens) + { + if( rScreen.m_bInit ) + { + XRRScreenConfiguration *pConfig = nullptr; + XRRScreenSize *pSizes = nullptr; + int nSizes = 0; + Rotation nRot = 0; + SizeID nId = 0; + + pConfig = pWrapper->XRRGetScreenInfo( GetDisplay(), rScreen.m_aRoot ); + nId = pWrapper->XRRConfigCurrentConfiguration( pConfig, &nRot ); + pSizes = pWrapper->XRRConfigSizes( pConfig, &nSizes ); + XRRScreenSize *pTargetSize = pSizes + nId; + + bNotify = bNotify || + rScreen.m_aSize.Width() != pTargetSize->width || + rScreen.m_aSize.Height() != pTargetSize->height; + + rScreen.m_aSize = AbsoluteScreenPixelSize( pTargetSize->width, pTargetSize->height ); + + pWrapper->XRRFreeScreenConfigInfo( pConfig ); + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.app", "screen " << nId + << " changed to size " << (int)pTargetSize->width + << "x" << (int)pTargetSize->height); +#endif + } + } + if( bNotify ) + emitDisplayChanged(); +#else + (void)this; + (void)pEvent; +#endif +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/app/saldata.cxx b/vcl/unx/generic/app/saldata.cxx new file mode 100644 index 0000000000..34c7c08789 --- /dev/null +++ b/vcl/unx/generic/app/saldata.cxx @@ -0,0 +1,775 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include + +#include +#include +#include +#ifdef SUN +#include +#endif +#ifdef FREEBSD +#include +#include +#endif + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include + +X11SalData* GetX11SalData() +{ + return static_cast(ImplGetSVData()->mpSalData); +} + +extern "C" { + +static int XErrorHdl( Display *pDisplay, XErrorEvent *pEvent ) +{ + GetX11SalData()->XError( pDisplay, pEvent ); + return 0; +} + +static int XIOErrorHdl( Display * ) +{ + if ( Application::IsMainThread() ) + { + /* #106197# hack: until a real shutdown procedure exists + * _exit ASAP + */ + if( ImplGetSVData()->maAppData.mbAppQuit ) + _exit(1); + + // really bad hack + if( ! SessionManagerClient::checkDocumentsSaved() ) + /* oslSignalAction eToDo = */ osl_raiseSignal (OSL_SIGNAL_USER_X11SUBSYSTEMERROR, nullptr); + } + + std::fprintf( stderr, "X IO Error\n" ); + std::fflush( stdout ); + std::fflush( stderr ); + + /* #106197# the same reasons to use _exit instead of exit in salmain + * do apply here. Since there is nothing to be done after an XIO + * error we have to _exit immediately. + */ + _exit(1); + return 0; +} + +} + +const struct timeval noyield_ = { 0, 0 }; +const struct timeval yield_ = { 0, 10000 }; + +static const char* XRequest[] = { + // see /usr/lib/X11/XErrorDB, /usr/openwin/lib/XErrorDB ... + nullptr, + "X_CreateWindow", + "X_ChangeWindowAttributes", + "X_GetWindowAttributes", + "X_DestroyWindow", + "X_DestroySubwindows", + "X_ChangeSaveSet", + "X_ReparentWindow", + "X_MapWindow", + "X_MapSubwindows", + "X_UnmapWindow", + "X_UnmapSubwindows", + "X_ConfigureWindow", + "X_CirculateWindow", + "X_GetGeometry", + "X_QueryTree", + "X_InternAtom", + "X_GetAtomName", + "X_ChangeProperty", + "X_DeleteProperty", + "X_GetProperty", + "X_ListProperties", + "X_SetSelectionOwner", + "X_GetSelectionOwner", + "X_ConvertSelection", + "X_SendEvent", + "X_GrabPointer", + "X_UngrabPointer", + "X_GrabButton", + "X_UngrabButton", + "X_ChangeActivePointerGrab", + "X_GrabKeyboard", + "X_UngrabKeyboard", + "X_GrabKey", + "X_UngrabKey", + "X_AllowEvents", + "X_GrabServer", + "X_UngrabServer", + "X_QueryPointer", + "X_GetMotionEvents", + "X_TranslateCoords", + "X_WarpPointer", + "X_SetInputFocus", + "X_GetInputFocus", + "X_QueryKeymap", + "X_OpenFont", + "X_CloseFont", + "X_QueryFont", + "X_QueryTextExtents", + "X_ListFonts", + "X_ListFontsWithInfo", + "X_SetFontPath", + "X_GetFontPath", + "X_CreatePixmap", + "X_FreePixmap", + "X_CreateGC", + "X_ChangeGC", + "X_CopyGC", + "X_SetDashes", + "X_SetClipRectangles", + "X_FreeGC", + "X_ClearArea", + "X_CopyArea", + "X_CopyPlane", + "X_PolyPoint", + "X_PolyLine", + "X_PolySegment", + "X_PolyRectangle", + "X_PolyArc", + "X_FillPoly", + "X_PolyFillRectangle", + "X_PolyFillArc", + "X_PutImage", + "X_GetImage", + "X_PolyText8", + "X_PolyText16", + "X_ImageText8", + "X_ImageText16", + "X_CreateColormap", + "X_FreeColormap", + "X_CopyColormapAndFree", + "X_InstallColormap", + "X_UninstallColormap", + "X_ListInstalledColormaps", + "X_AllocColor", + "X_AllocNamedColor", + "X_AllocColorCells", + "X_AllocColorPlanes", + "X_FreeColors", + "X_StoreColors", + "X_StoreNamedColor", + "X_QueryColors", + "X_LookupColor", + "X_CreateCursor", + "X_CreateGlyphCursor", + "X_FreeCursor", + "X_RecolorCursor", + "X_QueryBestSize", + "X_QueryExtension", + "X_ListExtensions", + "X_ChangeKeyboardMapping", + "X_GetKeyboardMapping", + "X_ChangeKeyboardControl", + "X_GetKeyboardControl", + "X_Bell", + "X_ChangePointerControl", + "X_GetPointerControl", + "X_SetScreenSaver", + "X_GetScreenSaver", + "X_ChangeHosts", + "X_ListHosts", + "X_SetAccessControl", + "X_SetCloseDownMode", + "X_KillClient", + "X_RotateProperties", + "X_ForceScreenSaver", + "X_SetPointerMapping", + "X_GetPointerMapping", + "X_SetModifierMapping", + "X_GetModifierMapping", + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + "X_NoOperation" +}; + +X11SalData::X11SalData() + : GenericUnixSalData() +{ + pXLib_ = nullptr; + + m_aOrigXIOErrorHandler = XSetIOErrorHandler ( XIOErrorHdl ); + PushXErrorLevel( !!getenv( "SAL_IGNOREXERRORS" ) ); +} + +X11SalData::~X11SalData() +{ + DeleteDisplay(); + PopXErrorLevel(); + XSetIOErrorHandler (m_aOrigXIOErrorHandler); +} + +void X11SalData::Dispose() +{ + delete GetDisplay(); + SetSalData( nullptr ); +} + +void X11SalData::DeleteDisplay() +{ + delete GetDisplay(); + SetDisplay( nullptr ); + pXLib_.reset(); +} + +void X11SalData::Init() +{ + pXLib_.reset(new SalXLib()); + pXLib_->Init(); +} + +void X11SalData::ErrorTrapPush() +{ + PushXErrorLevel( true ); +} + +bool X11SalData::ErrorTrapPop( bool bIgnoreError ) +{ + bool err = false; + if( !bIgnoreError ) + err = HasXErrorOccurred(); + ResetXErrorOccurred(); + PopXErrorLevel(); + return err; +} + +void X11SalData::PushXErrorLevel( bool bIgnore ) +{ + m_aXErrorHandlerStack.emplace_back( ); + XErrorStackEntry& rEnt = m_aXErrorHandlerStack.back(); + rEnt.m_bWas = false; + rEnt.m_bIgnore = bIgnore; + rEnt.m_aHandler = XSetErrorHandler( XErrorHdl ); +} + +void X11SalData::PopXErrorLevel() +{ + if( !m_aXErrorHandlerStack.empty() ) + { + XSetErrorHandler( m_aXErrorHandlerStack.back().m_aHandler ); + m_aXErrorHandlerStack.pop_back(); + } +} + +SalXLib::SalXLib() +{ + m_aTimeout.tv_sec = 0; + m_aTimeout.tv_usec = 0; + m_nTimeoutMS = 0; + + nFDs_ = 0; + FD_ZERO( &aReadFDS_ ); + FD_ZERO( &aExceptionFDS_ ); + + m_pInputMethod = nullptr; + m_pDisplay = nullptr; + + m_pTimeoutFDS[0] = m_pTimeoutFDS[1] = -1; + if (pipe (m_pTimeoutFDS) == -1) + return; + + // initialize 'wakeup' pipe. + int flags; + + // set close-on-exec descriptor flag. + if ((flags = fcntl (m_pTimeoutFDS[0], F_GETFD)) != -1) + { + flags |= FD_CLOEXEC; + (void)fcntl(m_pTimeoutFDS[0], F_SETFD, flags); + } + if ((flags = fcntl (m_pTimeoutFDS[1], F_GETFD)) != -1) + { + flags |= FD_CLOEXEC; + (void)fcntl(m_pTimeoutFDS[1], F_SETFD, flags); + } + + // set non-blocking I/O flag. + if ((flags = fcntl (m_pTimeoutFDS[0], F_GETFL)) != -1) + { + flags |= O_NONBLOCK; + (void)fcntl(m_pTimeoutFDS[0], F_SETFL, flags); + } + if ((flags = fcntl (m_pTimeoutFDS[1], F_GETFL)) != -1) + { + flags |= O_NONBLOCK; + (void)fcntl(m_pTimeoutFDS[1], F_SETFL, flags); + } + + // insert [0] into read descriptor set. + FD_SET( m_pTimeoutFDS[0], &aReadFDS_ ); + nFDs_ = m_pTimeoutFDS[0] + 1; +} + +SalXLib::~SalXLib() +{ + // close 'wakeup' pipe. + close (m_pTimeoutFDS[0]); + close (m_pTimeoutFDS[1]); + + m_pInputMethod.reset(); +} + +static Display *OpenX11Display(OString& rDisplay) +{ + /* + * open connection to X11 Display + * try in this order: + * o -display command line parameter, + * o $DISPLAY environment variable + * o default display + */ + + Display *pDisp = nullptr; + + // is there a -display command line parameter? + + sal_uInt32 nParams = osl_getCommandArgCount(); + OUString aParam; + for (sal_uInt32 i=0; iSetLocale(); + XrmInitialize(); + + OString aDisplay; + m_pDisplay = OpenX11Display(aDisplay); + + if ( m_pDisplay ) + return; + + OUString aProgramFileURL; + osl_getExecutableFile( &aProgramFileURL.pData ); + OUString aProgramSystemPath; + osl_getSystemPathFromFileURL (aProgramFileURL.pData, &aProgramSystemPath.pData); + OString aProgramName = OUStringToOString( + aProgramSystemPath, + osl_getThreadTextEncoding() ); + std::fprintf( stderr, "%s X11 error: Can't open display: %s\n", + aProgramName.getStr(), aDisplay.getStr()); + std::fprintf( stderr, " Set DISPLAY environment variable, use -display option\n"); + std::fprintf( stderr, " or check permissions of your X-Server\n"); + std::fprintf( stderr, " (See \"man X\" resp. \"man xhost\" for details)\n"); + std::fflush( stderr ); + exit(0); + +} + +extern "C" { +static void EmitFontpathWarning() +{ + static Bool bOnce = False; + if ( !bOnce ) + { + bOnce = True; + std::fprintf( stderr, "Please verify your fontpath settings\n" + "\t(See \"man xset\" for details" + " or ask your system administrator)\n" ); + } +} + +} /* extern "C" */ + +static void PrintXError( Display *pDisplay, XErrorEvent *pEvent ) +{ + char msg[ 120 ] = ""; + XGetErrorText( pDisplay, pEvent->error_code, msg, sizeof( msg ) ); + std::fprintf( stderr, "X-Error: %s\n", msg ); + if( pEvent->request_code < SAL_N_ELEMENTS( XRequest ) ) + { + const char* pName = XRequest[pEvent->request_code]; + if( !pName ) + pName = "BadRequest?"; + std::fprintf( stderr, "\tMajor opcode: %d (%s)\n", pEvent->request_code, pName ); + } + else + { + std::fprintf( stderr, "\tMajor opcode: %d\n", pEvent->request_code ); + // TODO: also display extension name? + std::fprintf( stderr, "\tMinor opcode: %d\n", pEvent->minor_code ); + } + + std::fprintf( stderr, "\tResource ID: 0x%lx\n", + pEvent->resourceid ); + std::fprintf( stderr, "\tSerial No: %ld (%ld)\n", + pEvent->serial, LastKnownRequestProcessed(pDisplay) ); + + if( !getenv( "SAL_SYNCHRONIZE" ) ) + { + std::fprintf( stderr, "These errors are reported asynchronously,\n"); + std::fprintf( stderr, "set environment variable SAL_SYNCHRONIZE to 1 to help debugging\n"); + } + + std::fflush( stdout ); + std::fflush( stderr ); +} + +void X11SalData::XError( Display *pDisplay, XErrorEvent *pEvent ) +{ + if( ! m_aXErrorHandlerStack.back().m_bIgnore ) + { + if ( (pEvent->error_code == BadAlloc) + && (pEvent->request_code == X_OpenFont) ) + { + static Bool bOnce = False; + if ( !bOnce ) + { + std::fprintf(stderr, "X-Error occurred in a request for X_OpenFont\n"); + EmitFontpathWarning(); + + bOnce = True ; + } + return; + } + /* ignore + * X_SetInputFocus: it's a hint only anyway + * X_GetProperty: this is part of the XGetWindowProperty call and will + * be handled by the return value of that function + */ + else if( pEvent->request_code == X_SetInputFocus || + pEvent->request_code == X_GetProperty + ) + return; + + if( pDisplay != vcl_sal::getSalDisplay(GetGenericUnixSalData())->GetDisplay() ) + return; + + PrintXError( pDisplay, pEvent ); + + oslSignalAction eToDo = osl_raiseSignal (OSL_SIGNAL_USER_X11SUBSYSTEMERROR, nullptr); + switch (eToDo) + { + case osl_Signal_ActIgnore : + return; + case osl_Signal_ActAbortApp : + abort(); + case osl_Signal_ActKillApp : + exit(0); + case osl_Signal_ActCallNextHdl : + break; + default : + break; + } + + } + + m_aXErrorHandlerStack.back().m_bWas = true; +} + +void X11SalData::Timeout() +{ + ImplSVData* pSVData = ImplGetSVData(); + if( pSVData->maSchedCtx.mpSalTimer ) + pSVData->maSchedCtx.mpSalTimer->CallCallback(); +} + +namespace { + +struct YieldEntry +{ + int fd; // file descriptor for reading + void* data; // data for predicate and callback + YieldFunc pending; // predicate (determines pending events) + YieldFunc queued; // read and queue up events + YieldFunc handle; // handle pending events + + int HasPendingEvent() const { return pending( fd, data ); } + int IsEventQueued() const { return queued( fd, data ); } + void HandleNextEvent() const { handle( fd, data ); } +}; + +} + +#define MAX_NUM_DESCRIPTORS 128 + +static YieldEntry yieldTable[ MAX_NUM_DESCRIPTORS ]; + +void SalXLib::Insert( int nFD, void* data, + YieldFunc pending, + YieldFunc queued, + YieldFunc handle ) +{ + SAL_WARN_IF( !nFD, "vcl", "can not insert stdin descriptor" ); + SAL_WARN_IF( yieldTable[nFD].fd, "vcl", "SalXLib::Insert fd twice" ); + + yieldTable[nFD].fd = nFD; + yieldTable[nFD].data = data; + yieldTable[nFD].pending = pending; + yieldTable[nFD].queued = queued; + yieldTable[nFD].handle = handle; + + FD_SET( nFD, &aReadFDS_ ); + FD_SET( nFD, &aExceptionFDS_ ); + + if( nFD >= nFDs_ ) + nFDs_ = nFD + 1; +} + +void SalXLib::Remove( int nFD ) +{ + FD_CLR( nFD, &aReadFDS_ ); + FD_CLR( nFD, &aExceptionFDS_ ); + + yieldTable[nFD].fd = 0; + + if ( nFD == nFDs_ ) + { + for ( nFD = nFDs_ - 1; + nFD >= 0 && !yieldTable[nFD].fd; + nFD-- ) ; + + nFDs_ = nFD + 1; + } +} + +bool SalXLib::CheckTimeout( bool bExecuteTimers ) +{ + bool bRet = false; + if( m_aTimeout.tv_sec ) // timer is started + { + timeval aTimeOfDay; + gettimeofday( &aTimeOfDay, nullptr ); + if( aTimeOfDay >= m_aTimeout ) + { + bRet = true; + if( bExecuteTimers ) + { + // timed out, update timeout + m_aTimeout = aTimeOfDay; + /* + * #107827# autorestart immediately, will be stopped (or set + * to different value in notify hdl if necessary; + * CheckTimeout should return false while + * timers are being dispatched. + */ + m_aTimeout += m_nTimeoutMS; + // notify + X11SalData::Timeout(); + } + } + } + return bRet; +} + +bool +SalXLib::Yield( bool bWait, bool bHandleAllCurrentEvents ) +{ + // check for timeouts here if you want to make screenshots + static char* p_prioritize_timer = getenv ("SAL_HIGHPRIORITY_REPAINT"); + bool bHandledEvent = false; + if (p_prioritize_timer != nullptr) + bHandledEvent = CheckTimeout(); + + const int nMaxEvents = bHandleAllCurrentEvents ? 100 : 1; + + // first, check for already queued events. + for ( int nFD = 0; nFD < nFDs_; nFD++ ) + { + YieldEntry* pEntry = &(yieldTable[nFD]); + if ( pEntry->fd ) + { + SAL_WARN_IF( nFD != pEntry->fd, "vcl", "wrong fd in Yield()" ); + for( int i = 0; i < nMaxEvents && pEntry->HasPendingEvent(); i++ ) + { + pEntry->HandleNextEvent(); + if( ! bHandleAllCurrentEvents ) + { + return true; + } + } + } + } + + // next, select with or without timeout according to bWait. + int nFDs = nFDs_; + fd_set ReadFDS = aReadFDS_; + fd_set ExceptionFDS = aExceptionFDS_; + int nFound = 0; + + timeval Timeout = noyield_; + timeval *pTimeout = &Timeout; + + + if (bWait) + { + pTimeout = nullptr; + if (m_aTimeout.tv_sec) // Timer is started. + { + // determine remaining timeout. + gettimeofday (&Timeout, nullptr); + Timeout = m_aTimeout - Timeout; + if (yield_ >= Timeout) + { + // guard against micro timeout. + Timeout = yield_; + } + pTimeout = &Timeout; + } + } + + { + // release YieldMutex (and re-acquire at block end) + SolarMutexReleaser aReleaser; + nFound = select( nFDs, &ReadFDS, nullptr, &ExceptionFDS, pTimeout ); + } + if( nFound < 0 ) // error + { +#ifdef DBG_UTIL + SAL_INFO("vcl.app", "SalXLib::Yield e=" << errno << " f=" << nFound); +#endif + if( EINTR == errno ) + { + errno = 0; + } + } + + // usually handle timeouts here (as in 5.2) + if (p_prioritize_timer == nullptr) + bHandledEvent = CheckTimeout() || bHandledEvent; + + // handle wakeup events. + if ((nFound > 0) && FD_ISSET(m_pTimeoutFDS[0], &ReadFDS)) + { + int buffer; + while (read (m_pTimeoutFDS[0], &buffer, sizeof(buffer)) > 0) + continue; + nFound -= 1; + } + + // handle other events. + if( nFound > 0 ) + { + // now we are in the protected section ! + // recall select if we have acquired fd's, ready for reading, + + struct timeval noTimeout = { 0, 0 }; + nFound = select( nFDs_, &ReadFDS, nullptr, + &ExceptionFDS, &noTimeout ); + + // someone-else has done the job for us + if (nFound == 0) + { + return false; + } + + for ( int nFD = 0; nFD < nFDs_; nFD++ ) + { + YieldEntry* pEntry = &(yieldTable[nFD]); + if ( pEntry->fd ) + { + if ( FD_ISSET( nFD, &ExceptionFDS ) ) { +#if OSL_DEBUG_LEVEL > 1 + SAL_WARN("vcl.app", "SalXLib::Yield exception."); +#endif + nFound--; + } + if ( FD_ISSET( nFD, &ReadFDS ) ) + { + for( int i = 0; pEntry->IsEventQueued() && i < nMaxEvents; i++ ) + { + pEntry->HandleNextEvent(); + bHandledEvent = true; + // if a recursive call has done the job + // so abort here + } + nFound--; + } + } + } + } + + return bHandledEvent; +} + +void SalXLib::Wakeup() +{ + OSL_VERIFY(write (m_pTimeoutFDS[1], "", 1) == 1); +} + +void SalXLib::TriggerUserEventProcessing() +{ + Wakeup(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/app/saldisp.cxx b/vcl/unx/generic/app/saldisp.cxx new file mode 100644 index 0000000000..6733e48323 --- /dev/null +++ b/vcl/unx/generic/app/saldisp.cxx @@ -0,0 +1,2489 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include +#include +#include + +#if defined(__sun) +#include +#endif + +#include +#include +#include + +#include +#include +#include +#ifdef __sun +#define XK_KOREAN +#endif +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +/* From */ +typedef unsigned long Pixel; + +using namespace vcl_sal; + +#ifdef DBG_UTIL +static const char *Null( const char *p ) { return p ? p : ""; } +static const char *GetEnv( const char *p ) { return Null( getenv( p ) ); } +static const char *KeyStr( KeySym n ) { return Null( XKeysymToString( n ) ); } + +static const char *GetAtomName( Display *d, Atom a ) +{ return Null( XGetAtomName( d, a ) ); } +#endif + +// check if the resolution is sane +static bool sal_ValidDPI(tools::Long nDPI) +{ + return (nDPI >= 50) && (nDPI <= 500); +} + +static bool sal_GetVisualInfo( Display *pDisplay, XID nVID, XVisualInfo &rVI ) +{ + int nInfos; + XVisualInfo aTemplate; + XVisualInfo*pInfo; + + aTemplate.visualid = nVID; + + pInfo = XGetVisualInfo( pDisplay, VisualIDMask, &aTemplate, &nInfos ); + if( !pInfo ) + return false; + + rVI = *pInfo; + XFree( pInfo ); + + SAL_WARN_IF( rVI.visualid != nVID, "vcl", + "sal_GetVisualInfo: could not get correct visual by visualId" ); + return true; +} + +extern "C" srv_vendor_t +sal_GetServerVendor( Display *p_display ) +{ + struct vendor_t { + srv_vendor_t e_vendor; // vendor as enum + const char* p_name; // vendor name as returned by VendorString() + unsigned int n_len; // number of chars to compare + }; + + static const vendor_t vendorlist[] = { + { vendor_sun, "Sun Microsystems, Inc.", 10 }, + }; + + // handle regular server vendors + char *p_name = ServerVendor( p_display ); + for (auto const & vendor : vendorlist) + { + if ( strncmp (p_name, vendor.p_name, vendor.n_len) == 0 ) + return vendor.e_vendor; + } + + // vendor not found in list + return vendor_unknown; +} + +bool SalDisplay::BestVisual( Display *pDisplay, + int nScreen, + XVisualInfo &rVI ) +{ + VisualID nDefVID = XVisualIDFromVisual( DefaultVisual( pDisplay, nScreen ) ); + VisualID nVID = 0; + char *pVID = getenv( "SAL_VISUAL" ); + if( pVID ) + sscanf( pVID, "%li", &nVID ); + + if( nVID && sal_GetVisualInfo( pDisplay, nVID, rVI ) ) + return rVI.visualid == nDefVID; + + XVisualInfo aVI; + aVI.screen = nScreen; + // get all visuals + int nVisuals; + XVisualInfo* pVInfos = XGetVisualInfo( pDisplay, VisualScreenMask, + &aVI, &nVisuals ); + // pVInfos should contain at least one visual, otherwise + // we're in trouble + std::vector aWeights(nVisuals); + int i; + for( i = 0; i < nVisuals; i++ ) + { + bool bUsable = false; + int nTrueColor = 1; + + if ( pVInfos[i].screen != nScreen ) + { + bUsable = false; + } + else if( pVInfos[i].c_class == TrueColor ) + { + nTrueColor = 2048; + if( pVInfos[i].depth == 24 ) + bUsable = true; + } + else if( pVInfos[i].c_class == PseudoColor ) + { + bUsable = true; + } + aWeights[i] = bUsable ? nTrueColor*pVInfos[i].depth : -1024; + aWeights[i] -= pVInfos[ i ].visualid; + } + + int nBestVisual = 0; + int nBestWeight = -1024; + for( i = 0; i < nVisuals; i++ ) + { + if (aWeights[i] > nBestWeight) + { + nBestWeight = aWeights[i]; + nBestVisual = i; + } + } + + rVI = pVInfos[ nBestVisual ]; + + XFree( pVInfos ); + return rVI.visualid == nDefVID; +} + +SalDisplay::SalDisplay( Display *display ) : + pXLib_( nullptr ), + mpKbdExtension( nullptr ), + pDisp_( display ), + m_nXDefaultScreen( 0 ), + nMaxRequestSize_( 0 ), + meServerVendor( vendor_unknown ), + bNumLockFromXS_( false ), + nNumLockIndex_( 0 ), + nShiftKeySym_( 0 ), + nCtrlKeySym_( 0 ), + nMod1KeySym_( 0 ), + m_bXinerama( false ), + m_nLastUserEventTime( CurrentTime ) +{ +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.app", "SalDisplay::SalDisplay()."); +#endif + GenericUnixSalData *pData = GetGenericUnixSalData(); + + SAL_WARN_IF( pData->GetDisplay(), "vcl", "Second SalDisplay created !!!" ); + pData->SetDisplay( this ); + + m_nXDefaultScreen = SalX11Screen( DefaultScreen( pDisp_ ) ); +} + +SalDisplay::~SalDisplay() +{ +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.app", "SalDisplay::~SalDisplay()."); +#endif + if( pDisp_ ) + { + doDestruct(); +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.app", "display " << pDisp_ << " closed."); +#endif + pDisp_ = nullptr; + } + // don't do this in doDestruct since RandR extension adds hooks into Display + // that is XCloseDisplay still needs the RandR library if it was used + DeInitRandR(); +} + +void SalDisplay::doDestruct() +{ + GenericUnixSalData *pData = GetGenericUnixSalData(); + + m_pWMAdaptor.reset(); + + FreetypeManager::get().ClearFontCache(); + + if( IsDisplay() ) + { + delete mpKbdExtension; + mpKbdExtension = nullptr; + + for( size_t i = 0; i < m_aScreens.size(); i++ ) + { + ScreenData& rData = m_aScreens[i]; + if( rData.m_bInit ) + { + if( rData.m_aMonoGC != rData.m_aCopyGC ) + XFreeGC( pDisp_, rData.m_aMonoGC ); + XFreeGC( pDisp_, rData.m_aCopyGC ); + XFreeGC( pDisp_, rData.m_aAndInvertedGC ); + XFreeGC( pDisp_, rData.m_aAndGC ); + XFreeGC( pDisp_, rData.m_aOrGC ); + XFreeGC( pDisp_, rData.m_aStippleGC ); + XFreePixmap( pDisp_, rData.m_hInvert50 ); + XDestroyWindow( pDisp_, rData.m_aRefWindow ); + Colormap aColMap = rData.m_aColormap.GetXColormap(); + if( aColMap != None && aColMap != DefaultColormap( pDisp_, i ) ) + XFreeColormap( pDisp_, aColMap ); + } + } + + for( const Cursor & aCsr : aPointerCache_ ) + { + if( aCsr ) + XFreeCursor( pDisp_, aCsr ); + } + + if( pXLib_ ) + pXLib_->Remove( ConnectionNumber( pDisp_ ) ); + } + + if( pData->GetDisplay() == static_cast( this ) ) + pData->SetDisplay( nullptr ); +} + +static int DisplayHasEvent( int fd, void * data ) +{ + auto pDisplay = static_cast(data); + SAL_WARN_IF( ConnectionNumber( pDisplay->GetDisplay() ) != fd, "vcl", + "wrong fd in DisplayHasEvent" ); + if( ! pDisplay->IsDisplay() ) + return 0; + + bool result; + + SolarMutexGuard aGuard; + result = pDisplay->IsEvent(); + return int(result); +} +static int DisplayQueue( int fd, void * data ) +{ + auto pDisplay = static_cast(data); + SAL_WARN_IF( ConnectionNumber( pDisplay->GetDisplay() ) != fd, "vcl", + "wrong fd in DisplayHasEvent" ); + int result; + + SolarMutexGuard aGuard; + result = XEventsQueued( pDisplay->GetDisplay(), + QueuedAfterReading ); + return result; +} +static int DisplayYield( int fd, void * data ) +{ + auto pDisplay = static_cast(data); + SAL_WARN_IF( ConnectionNumber( pDisplay->GetDisplay() ) != fd, "vcl", + "wrong fd in DisplayHasEvent" ); + + SolarMutexGuard aGuard; + pDisplay->Yield(); + return 1; +} + +SalX11Display::SalX11Display( Display *display ) + : SalDisplay( display ) +{ + Init(); + + pXLib_ = GetX11SalData()->GetLib(); + pXLib_->Insert( ConnectionNumber( pDisp_ ), + this, + reinterpret_cast(DisplayHasEvent), + reinterpret_cast(DisplayQueue), + reinterpret_cast(DisplayYield) ); +} + +SalX11Display::~SalX11Display() +{ +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.app", "SalX11Display::~SalX11Display()."); +#endif + if( pDisp_ ) + { + doDestruct(); + XCloseDisplay( pDisp_ ); + pDisp_ = nullptr; + } +} + +void SalX11Display::TriggerUserEventProcessing() +{ + if( pXLib_ ) + pXLib_->TriggerUserEventProcessing(); +} + +SalDisplay::ScreenData * +SalDisplay::initScreen( SalX11Screen nXScreen ) const +{ + if( nXScreen.getXScreen() >= m_aScreens.size() ) + nXScreen = m_nXDefaultScreen; + ScreenData* pSD = const_cast(&m_aScreens[nXScreen.getXScreen()]); + if( pSD->m_bInit ) + return nullptr; + pSD->m_bInit = true; + + XVisualInfo aVI; + Colormap aColMap; + + if( SalDisplay::BestVisual( pDisp_, nXScreen.getXScreen(), aVI ) ) // DefaultVisual + aColMap = DefaultColormap( pDisp_, nXScreen.getXScreen() ); + else + aColMap = XCreateColormap( pDisp_, + RootWindow( pDisp_, nXScreen.getXScreen() ), + aVI.visual, + AllocNone ); + + Screen* pScreen = ScreenOfDisplay( pDisp_, nXScreen.getXScreen() ); + + pSD->m_aSize = AbsoluteScreenPixelSize( WidthOfScreen( pScreen ), HeightOfScreen( pScreen ) ); + pSD->m_aRoot = RootWindow( pDisp_, nXScreen.getXScreen() ); + pSD->m_aVisual = SalVisual( &aVI ); + pSD->m_aColormap = SalColormap( this, aColMap, nXScreen ); + + // we're interested in configure notification of root windows + InitRandR( pSD->m_aRoot ); + + // - - - - - - - - - - Reference Window/Default Drawable - - + XSetWindowAttributes aXWAttributes; + aXWAttributes.border_pixel = 0; + aXWAttributes.background_pixel = 0; + aXWAttributes.colormap = aColMap; + pSD->m_aRefWindow = XCreateWindow( pDisp_, + pSD->m_aRoot, + 0,0, 16,16, 0, + pSD->m_aVisual.GetDepth(), + InputOutput, + pSD->m_aVisual.GetVisual(), + CWBorderPixel|CWBackPixel|CWColormap, + &aXWAttributes ); + + // set client leader (session id gets set when session is started) + if( pSD->m_aRefWindow ) + { + // client leader must have WM_CLIENT_LEADER pointing to itself + XChangeProperty( pDisp_, + pSD->m_aRefWindow, + XInternAtom( pDisp_, "WM_CLIENT_LEADER", False ), + XA_WINDOW, + 32, + PropModeReplace, + reinterpret_cast(&pSD->m_aRefWindow), + 1 + ); + + OString aExec(OUStringToOString(SessionManagerClient::getExecName(), osl_getThreadTextEncoding())); + const char* argv[1]; + argv[0] = aExec.getStr(); + XSetCommand( pDisp_, pSD->m_aRefWindow, const_cast(argv), 1 ); + XSelectInput( pDisp_, pSD->m_aRefWindow, PropertyChangeMask ); + + // - - - - - - - - - - GCs - - - - - - - - - - - - - - - - - + XGCValues values; + values.graphics_exposures = False; + values.fill_style = FillOpaqueStippled; + values.background = (1<m_aVisual.GetDepth())-1; + values.foreground = 0; + + pSD->m_aCopyGC = XCreateGC( pDisp_, + pSD->m_aRefWindow, + GCGraphicsExposures + | GCForeground + | GCBackground, + &values ); + pSD->m_aAndInvertedGC= XCreateGC( pDisp_, + pSD->m_aRefWindow, + GCGraphicsExposures + | GCForeground + | GCBackground, + &values ); + pSD->m_aAndGC = XCreateGC( pDisp_, + pSD->m_aRefWindow, + GCGraphicsExposures + | GCForeground + | GCBackground, + &values ); + pSD->m_aOrGC = XCreateGC( pDisp_, + pSD->m_aRefWindow, + GCGraphicsExposures + | GCForeground + | GCBackground, + &values ); + pSD->m_aStippleGC = XCreateGC( pDisp_, + pSD->m_aRefWindow, + GCGraphicsExposures + | GCFillStyle + | GCForeground + | GCBackground, + &values ); + + XSetFunction( pDisp_, pSD->m_aAndInvertedGC, GXandInverted ); + XSetFunction( pDisp_, pSD->m_aAndGC, GXand ); + // PowerPC Solaris 2.5 (XSun 3500) Bug: GXor = GXnop + XSetFunction( pDisp_, pSD->m_aOrGC, GXxor ); + + if( 1 == pSD->m_aVisual.GetDepth() ) + { + XSetFunction( pDisp_, pSD->m_aCopyGC, GXcopyInverted ); + pSD->m_aMonoGC = pSD->m_aCopyGC; + } + else + { + Pixmap hPixmap = XCreatePixmap( pDisp_, pSD->m_aRefWindow, 1, 1, 1 ); + pSD->m_aMonoGC = XCreateGC( pDisp_, + hPixmap, + GCGraphicsExposures, + &values ); + XFreePixmap( pDisp_, hPixmap ); + } + pSD->m_hInvert50 = XCreateBitmapFromData( pDisp_, + pSD->m_aRefWindow, + reinterpret_cast(invert50_bits), + invert50_width, + invert50_height ); + } + return pSD; +} + +void SalDisplay::Init() +{ + for( Cursor & aCsr : aPointerCache_ ) + aCsr = None; + + m_bXinerama = false; + + int nDisplayScreens = ScreenCount( pDisp_ ); + m_aScreens = std::vector(nDisplayScreens); + + bool bExactResolution = false; + /* #i15507# + * Xft resolution should take precedence since + * it is what modern desktops use. + */ + const char* pValStr = XGetDefault( pDisp_, "Xft", "dpi" ); + if( pValStr != nullptr ) + { + const OString aValStr( pValStr ); + const tools::Long nDPI = static_cast(aValStr.toDouble()); + // guard against insane resolution + if( sal_ValidDPI(nDPI) ) + { + aResolution_ = Pair( nDPI, nDPI ); + bExactResolution = true; + } + } + if( !bExactResolution ) + { + /* if Xft.dpi is not set, try and find the DPI from the + * reported screen sizes and resolution. If there are multiple + * screens, just fall back to the default 96x96 + */ + tools::Long xDPI = 96; + tools::Long yDPI = 96; + if (m_aScreens.size() == 1) { + xDPI = static_cast(round(DisplayWidth(pDisp_, 0)*25.4/DisplayWidthMM(pDisp_, 0))); + yDPI = static_cast(round(DisplayHeight(pDisp_, 0)*25.4/DisplayHeightMM(pDisp_, 0))); + // if either is invalid set it equal to the other + if (!sal_ValidDPI(xDPI) && sal_ValidDPI(yDPI)) + xDPI = yDPI; + if (!sal_ValidDPI(yDPI) && sal_ValidDPI(xDPI)) + yDPI = xDPI; + // if both are invalid, reset them to the default + if (!sal_ValidDPI(xDPI) && !sal_ValidDPI(yDPI)) + xDPI = yDPI = 96; + } + aResolution_ = Pair( xDPI, yDPI ); + } + + nMaxRequestSize_ = XExtendedMaxRequestSize( pDisp_ ) * 4; + if( !nMaxRequestSize_ ) + nMaxRequestSize_ = XMaxRequestSize( pDisp_ ) * 4; + + meServerVendor = sal_GetServerVendor(pDisp_); + + // - - - - - - - - - - Synchronize - - - - - - - - - - - - - + if( getenv( "SAL_SYNCHRONIZE" ) ) + XSynchronize( pDisp_, True ); + + // - - - - - - - - - - Keyboardmapping - - - - - - - - - - - + ModifierMapping(); + + // - - - - - - - - - - Window Manager - - - - - - - - - - - + m_pWMAdaptor = ::vcl_sal::WMAdaptor::createWMAdaptor( this ); + + InitXinerama(); + +#ifdef DBG_UTIL + PrintInfo(); +#endif +} + +void SalX11Display::SetupInput() +{ + GetGenericUnixSalData()->ErrorTrapPush(); + SalI18N_KeyboardExtension *pKbdExtension = new SalI18N_KeyboardExtension( pDisp_ ); + XSync( pDisp_, False ); + + bool bError = GetGenericUnixSalData()->ErrorTrapPop( false ); + GetGenericUnixSalData()->ErrorTrapPush(); + pKbdExtension->UseExtension( ! bError ); + GetGenericUnixSalData()->ErrorTrapPop(); + + SetKbdExtension( pKbdExtension ); +} + +// Sound +void SalDisplay::Beep() const +{ + XBell( pDisp_, 100 ); +} + +// Keyboard + +namespace { + +bool InitXkb(Display* dpy) +{ + int nOpcode, nEvent, nError; + int nXkbMajor = XkbMajorVersion; + int nXkbMinor = XkbMinorVersion; + + if (!XkbLibraryVersion(&nXkbMajor, &nXkbMinor)) + return false; + + return XkbQueryExtension( + dpy, &nOpcode, &nEvent, &nError, &nXkbMajor, &nXkbMinor); +} + +unsigned int GetKeySymMask(Display* dpy, KeySym nKeySym) +{ + int nMask = 0; + XModifierKeymap* pXmkMap = XGetModifierMapping(dpy); + KeyCode nKeyCode = XKeysymToKeycode(dpy, nKeySym); + if (nKeyCode == NoSymbol) + return 0; + + for (int i = 0; i < 8; ++i) + { + KeyCode nThisKeyCode = pXmkMap->modifiermap[pXmkMap->max_keypermod*i]; + if (nThisKeyCode == nKeyCode) + nMask = 1 << i; + } + XFreeModifiermap(pXmkMap); + return nMask; +} + +} + +void SalDisplay::SimulateKeyPress( sal_uInt16 nKeyCode ) +{ + if (nKeyCode != KEY_CAPSLOCK) + return; + + Display* dpy = GetDisplay(); + if (!InitXkb(dpy)) + return; + + unsigned int nMask = GetKeySymMask(dpy, XK_Caps_Lock); + XkbStateRec xkbState; + XkbGetState(dpy, XkbUseCoreKbd, &xkbState); + unsigned int nCapsLockState = xkbState.locked_mods & nMask; + if (nCapsLockState) + XkbLockModifiers (dpy, XkbUseCoreKbd, nMask, 0); + else + XkbLockModifiers (dpy, XkbUseCoreKbd, nMask, nMask); +} + +KeyIndicatorState SalDisplay::GetIndicatorState() const +{ + unsigned int _state = 0; + KeyIndicatorState nState = KeyIndicatorState::NONE; + XkbGetIndicatorState(pDisp_, XkbUseCoreKbd, &_state); + + if (_state & 0x00000001) + nState |= KeyIndicatorState::CAPSLOCK; + if (_state & 0x00000002) + nState |= KeyIndicatorState::NUMLOCK; + if (_state & 0x00000004) + nState |= KeyIndicatorState::SCROLLLOCK; + + return nState; +} + +OUString SalDisplay::GetKeyNameFromKeySym( KeySym nKeySym ) const +{ + OUString aLang = Application::GetSettings().GetUILanguageTag().getLanguage(); + OUString aRet; + + // return an empty string for keysyms that are not bound to + // any key code + KeyCode aKeyCode = XKeysymToKeycode( GetDisplay(), nKeySym ); + static_assert(NoSymbol == 0, "X11 inconsistency"); + if( aKeyCode != NoSymbol ) + { + if( !nKeySym ) + aRet = "???"; + else + { + aRet = ::vcl_sal::getKeysymReplacementName( aLang, nKeySym ); + if( aRet.isEmpty() ) + { + const char *pString = XKeysymToString( nKeySym ); + if (pString) + { + int n = strlen( pString ); + if( n > 2 && pString[n-2] == '_' ) + aRet = OUString( pString, n-2, RTL_TEXTENCODING_ISO_8859_1 ); + else + aRet = OUString( pString, n, RTL_TEXTENCODING_ISO_8859_1 ); + } + else + aRet = "???"; + } + } + } + return aRet; +} + +static KeySym sal_XModifier2Keysym( Display *pDisplay, + XModifierKeymap const *pXModMap, + int n ) +{ + return XkbKeycodeToKeysym( pDisplay, + pXModMap->modifiermap[n*pXModMap->max_keypermod], + 0,0 ); +} + +void SalDisplay::ModifierMapping() +{ + XModifierKeymap *pXModMap = XGetModifierMapping( pDisp_ ); + + bNumLockFromXS_ = True; + nShiftKeySym_ = sal_XModifier2Keysym( pDisp_, pXModMap, ShiftMapIndex ); + nCtrlKeySym_ = sal_XModifier2Keysym( pDisp_, pXModMap, ControlMapIndex ); + nMod1KeySym_ = sal_XModifier2Keysym( pDisp_, pXModMap, Mod1MapIndex ); + // on Sun and SCO servers XLookupString does not account for NumLock + if( GetServerVendor() == vendor_sun ) + { + KeyCode aNumLock = XKeysymToKeycode( pDisp_, XK_Num_Lock ); + + if( aNumLock ) + for( int i = ShiftMapIndex; i <= Mod5MapIndex; i++ ) + { + if( pXModMap->modifiermap[i*pXModMap->max_keypermod] == aNumLock ) + { + bNumLockFromXS_ = False; + nNumLockIndex_ = i; + break; + } + } + } + + XFreeModifiermap( pXModMap ); +} + +OUString SalDisplay::GetKeyName( sal_uInt16 nKeyCode ) const +{ + OUString aStrMap; + OUString aCustomKeyName; + + if( nKeyCode & KEY_MOD1 ) + aStrMap += GetKeyNameFromKeySym( nCtrlKeySym_ ); + + if( nKeyCode & KEY_MOD2 ) + { + if( !aStrMap.isEmpty() ) + aStrMap += "+"; + aStrMap += GetKeyNameFromKeySym( nMod1KeySym_ ); + } + + if( nKeyCode & KEY_SHIFT ) + { + if( !aStrMap.isEmpty() ) + aStrMap += "+"; + aStrMap += GetKeyNameFromKeySym( nShiftKeySym_ ); + } + nKeyCode &= 0x0FFF; + + KeySym nKeySym = 0; + + if( KEY_0 <= nKeyCode && nKeyCode <= KEY_9 ) + nKeySym = XK_0 + (nKeyCode - KEY_0); + else if( KEY_A <= nKeyCode && nKeyCode <= KEY_Z ) + nKeySym = XK_A + (nKeyCode - KEY_A); + else if( KEY_F1 <= nKeyCode && nKeyCode <= KEY_F26 ) // does this key exist? + nKeySym = XK_F1 + (nKeyCode - KEY_F1); + else switch( nKeyCode ) + { + case KEY_DOWN: + nKeySym = XK_Down; + break; + case KEY_UP: + nKeySym = XK_Up; + break; + case KEY_LEFT: + nKeySym = XK_Left; + break; + case KEY_RIGHT: + nKeySym = XK_Right; + break; + case KEY_HOME: + nKeySym = XK_Home; + break; + case KEY_END: + nKeySym = XK_End; + break; + case KEY_PAGEUP: + nKeySym = XK_Page_Up; + break; + case KEY_PAGEDOWN: + nKeySym = XK_Page_Down; + break; + case KEY_RETURN: + nKeySym = XK_Return; + break; + case KEY_ESCAPE: + nKeySym = XK_Escape; + break; + case KEY_TAB: + nKeySym = XK_Tab; + break; + case KEY_BACKSPACE: + nKeySym = XK_BackSpace; + break; + case KEY_SPACE: + nKeySym = XK_space; + break; + case KEY_INSERT: + nKeySym = XK_Insert; + break; + case KEY_DELETE: + nKeySym = XK_Delete; + break; + + #if !defined (SunXK_Undo) + // we don't intend to use SunXK_Undo, but if it has not been + // defined already, then we _do_ need the following: + #define SunXK_Props 0x1005FF70 + #define SunXK_Front 0x1005FF71 + #define SunXK_Copy 0x1005FF72 + #define SunXK_Open 0x1005FF73 + #define SunXK_Paste 0x1005FF74 + #define SunXK_Cut 0x1005FF75 + #endif + // the following are for XF86 systems + #define XF86XK_Copy 0x1008FF57 + #define XF86XK_Cut 0x1008FF58 + #define XF86XK_Open 0x1008FF6B + #define XF86XK_Paste 0x1008FF6D + // which leaves Apollo and OSF systems in the lurch + + case KEY_REPEAT: + nKeySym = XK_Redo; + break; + case KEY_PROPERTIES: + nKeySym = SunXK_Props; + break; + case KEY_UNDO: + nKeySym = XK_Undo; + break; + case KEY_FRONT: + nKeySym = SunXK_Front; + break; + case KEY_COPY: + nKeySym = GetServerVendor() == vendor_sun ? SunXK_Copy : XF86XK_Copy; + break; + case KEY_OPEN: + nKeySym = GetServerVendor() == vendor_sun ? SunXK_Open : XF86XK_Open; + break; + case KEY_PASTE: + nKeySym = GetServerVendor() == vendor_sun ? SunXK_Paste : XF86XK_Paste; + break; + case KEY_FIND: + nKeySym = XK_Find; + break; + case KEY_CUT: + nKeySym = GetServerVendor() == vendor_sun ? SunXK_Cut : XF86XK_Cut; + /* The original code here had: + nKeySym = GetServerVendor() == vendor_sun ? SunXK_Cut : XK_L10; + if anyone can remember which non-vendor_sun system used this + XK_L10 keysym, and why this hack only applied to KEY_CUT, + then please re-hack this code to put it back + */ + break; + case KEY_ADD: + aCustomKeyName = "+"; + break; + case KEY_SUBTRACT: + aCustomKeyName = "-"; + break; + case KEY_MULTIPLY: + nKeySym = XK_asterisk; + break; + case KEY_DIVIDE: + nKeySym = XK_slash; + break; + case KEY_POINT: + aCustomKeyName = "."; + break; + case KEY_COMMA: + nKeySym = XK_comma; + break; + case KEY_LESS: + nKeySym = XK_less; + break; + case KEY_GREATER: + nKeySym = XK_greater; + break; + case KEY_EQUAL: + nKeySym = XK_equal; + break; + case KEY_HELP: + nKeySym = XK_Help; + break; + case KEY_HANGUL_HANJA: + nKeySym = XK_Hangul_Hanja; + break; + case KEY_TILDE: + nKeySym = XK_asciitilde; + break; + case KEY_QUOTELEFT: + nKeySym = XK_grave; + break; + case KEY_BRACKETLEFT: + aCustomKeyName = "["; + break; + case KEY_BRACKETRIGHT: + aCustomKeyName = "]"; + break; + case KEY_SEMICOLON: + aCustomKeyName = ";"; + break; + case KEY_QUOTERIGHT: + aCustomKeyName = "'"; + break; + case KEY_RIGHTCURLYBRACKET: + aCustomKeyName = "}"; + break; + case KEY_NUMBERSIGN: + aCustomKeyName = "#"; + break; + case KEY_XF86FORWARD: + aCustomKeyName = "XF86Forward"; + break; + case KEY_XF86BACK: + aCustomKeyName = "XF86Back"; + break; + case KEY_COLON: + aCustomKeyName = ":"; + break; + default: + nKeySym = 0; + break; + } + + if( nKeySym ) + { + OUString aKeyName = GetKeyNameFromKeySym( nKeySym ); + if( !aKeyName.isEmpty() ) + { + if( !aStrMap.isEmpty() ) + aStrMap += "+"; + aStrMap += aKeyName; + } + else + aStrMap.clear(); + } + else if (!aCustomKeyName.isEmpty()) + { + // For semicolon, bracket left and bracket right, it's better to use + // their keys than their names. (fdo#32891) + if (!aStrMap.isEmpty()) + aStrMap += "+"; + aStrMap += aCustomKeyName; + } + else + aStrMap.clear(); + + return aStrMap; +} + +#ifndef IsISOKey +#define IsISOKey( n ) (0x0000FE00==((n)&0xFFFFFF00)) +#endif + +sal_uInt16 SalDisplay::GetKeyCode( KeySym keysym, char*pcPrintable ) const +{ + sal_uInt16 nKey = 0; + + if( XK_a <= keysym && XK_z >= keysym ) + nKey = static_cast(KEY_A + (keysym - XK_a)); + else if( XK_A <= keysym && XK_Z >= keysym ) + nKey = static_cast(KEY_A + (keysym - XK_A)); + else if( XK_0 <= keysym && XK_9 >= keysym ) + nKey = static_cast(KEY_0 + (keysym - XK_0)); + else if( IsModifierKey( keysym ) ) + ; + else if( IsKeypadKey( keysym ) ) + { + if( (keysym >= XK_KP_0) && (keysym <= XK_KP_9) ) + { + nKey = static_cast(KEY_0 + (keysym - XK_KP_0)); + *pcPrintable = '0' + nKey - KEY_0; + } + else if( IsPFKey( keysym ) ) + nKey = static_cast(KEY_F1 + (keysym - XK_KP_F1)); + else switch( keysym ) + { + case XK_KP_Space: + nKey = KEY_SPACE; + *pcPrintable = ' '; + break; + case XK_KP_Tab: + nKey = KEY_TAB; + break; + case XK_KP_Enter: + nKey = KEY_RETURN; + break; + case XK_KP_Begin: + case XK_KP_Home: + nKey = KEY_HOME; + break; + case XK_KP_Left: + nKey = KEY_LEFT; + break; + case XK_KP_Up: + nKey = KEY_UP; + break; + case XK_KP_Right: + nKey = KEY_RIGHT; + break; + case XK_KP_Down: + nKey = KEY_DOWN; + break; + case XK_KP_Page_Up: // XK_KP_Page_Up + nKey = KEY_PAGEUP; + break; + case XK_KP_Page_Down: // XK_KP_Page_Down + nKey = KEY_PAGEDOWN; + break; + case XK_KP_End: + nKey = KEY_END; + break; + case XK_KP_Insert: + nKey = KEY_INSERT; + break; + case XK_KP_Delete: + nKey = KEY_DELETE; + break; + case XK_KP_Equal: + nKey = KEY_EQUAL; + *pcPrintable = '='; + break; + case XK_KP_Multiply: + nKey = KEY_MULTIPLY; + *pcPrintable = '*'; + break; + case XK_KP_Add: + nKey = KEY_ADD; + *pcPrintable = '+'; + break; + case XK_KP_Separator: + nKey = KEY_DECIMAL; + *pcPrintable = ','; + break; + case XK_KP_Subtract: + nKey = KEY_SUBTRACT; + *pcPrintable = '-'; + break; + case XK_KP_Decimal: + nKey = KEY_DECIMAL; + *pcPrintable = '.'; + break; + case XK_KP_Divide: + nKey = KEY_DIVIDE; + *pcPrintable = '/'; + break; + } + } + else if( IsFunctionKey( keysym ) ) + { + if( bNumLockFromXS_ ) + { + if( keysym >= XK_F1 && keysym <= XK_F26 ) + nKey = static_cast(KEY_F1 + keysym - XK_F1); + } + else switch( keysym ) + { + // - - - - - Sun X-Server keyboard without Cursorblock ??? - - - + case XK_R7: // XK_F27: + nKey = KEY_HOME; + break; + case XK_R8: // XK_F28: + nKey = KEY_UP; + break; + case XK_R9: // XK_F29: + nKey = KEY_PAGEUP; + break; + case XK_R10: // XK_F30: + nKey = KEY_LEFT; + break; + case XK_R11: // XK_F31: + nKey = 0; // KEY_F31 + break; + case XK_R12: // XK_F32: + nKey = KEY_RIGHT; + break; + case XK_R13: // XK_F33: + nKey = KEY_END; + break; + case XK_R14: // XK_F34: + nKey = KEY_DOWN; + break; + case XK_R15: // XK_F35: + nKey = KEY_PAGEDOWN; + break; + // - - - - - Sun X-Server keyboard ??? - - - - - - - - - - - - + case XK_L1: // XK_F11: + nKey = KEY_F11; // on a sun keyboard this actually is usually SunXK_Stop = 0x0000FF69 (XK_Cancel), + // but VCL doesn't have a key definition for that + break; + case XK_L2: // XK_F12: + if ( GetServerVendor() == vendor_sun ) + nKey = KEY_REPEAT; + else + nKey = KEY_F12; + break; + case XK_L3: // XK_F13: + nKey = KEY_PROPERTIES; // KEY_F13 + break; + case XK_L4: // XK_F14: + nKey = KEY_UNDO; // KEY_F14 + break; + case XK_L5: // XK_F15: + nKey = KEY_F15; // KEY_FRONT + break; + case XK_L6: // XK_F16: + nKey = KEY_COPY; // KEY_F16 + break; + case XK_L7: // XK_F17: + nKey = KEY_F17; // KEY_OPEN + break; + case XK_L8: // XK_F18: + nKey = KEY_PASTE; // KEY_F18 + break; + case XK_L9: // XK_F19: + nKey = KEY_F19; // KEY_FIND + break; + case XK_L10: // XK_F20: + nKey = KEY_CUT; // KEY_F20 + break; + default: + if( keysym >= XK_F1 && keysym <= XK_F26 ) + nKey = static_cast(KEY_F1 + keysym - XK_F1); + break; + } + } + else if( IsCursorKey( keysym ) ) + { + switch( keysym ) + { + case XK_Begin: + case XK_Home: + nKey = KEY_HOME; + break; + case XK_Left: + nKey = KEY_LEFT; + break; + case XK_Up: + nKey = KEY_UP; + break; + case XK_Right: + nKey = KEY_RIGHT; + break; + case XK_Down: + nKey = KEY_DOWN; + break; + case XK_Page_Up: // XK_Page_Up + nKey = KEY_PAGEUP; + break; + case XK_Page_Down: // XK_Page_Down + nKey = KEY_PAGEDOWN; + break; + case XK_End: + nKey = KEY_END; + break; + } + } + else if( IsMiscFunctionKey( keysym ) ) + { + switch( keysym ) + { + case XK_Insert: + nKey = KEY_INSERT; + break; + case XK_Redo: + nKey = KEY_REPEAT; + break; + case XK_Undo: + nKey = KEY_UNDO; + break; + case XK_Find: + nKey = KEY_FIND; + break; + case XK_Help: + nKey = KEY_HELP; + break; + case XK_Menu: + nKey = KEY_CONTEXTMENU; + break; + } + } + else if( IsISOKey( keysym ) ) // XK_ISO_ + { + switch( keysym ) + { + case 0xFE20: // XK_ISO_Left_Tab: + nKey = KEY_TAB; + break; + } + } + else switch( keysym ) + { + case XK_Return: + nKey = KEY_RETURN; + break; + case XK_BackSpace: + nKey = KEY_BACKSPACE; + break; + case XK_Delete: + nKey = KEY_DELETE; + break; + case XK_space: + nKey = KEY_SPACE; + break; + case XK_Tab: + nKey = KEY_TAB; + break; + case XK_Escape: + nKey = KEY_ESCAPE; + break; + case XK_plus: + nKey = KEY_ADD; + break; + case XK_minus: + nKey = KEY_SUBTRACT; + break; + case XK_asterisk: + nKey = KEY_MULTIPLY; + break; + case XK_slash: + nKey = KEY_DIVIDE; + break; + case XK_period: + nKey = KEY_POINT; + *pcPrintable = '.'; + break; + case XK_comma: + nKey = KEY_COMMA; + break; + case XK_less: + nKey = KEY_LESS; + break; + case XK_greater: + nKey = KEY_GREATER; + break; + case XK_equal: + nKey = KEY_EQUAL; + break; + case XK_Hangul_Hanja: + nKey = KEY_HANGUL_HANJA; + break; + case XK_asciitilde: + nKey = KEY_TILDE; + *pcPrintable = '~'; + break; + case XK_grave: + nKey = KEY_QUOTELEFT; + *pcPrintable = '`'; + break; + case XK_bracketleft: + nKey = KEY_BRACKETLEFT; + *pcPrintable = '['; + break; + case XK_bracketright: + nKey = KEY_BRACKETRIGHT; + *pcPrintable = ']'; + break; + case XK_semicolon: + nKey = KEY_SEMICOLON; + *pcPrintable = ';'; + break; + case XK_quoteright: + nKey = KEY_QUOTERIGHT; + *pcPrintable = '\''; + break; + case XK_braceright: + nKey = KEY_RIGHTCURLYBRACKET; + *pcPrintable = '\''; + break; + case XK_numbersign: + nKey = KEY_NUMBERSIGN; + *pcPrintable = '#'; + break; + case XK_colon: + nKey = KEY_COLON; + *pcPrintable = ':'; + break; + case 0x1008ff27: // tdf#148986: XF86Forward + nKey = KEY_XF86FORWARD; + break; + case 0x1008ff26: // tdf#148986: XF86Back + nKey = KEY_XF86BACK; + break; + // - - - - - - - - - - - - - Apollo - - - - - - - - - - - - - 0x1000 + case 0x1000FF02: // apXK_Copy + nKey = KEY_COPY; + break; + case 0x1000FF03: // apXK_Cut + nKey = KEY_CUT; + break; + case 0x1000FF04: // apXK_Paste + nKey = KEY_PASTE; + break; + case 0x1000FF14: // apXK_Repeat + nKey = KEY_REPEAT; + break; + // Exit, Save + // - - - - - - - - - - - - - - D E C - - - - - - - - - - - - - 0x1000 + case 0x1000FF00: + nKey = KEY_DELETE; + break; + // - - - - - - - - - - - - - - H P - - - - - - - - - - - - - 0x1000 + case 0x1000FF73: // hpXK_DeleteChar + nKey = KEY_DELETE; + break; + case 0x1000FF74: // hpXK_BackTab + case 0x1000FF75: // hpXK_KP_BackTab + nKey = KEY_TAB; + break; + // - - - - - - - - - - - - - - I B M - - - - - - - - - - - - - + // - - - - - - - - - - - - - - O S F - - - - - - - - - - - - - 0x1004 + case 0x1004FF02: // osfXK_Copy + nKey = KEY_COPY; + break; + case 0x1004FF03: // osfXK_Cut + nKey = KEY_CUT; + break; + case 0x1004FF04: // osfXK_Paste + nKey = KEY_PASTE; + break; + case 0x1004FF07: // osfXK_BackTab + nKey = KEY_TAB; + break; + case 0x1004FF08: // osfXK_BackSpace + nKey = KEY_BACKSPACE; + break; + case 0x1004FF1B: // osfXK_Escape + nKey = KEY_ESCAPE; + break; + // Up, Down, Left, Right, PageUp, PageDown + // - - - - - - - - - - - - - - S C O - - - - - - - - - - - - - + // - - - - - - - - - - - - - - S G I - - - - - - - - - - - - - 0x1007 + // - - - - - - - - - - - - - - S N I - - - - - - - - - - - - - + // - - - - - - - - - - - - - - S U N - - - - - - - - - - - - - 0x1005 + case 0x1005FF10: // SunXK_F36 + nKey = KEY_F11; + break; + case 0x1005FF11: // SunXK_F37 + nKey = KEY_F12; + break; + case 0x1005FF70: // SunXK_Props + nKey = KEY_PROPERTIES; + break; + case 0x1005FF71: // SunXK_Front + nKey = KEY_FRONT; + break; + case 0x1005FF72: // SunXK_Copy + nKey = KEY_COPY; + break; + case 0x1005FF73: // SunXK_Open + nKey = KEY_OPEN; + break; + case 0x1005FF74: // SunXK_Paste + nKey = KEY_PASTE; + break; + case 0x1005FF75: // SunXK_Cut + nKey = KEY_CUT; + break; + } + return nKey; +} + +KeySym SalDisplay::GetKeySym( XKeyEvent *pEvent, + char *pPrintable, + int *pLen, + KeySym *pUnmodifiedKeySym, + Status *pStatusReturn, + XIC aInputContext ) const +{ + KeySym nKeySym = 0; + memset( pPrintable, 0, *pLen ); + *pStatusReturn = 0; + + SalI18N_InputMethod* const pInputMethod = + pXLib_ ? pXLib_->GetInputMethod() : nullptr; + + // first get the printable of the possibly modified KeySym + if ( (aInputContext == nullptr) + || (pEvent->type == KeyRelease) + || (pInputMethod != nullptr && pInputMethod->PosixLocale()) ) + { + // XmbLookupString must not be called for KeyRelease events + // Cannot enter space in c locale problem #89616# #88978# btraq #4478197 + *pLen = XLookupString( pEvent, pPrintable, 1, &nKeySym, nullptr ); + } + else + { + *pLen = XmbLookupString( aInputContext, + pEvent, pPrintable, *pLen - 1, &nKeySym, pStatusReturn ); + + // Lookup the string again, now with appropriate size + if ( *pStatusReturn == XBufferOverflow ) + { + pPrintable[ 0 ] = '\0'; + return 0; + } + + switch ( *pStatusReturn ) + { + case XBufferOverflow: + /* unhandled error */ + break; + case XLookupNone: + /* unhandled error */ + break; + case XLookupKeySym: + /* this is a strange one: on exceed sometimes + * no printable is returned for the first char entered, + * just to retry lookup solves the problem. The problem + * is not yet fully understood, so restrict 2nd lookup + * to 7bit ascii chars */ + if ( (XK_space <= nKeySym) && (XK_asciitilde >= nKeySym) ) + { + *pLen = 1; + pPrintable[ 0 ] = static_cast(nKeySym); + } + break; + case XLookupBoth: + case XLookupChars: + + /* nothing to, char already in pPrintable */ + break; + } + } + + if( !bNumLockFromXS_ + && (IsCursorKey(nKeySym) + || IsFunctionKey(nKeySym) + || IsKeypadKey(nKeySym) + || XK_Delete == nKeySym ) ) + { + // For some X-servers special care is needed for Keypad keys. + // For example Solaris XServer: + // 2, 4, 6, 8 are classified as Cursorkeys (Up, Down, Left, Right) + // 1, 3, 5, 9 are classified as Functionkeys (F27,F29,F33,F35) + // 0 as Keypadkey, and the decimal point key not at all (KP_Insert) + KeySym nNewKeySym = XLookupKeysym( pEvent, nNumLockIndex_ ); + if( nNewKeySym != NoSymbol ) + nKeySym = nNewKeySym; + } + + // Now get the unmodified KeySym for KeyCode retrieval + // try to strip off modifiers, e.g. Ctrl-$ becomes Ctrl-Shift-4 + *pUnmodifiedKeySym = XkbKeycodeToKeysym( GetDisplay(), pEvent->keycode, 0, 0); + + return nKeySym; +} + +// Pointer +static unsigned char nullmask_bits[] = { 0x00, 0x00, 0x00, 0x00 }; +static unsigned char nullcurs_bits[] = { 0x00, 0x00, 0x00, 0x00 }; + +#define MAKE_BITMAP( name ) \ + XCreateBitmapFromData( pDisp_, \ + DefaultRootWindow( pDisp_ ), \ + reinterpret_cast(name##_bits), \ + name##_width, \ + name##_height ) + +#define MAKE_CURSOR( name ) \ + aCursBitmap = MAKE_BITMAP( name##curs ); \ + aMaskBitmap = MAKE_BITMAP( name##mask ); \ + nXHot = name##curs_x_hot; \ + nYHot = name##curs_y_hot + +Cursor SalDisplay::GetPointer( PointerStyle ePointerStyle ) +{ + Cursor &aCur = aPointerCache_[ePointerStyle]; + + if( aCur != None ) + return aCur; + + Pixmap aCursBitmap = None, aMaskBitmap = None; + unsigned int nXHot = 0, nYHot = 0; + + switch( ePointerStyle ) + { + case PointerStyle::Null: + MAKE_CURSOR( null ); + break; + case PointerStyle::Arrow: + aCur = XCreateFontCursor( pDisp_, XC_left_ptr ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::Wait: + aCur = XCreateFontCursor( pDisp_, XC_watch ); + break; + case PointerStyle::Text: // Mouse Pointer is a "I" Beam + aCur = XCreateFontCursor( pDisp_, XC_xterm ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::Help: + aCur = XCreateFontCursor( pDisp_, XC_question_arrow ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::Cross: // Mouse Pointer is a cross + aCur = XCreateFontCursor( pDisp_, XC_crosshair ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::NSize: + aCur = XCreateFontCursor( pDisp_, XC_sb_v_double_arrow ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::SSize: + aCur = XCreateFontCursor( pDisp_, XC_sb_v_double_arrow ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::WSize: + aCur = XCreateFontCursor( pDisp_, XC_sb_h_double_arrow ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::ESize: + aCur = XCreateFontCursor( pDisp_, XC_sb_h_double_arrow ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::WindowNSize: + aCur = XCreateFontCursor( pDisp_, XC_top_side ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::WindowSSize: + aCur = XCreateFontCursor( pDisp_, XC_bottom_side ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::WindowWSize: + aCur = XCreateFontCursor( pDisp_, XC_left_side ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::WindowESize: + aCur = XCreateFontCursor( pDisp_, XC_right_side ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::NWSize: + aCur = XCreateFontCursor( pDisp_, XC_top_left_corner ); + break; + case PointerStyle::NESize: + aCur = XCreateFontCursor( pDisp_, XC_top_right_corner ); + break; + case PointerStyle::SWSize: + aCur = XCreateFontCursor( pDisp_, XC_bottom_left_corner ); + break; + case PointerStyle::SESize: + aCur = XCreateFontCursor( pDisp_, XC_bottom_right_corner ); + break; + case PointerStyle::WindowNWSize: + aCur = XCreateFontCursor( pDisp_, XC_top_left_corner ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::WindowNESize: + aCur = XCreateFontCursor( pDisp_, XC_top_right_corner ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::WindowSWSize: + aCur = XCreateFontCursor( pDisp_, XC_bottom_left_corner ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::WindowSESize: + aCur = XCreateFontCursor( pDisp_, XC_bottom_right_corner ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::HSplit: + aCur = XCreateFontCursor( pDisp_, XC_sb_h_double_arrow ); + break; + case PointerStyle::VSplit: + aCur = XCreateFontCursor( pDisp_, XC_sb_v_double_arrow ); + break; + case PointerStyle::HSizeBar: + aCur = XCreateFontCursor( pDisp_, XC_sb_h_double_arrow ); // ??? + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::VSizeBar: + aCur = XCreateFontCursor( pDisp_, XC_sb_v_double_arrow ); // ??? + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::RefHand: + aCur = XCreateFontCursor( pDisp_, XC_hand1 ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::Hand: + aCur = XCreateFontCursor( pDisp_, XC_hand2 ); + break; + case PointerStyle::Magnify: + MAKE_CURSOR( magnify_ ); + break; + case PointerStyle::Fill: + MAKE_CURSOR( fill_ ); + break; + case PointerStyle::Move: + aCur = XCreateFontCursor( pDisp_, XC_fleur ); + break; + case PointerStyle::MoveData: + MAKE_CURSOR( movedata_ ); + break; + case PointerStyle::CopyData: + MAKE_CURSOR( copydata_ ); + break; + case PointerStyle::MoveFile: + MAKE_CURSOR( movefile_ ); + break; + case PointerStyle::CopyFile: + MAKE_CURSOR( copyfile_ ); + break; + case PointerStyle::MoveFiles: + MAKE_CURSOR( movefiles_ ); + break; + case PointerStyle::CopyFiles: + MAKE_CURSOR( copyfiles_ ); + break; + case PointerStyle::NotAllowed: + MAKE_CURSOR( nodrop_ ); + break; + case PointerStyle::Rotate: + MAKE_CURSOR( rotate_ ); + break; + case PointerStyle::HShear: + MAKE_CURSOR( hshear_ ); + break; + case PointerStyle::VShear: + MAKE_CURSOR( vshear_ ); + break; + case PointerStyle::DrawLine: + MAKE_CURSOR( drawline_ ); + break; + case PointerStyle::DrawRect: + MAKE_CURSOR( drawrect_ ); + break; + case PointerStyle::DrawPolygon: + MAKE_CURSOR( drawpolygon_ ); + break; + case PointerStyle::DrawBezier: + MAKE_CURSOR( drawbezier_ ); + break; + case PointerStyle::DrawArc: + MAKE_CURSOR( drawarc_ ); + break; + case PointerStyle::DrawPie: + MAKE_CURSOR( drawpie_ ); + break; + case PointerStyle::DrawCircleCut: + MAKE_CURSOR( drawcirclecut_ ); + break; + case PointerStyle::DrawEllipse: + MAKE_CURSOR( drawellipse_ ); + break; + case PointerStyle::DrawConnect: + MAKE_CURSOR( drawconnect_ ); + break; + case PointerStyle::DrawText: + MAKE_CURSOR( drawtext_ ); + break; + case PointerStyle::Mirror: + MAKE_CURSOR( mirror_ ); + break; + case PointerStyle::Crook: + MAKE_CURSOR( crook_ ); + break; + case PointerStyle::Crop: + MAKE_CURSOR( crop_ ); + break; + case PointerStyle::MovePoint: + MAKE_CURSOR( movepoint_ ); + break; + case PointerStyle::MoveBezierWeight: + MAKE_CURSOR( movebezierweight_ ); + break; + case PointerStyle::DrawFreehand: + MAKE_CURSOR( drawfreehand_ ); + break; + case PointerStyle::DrawCaption: + MAKE_CURSOR( drawcaption_ ); + break; + case PointerStyle::Pen: // Mouse Pointer is a pencil + aCur = XCreateFontCursor( pDisp_, XC_pencil ); + SAL_WARN_IF( aCur == None, "vcl", "GetPointer: Could not define cursor" ); + break; + case PointerStyle::LinkData: + MAKE_CURSOR( linkdata_ ); + break; + case PointerStyle::MoveDataLink: + MAKE_CURSOR( movedlnk_ ); + break; + case PointerStyle::CopyDataLink: + MAKE_CURSOR( copydlnk_ ); + break; + case PointerStyle::LinkFile: + MAKE_CURSOR( linkfile_ ); + break; + case PointerStyle::MoveFileLink: + MAKE_CURSOR( moveflnk_ ); + break; + case PointerStyle::CopyFileLink: + MAKE_CURSOR( copyflnk_ ); + break; + case PointerStyle::Chart: + MAKE_CURSOR( chart_ ); + break; + case PointerStyle::Detective: + MAKE_CURSOR( detective_ ); + break; + case PointerStyle::PivotCol: + MAKE_CURSOR( pivotcol_ ); + break; + case PointerStyle::PivotRow: + MAKE_CURSOR( pivotrow_ ); + break; + case PointerStyle::PivotField: + MAKE_CURSOR( pivotfld_ ); + break; + case PointerStyle::PivotDelete: + MAKE_CURSOR( pivotdel_ ); + break; + case PointerStyle::Chain: + MAKE_CURSOR( chain_ ); + break; + case PointerStyle::ChainNotAllowed: + MAKE_CURSOR( chainnot_ ); + break; + case PointerStyle::AutoScrollN: + MAKE_CURSOR(asn_ ); + break; + case PointerStyle::AutoScrollS: + MAKE_CURSOR( ass_ ); + break; + case PointerStyle::AutoScrollW: + MAKE_CURSOR( asw_ ); + break; + case PointerStyle::AutoScrollE: + MAKE_CURSOR( ase_ ); + break; + case PointerStyle::AutoScrollNW: + MAKE_CURSOR( asnw_ ); + break; + case PointerStyle::AutoScrollNE: + MAKE_CURSOR( asne_ ); + break; + case PointerStyle::AutoScrollSW: + MAKE_CURSOR( assw_ ); + break; + case PointerStyle::AutoScrollSE: + MAKE_CURSOR( asse_ ); + break; + case PointerStyle::AutoScrollNS: + MAKE_CURSOR( asns_ ); + break; + case PointerStyle::AutoScrollWE: + MAKE_CURSOR( aswe_ ); + break; + case PointerStyle::AutoScrollNSWE: + MAKE_CURSOR( asnswe_ ); + break; + case PointerStyle::TextVertical: + MAKE_CURSOR( vertcurs_ ); + break; + + // #i32329# Enhanced table selection + case PointerStyle::TabSelectS: + MAKE_CURSOR( tblsels_ ); + break; + case PointerStyle::TabSelectE: + MAKE_CURSOR( tblsele_ ); + break; + case PointerStyle::TabSelectSE: + MAKE_CURSOR( tblselse_ ); + break; + case PointerStyle::TabSelectW: + MAKE_CURSOR( tblselw_ ); + break; + case PointerStyle::TabSelectSW: + MAKE_CURSOR( tblselsw_ ); + break; + + case PointerStyle::HideWhitespace: + MAKE_CURSOR( hidewhitespace_ ); + break; + case PointerStyle::ShowWhitespace: + MAKE_CURSOR( showwhitespace_ ); + break; + case PointerStyle::FatCross: + MAKE_CURSOR( fatcross_ ); + break; + + default: + OSL_FAIL("pointer not implemented"); + aCur = XCreateFontCursor( pDisp_, XC_arrow ); + break; + } + + if( None == aCur ) + { + XColor aBlack, aWhite, aDummy; + Colormap hColormap = GetColormap(m_nXDefaultScreen).GetXColormap(); + + XAllocNamedColor( pDisp_, hColormap, "black", &aBlack, &aDummy ); + XAllocNamedColor( pDisp_, hColormap, "white", &aWhite, &aDummy ); + + aCur = XCreatePixmapCursor( pDisp_, + aCursBitmap, aMaskBitmap, + &aBlack, &aWhite, + nXHot, nYHot ); + + XFreePixmap( pDisp_, aCursBitmap ); + XFreePixmap( pDisp_, aMaskBitmap ); + } + + return aCur; +} + +int SalDisplay::CaptureMouse( SalFrame *pCapture ) +{ + static const char* pEnv = getenv( "SAL_NO_MOUSEGRABS" ); + + if( !pCapture ) + { + m_pCapture = nullptr; + if( !pEnv || !*pEnv ) + XUngrabPointer( GetDisplay(), CurrentTime ); + XFlush( GetDisplay() ); + return 0; + } + + m_pCapture = nullptr; + + // FIXME: get rid of X11SalFrame + const SystemEnvData* pEnvData = pCapture->GetSystemData(); + if( !pEnv || !*pEnv ) + { + int ret = XGrabPointer( GetDisplay(), + static_cast<::Window>(pEnvData->GetWindowHandle(pCapture)), + False, + PointerMotionMask| ButtonPressMask|ButtonReleaseMask, + GrabModeAsync, + GrabModeAsync, + None, + static_cast(pCapture)->GetCursor(), + CurrentTime ); + + if( ret != GrabSuccess ) + { + SAL_WARN("vcl", "SalDisplay::CaptureMouse could not grab pointer: " << ret); + return -1; + } + } + + m_pCapture = pCapture; + return 1; +} + +// Events + +bool SalX11Display::IsEvent() +{ + if( HasUserEvents() || XEventsQueued( pDisp_, QueuedAlready ) ) + return true; + + XFlush( pDisp_ ); + return false; +} + +void SalX11Display::Yield() +{ + if( DispatchInternalEvent() ) + return; + + XEvent aEvent; + DBG_ASSERT(GetSalInstance()->GetYieldMutex()->IsCurrentThread(), + "will crash soon since solar mutex not locked in SalDisplay::Yield" ); + + XNextEvent( pDisp_, &aEvent ); + + // coverity[overrun-buffer-val : FALSE] - coverity has problems with uno::Sequence + Dispatch( &aEvent ); + +#ifdef DBG_UTIL + if( GetX11SalData()->HasXErrorOccurred() ) + { + XFlush( pDisp_ ); + DbgPrintDisplayEvent("SalDisplay::Yield (WasXError)", &aEvent); + } +#endif + GetX11SalData()->ResetXErrorOccurred(); +} + +void SalX11Display::Dispatch( XEvent *pEvent ) +{ + SalI18N_InputMethod* const pInputMethod = + pXLib_ ? pXLib_->GetInputMethod() : nullptr; + + if( pInputMethod ) + { + ::Window aFrameWindow = None; + if( pEvent->type == KeyPress || pEvent->type == KeyRelease ) + { + const ::Window aWindow = pEvent->xkey.window; + for( auto pSalFrame : m_aFrames ) + { + const X11SalFrame* pFrame = static_cast< const X11SalFrame* >( pSalFrame ); + const ::Window aCurFrameWindow = pFrame->GetWindow(); + if( aCurFrameWindow == aWindow || pFrame->GetShellWindow() == aWindow ) + { + aFrameWindow = aCurFrameWindow; + break; + } + } + } + if( pInputMethod->FilterEvent( pEvent, aFrameWindow ) ) + return; + } + + SalInstance* pInstance = GetSalInstance(); + pInstance->CallEventCallback( pEvent, sizeof( XEvent ) ); + + switch( pEvent->type ) + { + case MotionNotify: + while( XCheckWindowEvent( pEvent->xany.display, + pEvent->xany.window, + ButtonMotionMask, + pEvent ) ) + ; + m_nLastUserEventTime = pEvent->xmotion.time; + break; + case PropertyNotify: + if( pEvent->xproperty.atom == getWMAdaptor()->getAtom( WMAdaptor::VCL_SYSTEM_SETTINGS ) ) + { + for(const ScreenData & rScreen : m_aScreens) + { + if( pEvent->xproperty.window == rScreen.m_aRefWindow ) + { + for (auto pSalFrame : m_aFrames ) + pSalFrame->CallCallback( SalEvent::SettingsChanged, nullptr ); + return; + } + } + } + break; + case MappingNotify: + if( MappingModifier == pEvent->xmapping.request ) + { + XRefreshKeyboardMapping( &pEvent->xmapping ); + ModifierMapping(); + } + break; + case ButtonPress: + case ButtonRelease: + m_nLastUserEventTime = pEvent->xbutton.time; + break; + case KeyPress: + case KeyRelease: + m_nLastUserEventTime = pEvent->xkey.time; + break; + default: + + if ( GetKbdExtension()->UseExtension() + && GetKbdExtension()->GetEventBase() == pEvent->type ) + { + GetKbdExtension()->Dispatch( pEvent ); + return; + } + break; + } + + for (auto pSalFrame : m_aFrames ) + { + X11SalFrame* pFrame = static_cast( pSalFrame ); + + ::Window aDispatchWindow = pEvent->xany.window; + if( pFrame->GetWindow() == aDispatchWindow + || pFrame->GetShellWindow() == aDispatchWindow + || pFrame->GetForeignParent() == aDispatchWindow + ) + { + pFrame->Dispatch( pEvent ); + return; + } + if( pEvent->type == ConfigureNotify && pEvent->xconfigure.window == pFrame->GetStackingWindow() ) + { + pFrame->Dispatch( pEvent ); + return; + } + } + + // dispatch to salobjects + X11SalObject::Dispatch( pEvent ); + + // is this perhaps a root window that changed size ? + processRandREvent( pEvent ); +} + +#ifdef DBG_UTIL +void SalDisplay::DbgPrintDisplayEvent(const char *pComment, const XEvent *pEvent) const +{ + static const char* const EventNames[] = + { + nullptr, + nullptr, + "KeyPress", + "KeyRelease", + "ButtonPress", + "ButtonRelease", + "MotionNotify", + "EnterNotify", + "LeaveNotify", + "FocusIn", + "FocusOut", + "KeymapNotify", + "Expose", + "GraphicsExpose", + "NoExpose", + "VisibilityNotify", + "CreateNotify", + "DestroyNotify", + "UnmapNotify", + "MapNotify", + "MapRequest", + "ReparentNotify", + "ConfigureNotify", + "ConfigureRequest", + "GravityNotify", + "ResizeRequest", + "CirculateNotify", + "CirculateRequest", + "PropertyNotify", + "SelectionClear", + "SelectionRequest", + "SelectionNotify", + "ColormapNotify", + "ClientMessage", + "MappingNotify" + }; + + if( pEvent->type <= MappingNotify ) + { + SAL_INFO("vcl.app", "[" << pComment << "] " + << EventNames[pEvent->type] + << " s=" << pEvent->xany.send_event + << " w=" << pEvent->xany.window); + + switch( pEvent->type ) + { + case KeyPress: + case KeyRelease: + SAL_INFO("vcl.app", "\t\ts=" << pEvent->xkey.state + << " c=" << pEvent->xkey.keycode); + break; + + case ButtonPress: + case ButtonRelease: + SAL_INFO("vcl.app", "\t\ts=" << pEvent->xbutton.state + << " b=" << pEvent->xbutton.button + << " x=" << pEvent->xbutton.x + << " y=" << pEvent->xbutton.y + << " rx=" << pEvent->xbutton.x_root + << " ry=" << pEvent->xbutton.y_root); + break; + + case MotionNotify: + SAL_INFO("vcl.app", "\t\ts=" << pEvent->xmotion.state + << " x=" << pEvent->xmotion.x + << " y=" << pEvent->xmotion.y); + break; + + case EnterNotify: + case LeaveNotify: + SAL_INFO("vcl.app", "\t\tm=" << pEvent->xcrossing.mode + << " f=" << pEvent->xcrossing.focus + << " x=" << pEvent->xcrossing.x + << " y=" << pEvent->xcrossing.y); + break; + + case FocusIn: + case FocusOut: + SAL_INFO("vcl.app", "\t\tm=" << pEvent->xfocus.mode + << " d=" << pEvent->xfocus.detail); + break; + + case Expose: + case GraphicsExpose: + SAL_INFO("vcl.app", "\t\tc=" << pEvent->xexpose.count + << " " << pEvent->xexpose.width + << "*" << pEvent->xexpose.height + << " " << pEvent->xexpose.x + << "+" << pEvent->xexpose.y ); + break; + + case VisibilityNotify: + SAL_INFO("vcl.app", "\t\ts=" << pEvent->xvisibility.state); + break; + + case CreateNotify: + case DestroyNotify: + break; + + case MapNotify: + case UnmapNotify: + break; + + case ReparentNotify: + SAL_INFO("vcl.app", "\t\tp=" << sal::static_int_cast< int >( + pEvent->xreparent.parent) + << " x=" << pEvent->xreparent.x + << " y=" << pEvent->xreparent.y ); + break; + + case ConfigureNotify: + SAL_INFO("vcl.app", "\t\tb=" << pEvent->xconfigure.border_width + << " " << pEvent->xconfigure.width + << "*" << pEvent->xconfigure.height + << " " << pEvent->xconfigure.x + << "+" << pEvent->xconfigure.y); + break; + + case PropertyNotify: + SAL_INFO("vcl.app", "\t\ta=" << GetAtomName( + pDisp_, pEvent->xproperty.atom) + << std::showbase << std::hex << std::uppercase + << " (" << sal::static_int_cast< unsigned int >( + pEvent->xproperty.atom) << ")."); + break; + + case ColormapNotify: + SAL_INFO("vcl.app", "\t\tc=" << pEvent->xcolormap.colormap + << " n=" << pEvent->xcolormap.c_new + << " s=" << pEvent->xcolormap.state); + break; + + case ClientMessage: + SAL_INFO("vcl.app", "\t\ta=" << GetAtomName( + pDisp_, pEvent->xclient.message_type) + << std::showbase << std::hex << std::uppercase + << " (" << sal::static_int_cast< unsigned int >( + pEvent->xclient.message_type) << ")" + << std::dec + << " f=" << pEvent->xclient.format + << std::hex + << " [" << pEvent->xclient.data.l[0] + << "," << pEvent->xclient.data.l[1] + << "," << pEvent->xclient.data.l[2] + << "," << pEvent->xclient.data.l[3] + << "," << pEvent->xclient.data.l[4] + << "]"); + break; + + case MappingNotify: + SAL_INFO("vcl.app", "\t\tr=" + << (MappingModifier == pEvent->xmapping.request ? + "MappingModifier" : + (MappingKeyboard == pEvent->xmapping.request ? + "MappingKeyboard" : "MappingPointer")) + << "d"); + + break; + } + } + else + SAL_INFO("vcl.app", "[" << pComment << "] " + << pEvent->type + << " s=" << pEvent->xany.send_event + << " w=" << pEvent->xany.window); +} + +void SalDisplay::PrintInfo() const +{ + if( IsDisplay() ) + { + SAL_INFO( "vcl", "Environment" ); + SAL_INFO( "vcl", "\t$DISPLAY \t\"" << GetEnv( "DISPLAY" ) << "\""); + SAL_INFO( "vcl", "\t$SAL_VISUAL \t\"" << GetEnv( "SAL_VISUAL" ) << "\""); + SAL_INFO( "vcl", "\t$SAL_IGNOREXERRORS\t\"" << GetEnv( "SAL_IGNOREXERRORS" ) << "\""); + SAL_INFO( "vcl", "\t$SAL_PROPERTIES \t\"" << GetEnv( "SAL_PROPERTIES" ) << "\""); + SAL_INFO( "vcl", "\t$SAL_SYNCHRONIZE \t\"" << GetEnv( "SAL_SYNCHRONIZE" ) << "\""); + + char sHostname[ 120 ]; + gethostname (sHostname, 120 ); + SAL_INFO( "vcl", "Client" ); + SAL_INFO( "vcl", "\tHost \t\"" << sHostname << "\""); + + SAL_INFO( "vcl", "Display" ); + SAL_INFO( "vcl", "\tHost \t\"" << DisplayString(pDisp_) << "\""); + SAL_INFO( "vcl", "\tVendor (Release) \t\"" << ServerVendor(pDisp_) << " (" << VendorRelease(pDisp_) << ")\""); + SAL_INFO( "vcl", "\tProtocol \t" << ProtocolVersion(pDisp_) << "." << ProtocolRevision(pDisp_) ); + SAL_INFO( "vcl", "\tScreen (count,def)\t" << m_nXDefaultScreen.getXScreen() << " (" << ScreenCount(pDisp_) << "," << DefaultScreen(pDisp_) << ")"); + SAL_INFO( "vcl", "\tshift ctrl alt \t" << KeyStr( nShiftKeySym_ ) << " (0x" << std::hex << sal::static_int_cast< unsigned int >(nShiftKeySym_) << ") " + << KeyStr( nCtrlKeySym_ ) << " (0x" << sal::static_int_cast< unsigned int >(nCtrlKeySym_) << ") " + << KeyStr( nMod1KeySym_ ) << " (0x" << sal::static_int_cast< unsigned int >(nMod1KeySym_) << ")"); + if( XExtendedMaxRequestSize(pDisp_) != 0 ) + SAL_INFO( "vcl", "\tXMaxRequestSize \t" << XMaxRequestSize(pDisp_) * 4 << " " << XExtendedMaxRequestSize(pDisp_) * 4 << " [bytes]"); + SAL_INFO( "vcl", "\tWMName \t" << getWMAdaptor()->getWindowManagerName() ); + } + SAL_INFO( "vcl", "Screen" ); + SAL_INFO( "vcl", "\tResolution/Size \t" << aResolution_.A() << "*" << aResolution_.B() + << " " << m_aScreens[m_nXDefaultScreen.getXScreen()].m_aSize.Width() << "*" << m_aScreens[m_nXDefaultScreen.getXScreen()].m_aSize.Height() + << " " << (std::hypot( DisplayWidthMM ( pDisp_, m_nXDefaultScreen.getXScreen() ), + DisplayHeightMM( pDisp_, m_nXDefaultScreen.getXScreen() ) ) / 25.4 ) << "\"" ); + SAL_INFO( "vcl", "\tBlack&White \t" << GetColormap(m_nXDefaultScreen).GetBlackPixel() << " " + << GetColormap(m_nXDefaultScreen).GetWhitePixel() ); + SAL_INFO( "vcl", "\tRGB \t0x" << std::hex << GetVisual(m_nXDefaultScreen).red_mask + << " 0x" << GetVisual(m_nXDefaultScreen).green_mask + << " 0x" << GetVisual(m_nXDefaultScreen).blue_mask); +} +#endif + +void SalDisplay::addXineramaScreenUnique( int i, tools::Long i_nX, tools::Long i_nY, tools::Long i_nWidth, tools::Long i_nHeight ) +{ + // see if any frame buffers are at the same coordinates + // this can happen with weird configuration e.g. on + // XFree86 and Clone displays + const size_t nScreens = m_aXineramaScreens.size(); + for( size_t n = 0; n < nScreens; n++ ) + { + if( m_aXineramaScreens[n].Left() == i_nX && + m_aXineramaScreens[n].Top() == i_nY ) + { + if( m_aXineramaScreens[n].GetWidth() < i_nWidth || + m_aXineramaScreens[n].GetHeight() < i_nHeight ) + { + m_aXineramaScreenIndexMap[i] = n; + m_aXineramaScreens[n].SetSize( AbsoluteScreenPixelSize( i_nWidth, i_nHeight ) ); + } + return; + } + } + m_aXineramaScreenIndexMap[i] = m_aXineramaScreens.size(); + m_aXineramaScreens.emplace_back( AbsoluteScreenPixelPoint( i_nX, i_nY ), AbsoluteScreenPixelSize( i_nWidth, i_nHeight ) ); +} + +void SalDisplay::InitXinerama() +{ + if( m_aScreens.size() > 1 ) + { + m_bXinerama = false; + return; // multiple screens mean no xinerama + } + if( !XineramaIsActive( pDisp_ ) ) + return; + + int nFramebuffers = 1; + XineramaScreenInfo* pScreens = XineramaQueryScreens( pDisp_, &nFramebuffers ); + if( !pScreens ) + return; + + if( nFramebuffers > 1 ) + { + m_aXineramaScreens = std::vector(); + m_aXineramaScreenIndexMap = std::vector(nFramebuffers); + for( int i = 0; i < nFramebuffers; i++ ) + { + addXineramaScreenUnique( i, pScreens[i].x_org, + pScreens[i].y_org, + pScreens[i].width, + pScreens[i].height ); + } + m_bXinerama = m_aXineramaScreens.size() > 1; + } + XFree( pScreens ); +#if OSL_DEBUG_LEVEL > 1 + if( m_bXinerama ) + { + for (auto const& screen : m_aXineramaScreens) + SAL_INFO("vcl.app", "Xinerama screen: " + << screen.GetWidth() + << "x" << screen.GetHeight() + << "+" << screen.Left() + << "+" << screen.Top()); + } +#endif +} + +extern "C" +{ + static Bool timestamp_predicate( Display*, XEvent* i_pEvent, XPointer i_pArg ) + { + SalDisplay* pSalDisplay = reinterpret_cast(i_pArg); + if( i_pEvent->type == PropertyNotify && + i_pEvent->xproperty.window == pSalDisplay->GetDrawable( pSalDisplay->GetDefaultXScreen() ) && + i_pEvent->xproperty.atom == pSalDisplay->getWMAdaptor()->getAtom( WMAdaptor::SAL_GETTIMEEVENT ) + ) + return True; + + return False; + } +} + +Time SalDisplay::GetEventTimeImpl( bool i_bAlwaysReget ) const +{ + if( m_nLastUserEventTime == CurrentTime || i_bAlwaysReget ) + { + // get current server time + unsigned char c = 0; + XEvent aEvent; + Atom nAtom = getWMAdaptor()->getAtom( WMAdaptor::SAL_GETTIMEEVENT ); + XChangeProperty( GetDisplay(), GetDrawable( GetDefaultXScreen() ), + nAtom, nAtom, 8, PropModeReplace, &c, 1 ); + XIfEvent( GetDisplay(), &aEvent, timestamp_predicate, reinterpret_cast(const_cast(this))); + m_nLastUserEventTime = aEvent.xproperty.time; + } + return m_nLastUserEventTime; +} + +SalVisual::SalVisual() +{ + visual = nullptr; +} + +SalVisual::SalVisual( const XVisualInfo* pXVI ) +{ + *static_cast(this) = *pXVI; +} + +// Converts the order of bytes of a Pixel into bytes of a Color +// This is not reversible for the 6 XXXA + +// Color is RGB (ABGR) a=0xFF000000, r=0xFF0000, g=0xFF00, b=0xFF + +SalColormap::SalColormap( const SalDisplay *pDisplay, Colormap hColormap, + SalX11Screen nXScreen ) + : m_pDisplay( pDisplay ), + m_hColormap( hColormap ) +{ + m_aVisual = m_pDisplay->GetVisual( nXScreen ); + + XColor aColor; + + GetXPixel( aColor, 0x00, 0x00, 0x00 ); + m_nBlackPixel = aColor.pixel; + + GetXPixel( aColor, 0xFF, 0xFF, 0xFF ); + m_nWhitePixel = aColor.pixel; + + m_nUsed = 1 << m_aVisual.GetDepth(); + + if( m_aVisual.GetClass() != PseudoColor ) + return; + + int r, g, b; + + // black, white, gray, ~gray = 4 + GetXPixels( aColor, 0xC0, 0xC0, 0xC0 ); + + // light colors: 3 * 2 = 6 + + GetXPixels( aColor, 0x00, 0x00, 0xFF ); + GetXPixels( aColor, 0x00, 0xFF, 0x00 ); + GetXPixels( aColor, 0x00, 0xFF, 0xFF ); + + // standard colors: 7 * 2 = 14 + GetXPixels( aColor, 0x00, 0x00, 0x80 ); + GetXPixels( aColor, 0x00, 0x80, 0x00 ); + GetXPixels( aColor, 0x00, 0x80, 0x80 ); + GetXPixels( aColor, 0x80, 0x00, 0x00 ); + GetXPixels( aColor, 0x80, 0x00, 0x80 ); + GetXPixels( aColor, 0x80, 0x80, 0x00 ); + GetXPixels( aColor, 0x80, 0x80, 0x80 ); + GetXPixels( aColor, 0x00, 0xB8, 0xFF ); // Blue 7 + + // cube: 6*6*6 - 8 = 208 + for( r = 0; r < 0x100; r += 0x33 ) // 0x33, 0x66, 0x99, 0xCC, 0xFF + for( g = 0; g < 0x100; g += 0x33 ) + for( b = 0; b < 0x100; b += 0x33 ) + GetXPixels( aColor, r, g, b ); + + // gray: 16 - 6 = 10 + for( g = 0x11; g < 0xFF; g += 0x11 ) + GetXPixels( aColor, g, g, g ); + + // green: 16 - 6 = 10 + for( g = 0x11; g < 0xFF; g += 0x11 ) + GetXPixels( aColor, 0, g, 0 ); + + // red: 16 - 6 = 10 + for( r = 0x11; r < 0xFF; r += 0x11 ) + GetXPixels( aColor, r, 0, 0 ); + + // blue: 16 - 6 = 10 + for( b = 0x11; b < 0xFF; b += 0x11 ) + GetXPixels( aColor, 0, 0, b ); + +} + +// MonoChrome +SalColormap::SalColormap() + : m_pDisplay( vcl_sal::getSalDisplay(GetGenericUnixSalData()) ), + m_hColormap( None ), + m_nWhitePixel( 1 ), + m_nBlackPixel( 0 ), + m_nUsed( 2 ) +{ + m_aPalette = std::vector(m_nUsed); + + m_aPalette[m_nBlackPixel] = COL_BLACK; + m_aPalette[m_nWhitePixel] = COL_WHITE; +} + +// TrueColor +SalColormap::SalColormap( sal_uInt16 nDepth ) + : m_pDisplay( vcl_sal::getSalDisplay(GetGenericUnixSalData()) ), + m_hColormap( None ), + m_nWhitePixel( (1 << nDepth) - 1 ), + m_nBlackPixel( 0x00000000 ), + m_nUsed( 1 << nDepth ) +{ + SalX11Screen nXScreen( vcl_sal::getSalDisplay(GetGenericUnixSalData())->GetDefaultXScreen() ); + const SalVisual *pVisual = &m_pDisplay->GetVisual( nXScreen ); + + if( pVisual->GetClass() == TrueColor && pVisual->GetDepth() == nDepth ) + m_aVisual = *pVisual; + else + { + XVisualInfo aVI; + + if( !XMatchVisualInfo( m_pDisplay->GetDisplay(), + m_pDisplay->GetDefaultXScreen().getXScreen(), + nDepth, + TrueColor, + &aVI ) ) + { + aVI.visual = new Visual; + aVI.visualid = VisualID(-1); + aVI.screen = -1; + aVI.depth = nDepth; + aVI.c_class = TrueColor; + if( 24 == nDepth ) // 888 + { + aVI.red_mask = 0xFF0000; + aVI.green_mask = 0x00FF00; + aVI.blue_mask = 0x0000FF; + } + else if( 8 == nDepth ) // 332 + { + aVI.red_mask = 0x0000E0; + aVI.green_mask = 0x00001C; + aVI.blue_mask = 0x000003; + } + else + { + aVI.red_mask = 0x000000; + aVI.green_mask = 0x000000; + aVI.blue_mask = 0x000000; + } + aVI.colormap_size = 0; + aVI.bits_per_rgb = 8; + + aVI.visual->ext_data = nullptr; + aVI.visual->visualid = aVI.visualid; + aVI.visual->c_class = aVI.c_class; + aVI.visual->red_mask = aVI.red_mask; + aVI.visual->green_mask = aVI.green_mask; + aVI.visual->blue_mask = aVI.blue_mask; + aVI.visual->bits_per_rgb = aVI.bits_per_rgb; + aVI.visual->map_entries = aVI.colormap_size; + + m_aVisual = SalVisual( &aVI ); + m_aVisualOwnership.owner = true; + } + else + m_aVisual = SalVisual( &aVI ); + } +} + +SalColormap::~SalColormap() +{ + if (m_aVisualOwnership.owner) + { + delete m_aVisual.visual; + } +} + +inline bool SalColormap::GetXPixel( XColor &rColor, + int r, + int g, + int b ) const +{ + rColor.red = r * 257; + rColor.green = g * 257; + rColor.blue = b * 257; + return XAllocColor( GetXDisplay(), m_hColormap, &rColor ); +} + +bool SalColormap::GetXPixels( XColor &rColor, + int r, + int g, + int b ) const +{ + if( !GetXPixel( rColor, r, g, b ) ) + return false; + if( rColor.pixel & 1 ) + return true; + return GetXPixel( rColor, r^0xFF, g^0xFF, b^0xFF ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/app/salinst.cxx b/vcl/unx/generic/app/salinst.cxx new file mode 100644 index 0000000000..a77aca2648 --- /dev/null +++ b/vcl/unx/generic/app/salinst.cxx @@ -0,0 +1,253 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include +#include +#if HAVE_FEATURE_SKIA +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +// plugin factory function +extern "C" +{ + VCLPLUG_GEN_PUBLIC SalInstance* create_SalInstance() + { + /* #i92121# workaround deadlocks in the X11 implementation + */ + static const char* pNoXInitThreads = getenv( "SAL_NO_XINITTHREADS" ); + /* #i90094# + from now on we know that an X connection will be + established, so protect X against itself + */ + if( ! ( pNoXInitThreads && *pNoXInitThreads ) ) + XInitThreads(); + + X11SalInstance* pInstance = new X11SalInstance( std::make_unique() ); + + // initialize SalData + X11SalData *pSalData = new X11SalData(); + + pSalData->Init(); + pInstance->SetLib( pSalData->GetLib() ); + + return pInstance; + } +} + +X11SalInstance::X11SalInstance(std::unique_ptr pMutex) + : SalGenericInstance(std::move(pMutex)) + , mpXLib(nullptr) +{ + ImplSVData* pSVData = ImplGetSVData(); + pSVData->maAppData.mxToolkitName = OUString("x11"); + m_bSupportsOpenGL = true; +#if HAVE_FEATURE_SKIA + X11SkiaSalGraphicsImpl::prepareSkia(); +#if SKIA_USE_BITMAP32 + if (SkiaHelper::isVCLSkiaEnabled()) + m_bSupportsBitmap32 = true; +#endif +#endif +} + +X11SalInstance::~X11SalInstance() +{ + // close session management + SessionManagerClient::close(); + + // dispose SalDisplay list from SalData + // would be done in a static destructor else which is + // a little late + GetGenericUnixSalData()->Dispose(); + +#if HAVE_FEATURE_SKIA + SkiaHelper::cleanup(); +#endif +} + +SalX11Display* X11SalInstance::CreateDisplay() const +{ + return new SalX11Display( mpXLib->GetDisplay() ); +} + +// AnyInput from sv/mow/source/app/svapp.cxx + +namespace { + +struct PredicateReturn +{ + VclInputFlags nType; + bool bRet; +}; + +} + +extern "C" { +static Bool ImplPredicateEvent( Display *, XEvent *pEvent, char *pData ) +{ + PredicateReturn *pPre = reinterpret_cast(pData); + + if ( pPre->bRet ) + return False; + + VclInputFlags nType; + + switch( pEvent->type ) + { + case ButtonPress: + case ButtonRelease: + case MotionNotify: + case EnterNotify: + case LeaveNotify: + nType = VclInputFlags::MOUSE; + break; + + case KeyPress: + //case KeyRelease: + nType = VclInputFlags::KEYBOARD; + break; + case Expose: + case GraphicsExpose: + case NoExpose: + nType = VclInputFlags::PAINT; + break; + default: + nType = VclInputFlags::NONE; + } + + if ( (nType & pPre->nType) || ( nType == VclInputFlags::NONE && (pPre->nType & VclInputFlags::OTHER) ) ) + pPre->bRet = true; + + return False; +} +} + +bool X11SalInstance::AnyInput(VclInputFlags nType) +{ + GenericUnixSalData *pData = GetGenericUnixSalData(); + Display *pDisplay = vcl_sal::getSalDisplay(pData)->GetDisplay(); + bool bRet = false; + + if( (nType & VclInputFlags::TIMER) && (mpXLib && mpXLib->CheckTimeout(false)) ) + bRet = true; + + if( !bRet && XPending(pDisplay) ) + { + PredicateReturn aInput; + XEvent aEvent; + + aInput.bRet = false; + aInput.nType = nType; + + XCheckIfEvent(pDisplay, &aEvent, ImplPredicateEvent, + reinterpret_cast(&aInput) ); + + bRet = aInput.bRet; + } +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.app", "AnyInput " + << std::showbase << std::hex + << static_cast(nType) + << " = " << (bRet ? "true" : "false")); +#endif + return bRet; +} + +bool X11SalInstance::DoYield(bool bWait, bool bHandleAllCurrentEvents) +{ + return mpXLib->Yield( bWait, bHandleAllCurrentEvents ); +} + +OUString X11SalInstance::GetConnectionIdentifier() +{ + static const char* pDisplay = getenv( "DISPLAY" ); + return pDisplay ? OUString::createFromAscii(pDisplay) : OUString(); +} + +SalFrame *X11SalInstance::CreateFrame( SalFrame *pParent, SalFrameStyleFlags nSalFrameStyle ) +{ + SalFrame *pFrame = new X11SalFrame( pParent, nSalFrameStyle ); + + return pFrame; +} + +SalFrame* X11SalInstance::CreateChildFrame( SystemParentData* pParentData, SalFrameStyleFlags nStyle ) +{ + SalFrame* pFrame = new X11SalFrame( nullptr, nStyle, pParentData ); + + return pFrame; +} + +void X11SalInstance::DestroyFrame( SalFrame* pFrame ) +{ + delete pFrame; +} + +void X11SalInstance::AfterAppInit() +{ + assert( mpXLib->GetDisplay() ); + assert( mpXLib->GetInputMethod() ); + + SalX11Display *pSalDisplay = CreateDisplay(); + mpXLib->GetInputMethod()->CreateMethod( mpXLib->GetDisplay() ); + pSalDisplay->SetupInput(); +} + +void X11SalInstance::AddToRecentDocumentList(const OUString&, const OUString&, const OUString&) {} + +void X11SalInstance::PostPrintersChanged() +{ + SalDisplay* pDisp = vcl_sal::getSalDisplay(GetGenericUnixSalData()); + for (auto pSalFrame : pDisp->getFrames() ) + pDisp->PostEvent( pSalFrame, nullptr, SalEvent::PrinterChanged ); +} + +std::unique_ptr X11SalInstance::CreatePrintGraphics() +{ + return std::make_unique(); +} + +std::shared_ptr X11SalInstance::CreateSalBitmap() +{ +#if HAVE_FEATURE_SKIA + if (SkiaHelper::isVCLSkiaEnabled()) + return std::make_shared(); +#endif + return std::make_shared(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/app/saltimer.cxx b/vcl/unx/generic/app/saltimer.cxx new file mode 100644 index 0000000000..dc7a61dfe0 --- /dev/null +++ b/vcl/unx/generic/app/saltimer.cxx @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include +#include +#include + +void SalXLib::StopTimer() +{ + m_aTimeout.tv_sec = 0; + m_aTimeout.tv_usec = 0; + m_nTimeoutMS = 0; +} + +void SalXLib::StartTimer( sal_uInt64 nMS ) +{ + timeval Timeout (m_aTimeout); // previous timeout. + gettimeofday (&m_aTimeout, nullptr); + + m_nTimeoutMS = nMS; + m_aTimeout += m_nTimeoutMS; + + if ((Timeout > m_aTimeout) || (Timeout.tv_sec == 0)) + { + // Wakeup from previous timeout (or stopped timer). + Wakeup(); + } +} + +SalTimer* X11SalInstance::CreateSalTimer() +{ + return new X11SalTimer( mpXLib ); +} + +X11SalTimer::~X11SalTimer() +{ +} + +void X11SalTimer::Stop() +{ + mpXLib->StopTimer(); +} + +void X11SalTimer::Start( sal_uInt64 nMS ) +{ + mpXLib->StartTimer( nMS ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/app/sm.cxx b/vcl/unx/generic/app/sm.cxx new file mode 100644 index 0000000000..071ac32fdb --- /dev/null +++ b/vcl/unx/generic/app/sm.cxx @@ -0,0 +1,857 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include + +namespace { + +class IceSalSession : public SalSession +{ +public: + IceSalSession() {} + +private: + virtual ~IceSalSession() override {} + + virtual void queryInteraction() override; + virtual void interactionDone() override; + virtual void saveDone() override; + virtual bool cancelShutdown() override; +}; + +} + +std::unique_ptr X11SalInstance::CreateSalSession() +{ + SAL_INFO("vcl.sm", "X11SalInstance::CreateSalSession"); + + std::unique_ptr p(new IceSalSession); + SessionManagerClient::open(p.get()); + return p; +} + +void IceSalSession::queryInteraction() +{ + SAL_INFO("vcl.sm", "IceSalSession::queryInteraction"); + + if( ! SessionManagerClient::queryInteraction() ) + { + SAL_INFO("vcl.sm.debug", " call SalSessionInteractionEvent"); + SalSessionInteractionEvent aEvent( false ); + CallCallback( &aEvent ); + } +} + +void IceSalSession::interactionDone() +{ + SAL_INFO("vcl.sm", "IceSalSession::interactionDone"); + + SessionManagerClient::interactionDone( false ); +} + +void IceSalSession::saveDone() +{ + SAL_INFO("vcl.sm", "IceSalSession::saveDone"); + + SessionManagerClient::saveDone(); +} + +bool IceSalSession::cancelShutdown() +{ + SAL_INFO("vcl.sm", "IceSalSession::cancelShutdown"); + + SessionManagerClient::interactionDone( true ); + return false; +} + +extern "C" { + +static void ICEWatchProc( + IceConn ice_conn, IcePointer client_data, Bool opening, + IcePointer * watch_data); + +static void ICEConnectionWorker(void * data); + +} + +class ICEConnectionObserver +{ + friend void ICEWatchProc(IceConn, IcePointer, Bool, IcePointer *); + + friend void ICEConnectionWorker(void *); + + struct pollfd* m_pFilehandles; + int m_nConnections; + IceConn* m_pConnections; + int m_nWakeupFiles[2]; + oslThread m_ICEThread; + IceIOErrorHandler m_origIOErrorHandler; + IceErrorHandler m_origErrorHandler; + + void wakeup(); + +public: + osl::Mutex m_ICEMutex; + + ICEConnectionObserver() + : m_pFilehandles(nullptr) + , m_nConnections(0) + , m_pConnections(nullptr) + , m_ICEThread(nullptr) + , m_origIOErrorHandler(nullptr) + , m_origErrorHandler(nullptr) + { + SAL_INFO("vcl.sm", "ICEConnectionObserver::ICEConnectionObserver"); + + m_nWakeupFiles[0] = m_nWakeupFiles[1] = 0; + } + + void activate(); + void deactivate(); + void terminate(oslThread iceThread); +}; + +SalSession * SessionManagerClient::m_pSession = nullptr; +std::unique_ptr< ICEConnectionObserver > +SessionManagerClient::m_xICEConnectionObserver; +SmcConn SessionManagerClient::m_pSmcConnection = nullptr; +OString SessionManagerClient::m_aClientID = ""_ostr; +OString SessionManagerClient::m_aTimeID = ""_ostr; +OString SessionManagerClient::m_aClientTimeID = ""_ostr; +bool SessionManagerClient::m_bDocSaveDone = false; // HACK + +extern "C" { + +static void IgnoreIceErrors( + SAL_UNUSED_PARAMETER IceConn, SAL_UNUSED_PARAMETER Bool, + SAL_UNUSED_PARAMETER int, SAL_UNUSED_PARAMETER unsigned long, + SAL_UNUSED_PARAMETER int, SAL_UNUSED_PARAMETER int, + SAL_UNUSED_PARAMETER IcePointer) +{} + +static void IgnoreIceIOErrors(SAL_UNUSED_PARAMETER IceConn) {} + +} + +static SmProp* pSmProps = nullptr; +static SmProp** ppSmProps = nullptr; +static char ** ppSmDel = nullptr; + +static int nSmProps = 0; +static int nSmDel = 0; +static unsigned char *pSmRestartHint = nullptr; + + +enum { eCloneCommand, eProgram, eRestartCommand, eUserId, eRestartStyleHint }; +enum { eDiscardCommand }; + + +static void BuildSmPropertyList() +{ + SAL_INFO("vcl.sm", "BuildSmPropertyList"); + + if( ! pSmProps ) + { + nSmProps = 5; + nSmDel = 1; + pSmProps = new SmProp[ nSmProps ]; + ppSmProps = new SmProp*[ nSmProps ]; + ppSmDel = new char*[ nSmDel ]; + } + + OString aExec(OUStringToOString(SessionManagerClient::getExecName(), osl_getThreadTextEncoding())); + + pSmProps[ eCloneCommand ].name = const_cast(SmCloneCommand); + pSmProps[ eCloneCommand ].type = const_cast(SmLISTofARRAY8); + pSmProps[ eCloneCommand ].num_vals = 1; + pSmProps[ eCloneCommand ].vals = new SmPropValue; + pSmProps[ eCloneCommand ].vals->length = aExec.getLength()+1; + pSmProps[ eCloneCommand ].vals->value = strdup( aExec.getStr() ); + + pSmProps[ eProgram ].name = const_cast(SmProgram); + pSmProps[ eProgram ].type = const_cast(SmARRAY8); + pSmProps[ eProgram ].num_vals = 1; + pSmProps[ eProgram ].vals = new SmPropValue; + pSmProps[ eProgram ].vals->length = aExec.getLength()+1; + pSmProps[ eProgram ].vals->value = strdup( aExec.getStr() ); + + pSmProps[ eRestartCommand ].name = const_cast(SmRestartCommand); + pSmProps[ eRestartCommand ].type = const_cast(SmLISTofARRAY8); + pSmProps[ eRestartCommand ].num_vals = 3; + pSmProps[ eRestartCommand ].vals = new SmPropValue[3]; + pSmProps[ eRestartCommand ].vals[0].length = aExec.getLength()+1; + pSmProps[ eRestartCommand ].vals[0].value = strdup( aExec.getStr() ); + OString aRestartOption = "--session=" + SessionManagerClient::getSessionID(); + pSmProps[ eRestartCommand ].vals[1].length = aRestartOption.getLength()+1; + pSmProps[ eRestartCommand ].vals[1].value = strdup(aRestartOption.getStr()); + OString aRestartOptionNoLogo("--nologo"_ostr); + pSmProps[ eRestartCommand ].vals[2].length = aRestartOptionNoLogo.getLength()+1; + pSmProps[ eRestartCommand ].vals[2].value = strdup(aRestartOptionNoLogo.getStr()); + + OUString aUserName; + OString aUser; + oslSecurity aSec = osl_getCurrentSecurity(); + if( aSec ) + { + osl_getUserName( aSec, &aUserName.pData ); + aUser = OUStringToOString( aUserName, osl_getThreadTextEncoding() ); + osl_freeSecurityHandle( aSec ); + } + + pSmProps[ eUserId ].name = const_cast(SmUserID); + pSmProps[ eUserId ].type = const_cast(SmARRAY8); + pSmProps[ eUserId ].num_vals = 1; + pSmProps[ eUserId ].vals = new SmPropValue; + pSmProps[ eUserId ].vals->value = strdup( aUser.getStr() ); + pSmProps[ eUserId ].vals->length = rtl_str_getLength( static_cast(pSmProps[ 3 ].vals->value) )+1; + + pSmProps[ eRestartStyleHint ].name = const_cast(SmRestartStyleHint); + pSmProps[ eRestartStyleHint ].type = const_cast(SmCARD8); + pSmProps[ eRestartStyleHint ].num_vals = 1; + pSmProps[ eRestartStyleHint ].vals = new SmPropValue; + pSmProps[ eRestartStyleHint ].vals->value = malloc(1); + pSmRestartHint = static_cast(pSmProps[ 4 ].vals->value); + *pSmRestartHint = SmRestartIfRunning; + pSmProps[ eRestartStyleHint ].vals->length = 1; + + for( int i = 0; i < nSmProps; i++ ) + ppSmProps[ i ] = &pSmProps[i]; + + ppSmDel[eDiscardCommand] = const_cast(SmDiscardCommand); +} + +bool SessionManagerClient::checkDocumentsSaved() +{ + SAL_INFO("vcl.sm", "SessionManagerClient::checkDocumentsSaved"); + + SAL_INFO("vcl.sm.debug", " m_bcheckDocumentsSaved = " << (m_bDocSaveDone ? "true" : "false" )); + return m_bDocSaveDone; +} + +IMPL_STATIC_LINK( SessionManagerClient, SaveYourselfHdl, void*, pStateVal, void ) +{ + SAL_INFO("vcl.sm", "SessionManagerClient, SaveYourselfHdl"); + + // Decode argument smuggled in as void*: + sal_uIntPtr nStateVal = reinterpret_cast< sal_uIntPtr >(pStateVal); + bool shutdown = nStateVal != 0; + + static bool bFirstShutdown=true; + + SAL_INFO("vcl.sm.debug", " shutdown = " << (shutdown ? "true" : "false" ) << + ", bFirstShutdown = " << (bFirstShutdown ? "true" : "false" )); + if (shutdown && bFirstShutdown) //first shutdown request + { + bFirstShutdown = false; + /* + If we have no actual frames open, e.g. we launched a quickstarter, + and then shutdown all our frames leaving just a quickstarter running, + then we don't want to launch an empty toplevel frame on the next + start. (The job of scheduling the restart of the quick-starter is a + task of the quick-starter) + */ + *pSmRestartHint = SmRestartNever; + for (auto pSalFrame : vcl_sal::getSalDisplay(GetGenericUnixSalData())->getFrames() ) + { + vcl::Window *pWindow = pSalFrame->GetWindow(); + if (pWindow && pWindow->IsVisible()) + { + *pSmRestartHint = SmRestartIfRunning; + SAL_INFO("vcl.sm.debug", " pSmRestartHint = SmRestartIfRunning"); + break; + } + } + } + + if( m_pSession ) + { + SalSessionSaveRequestEvent aEvent( shutdown ); + m_pSession->CallCallback( &aEvent ); + } + else + saveDone(); +} + +IMPL_STATIC_LINK_NOARG( SessionManagerClient, InteractionHdl, void*, void ) +{ + SAL_INFO("vcl.sm", "SessionManagerClient, InteractionHdl"); + + if( m_pSession ) + { + SalSessionInteractionEvent aEvent( true ); + m_pSession->CallCallback( &aEvent ); + } +} + +IMPL_STATIC_LINK_NOARG( SessionManagerClient, ShutDownCancelHdl, void*, void ) +{ + SAL_INFO("vcl.sm", "SessionManagerClient, ShutDownCancelHdl"); + + if( m_pSession ) + { + SalSessionShutdownCancelEvent aEvent; + m_pSession->CallCallback( &aEvent ); + } +} + +void SessionManagerClient::SaveYourselfProc( + SmcConn, + SmPointer, + int save_type, + Bool shutdown, + int interact_style, + Bool + ) +{ + SAL_INFO("vcl.sm", "SessionManagerClient::SaveYourselfProc"); + + TimeValue now; + osl_getSystemTime(&now); + + SAL_INFO("vcl.sm", " save_type = " << ((save_type == SmSaveLocal ) ? "local" : + (save_type == SmSaveGlobal) ? "global" : "both") << + ", shutdown = " << (shutdown ? "true" : "false" ) << + ", interact_style = " << ((interact_style == SmInteractStyleNone) ? "SmInteractStyleNone" : + (interact_style == SmInteractStyleErrors) ? "SmInteractStyleErrors" : + "SmInteractStyleAny")); + char num[100]; + snprintf(num, sizeof(num), "_%" SAL_PRIuUINT32 "_%" SAL_PRIuUINT32, now.Seconds, (now.Nanosec / 1001)); + m_aTimeID = OString(num); + + BuildSmPropertyList(); + + SmcSetProperties( m_pSmcConnection, 1, &ppSmProps[ eProgram ] ); + SmcSetProperties( m_pSmcConnection, 1, &ppSmProps[ eUserId ] ); + + + m_bDocSaveDone = false; + /* #i49875# some session managers send a "die" message if the + * saveDone does not come early enough for their convenience + * this can occasionally happen on startup, especially the first + * startup. So shortcut the "not shutting down" case since the + * upper layers are currently not interested in that event anyway. + */ + if( ! shutdown ) + { + SessionManagerClient::saveDone(); + return; + } + // Smuggle argument in as void*: + sal_uIntPtr nStateVal = shutdown; + Application::PostUserEvent( LINK( nullptr, SessionManagerClient, SaveYourselfHdl ), reinterpret_cast< void * >(nStateVal) ); +} + +IMPL_STATIC_LINK_NOARG( SessionManagerClient, ShutDownHdl, void*, void ) +{ + SAL_INFO("vcl.sm", "SessionManagerClient, ShutDownHdl"); + + if( m_pSession ) + { + SalSessionQuitEvent aEvent; + m_pSession->CallCallback( &aEvent ); + } + + SalFrame *pAnyFrame = vcl_sal::getSalDisplay(GetGenericUnixSalData())->anyFrame(); + SAL_INFO("vcl.sm.debug", " rFrames.empty() = " << (pAnyFrame ? "true" : "false")); + if( pAnyFrame ) + pAnyFrame->CallCallback( SalEvent::Shutdown, nullptr ); +} + +void SessionManagerClient::DieProc( + SmcConn connection, + SmPointer + ) +{ + SAL_INFO("vcl.sm", "SessionManagerClient::DieProc"); + + if( connection == m_pSmcConnection ) + { + SAL_INFO("vcl.sm.debug", " connection == m_pSmcConnection" ); + Application::PostUserEvent( LINK( nullptr, SessionManagerClient, ShutDownHdl ) ); + } +} + +void SessionManagerClient::SaveCompleteProc( + SmcConn, + SmPointer + ) +{ + SAL_INFO("vcl.sm", "SessionManagerClient::SaveCompleteProc"); +} + +void SessionManagerClient::ShutdownCanceledProc( + SmcConn connection, + SmPointer ) +{ + SAL_INFO("vcl.sm", "SessionManagerClient::ShutdownCanceledProc" ); + + SAL_INFO("vcl.sm.debug", " connection == m_pSmcConnection = " << (( connection == m_pSmcConnection ) ? "true" : "false")); + if( connection == m_pSmcConnection ) + Application::PostUserEvent( LINK( nullptr, SessionManagerClient, ShutDownCancelHdl ) ); +} + +void SessionManagerClient::InteractProc( + SmcConn connection, + SmPointer ) +{ + SAL_INFO("vcl.sm", "SessionManagerClient::InteractProc" ); + + SAL_INFO("vcl.sm.debug", " connection == m_pSmcConnection = " << (( connection == m_pSmcConnection ) ? "true" : "false")); + if( connection == m_pSmcConnection ) + Application::PostUserEvent( LINK( nullptr, SessionManagerClient, InteractionHdl ) ); +} + +void SessionManagerClient::saveDone() +{ + SAL_INFO("vcl.sm", "SessionManagerClient::saveDone"); + + if( !m_pSmcConnection ) + return; + + assert(m_xICEConnectionObserver); + osl::MutexGuard g(m_xICEConnectionObserver->m_ICEMutex); + //SmcSetProperties( m_pSmcConnection, 1, &ppSmProps[ eCloneCommand ] ); + // this message-handling is now equal to kate and plasma desktop + SmcSetProperties( m_pSmcConnection, 1, &ppSmProps[ eRestartCommand ] ); + SmcDeleteProperties( m_pSmcConnection, 1, &ppSmDel[ eDiscardCommand ] ); + SmcSetProperties( m_pSmcConnection, 1, &ppSmProps[ eRestartStyleHint ] ); + + SmcSaveYourselfDone( m_pSmcConnection, True ); + SAL_INFO("vcl.sm.debug", " sent SmRestartHint = " << (*pSmRestartHint) ); + m_bDocSaveDone = true; +} + +void SessionManagerClient::open(SalSession * pSession) +{ + SAL_INFO("vcl.sm", "SessionManagerClient::open"); + + assert(!m_pSession && !m_xICEConnectionObserver && !m_pSmcConnection); + // must only be called once + m_pSession = pSession; + // This is the way Xt does it, so we can too: + if( getenv( "SESSION_MANAGER" ) ) + { + SAL_INFO("vcl.sm.debug", " getenv( SESSION_MANAGER ) = true"); + m_xICEConnectionObserver.reset(new ICEConnectionObserver); + m_xICEConnectionObserver->activate(); + + { + osl::MutexGuard g(m_xICEConnectionObserver->m_ICEMutex); + + static SmcCallbacks aCallbacks; // does this need to be static? + aCallbacks.save_yourself.callback = SaveYourselfProc; + aCallbacks.save_yourself.client_data = nullptr; + aCallbacks.die.callback = DieProc; + aCallbacks.die.client_data = nullptr; + aCallbacks.save_complete.callback = SaveCompleteProc; + aCallbacks.save_complete.client_data = nullptr; + aCallbacks.shutdown_cancelled.callback = ShutdownCanceledProc; + aCallbacks.shutdown_cancelled.client_data = nullptr; + OString aPrevId(getPreviousSessionID()); + char* pClientID = nullptr; + char aErrBuf[1024]; + m_pSmcConnection = SmcOpenConnection( nullptr, + nullptr, + SmProtoMajor, + SmProtoMinor, + SmcSaveYourselfProcMask | + SmcDieProcMask | + SmcSaveCompleteProcMask | + SmcShutdownCancelledProcMask , + &aCallbacks, + aPrevId.isEmpty() ? nullptr : const_cast(aPrevId.getStr()), + &pClientID, + sizeof( aErrBuf ), + aErrBuf ); + if( !m_pSmcConnection ) + SAL_INFO("vcl.sm.debug", " SmcOpenConnection failed: " << aErrBuf); + else + SAL_INFO("vcl.sm.debug", " SmcOpenConnection succeeded, client ID is " << pClientID ); + m_aClientID = OString(pClientID); + free( pClientID ); + pClientID = nullptr; + } + + SalDisplay* pDisp = vcl_sal::getSalDisplay(GetGenericUnixSalData()); + if( pDisp->GetDrawable(pDisp->GetDefaultXScreen()) && !m_aClientID.isEmpty() ) + { + SAL_INFO("vcl.sm.debug", " SmcOpenConnection open: pDisp->GetDrawable = true"); + XChangeProperty( pDisp->GetDisplay(), + pDisp->GetDrawable( pDisp->GetDefaultXScreen() ), + XInternAtom( pDisp->GetDisplay(), "SM_CLIENT_ID", False ), + XA_STRING, + 8, + PropModeReplace, + reinterpret_cast(m_aClientID.getStr()), + m_aClientID.getLength() + ); + } + } + else + { + SAL_INFO("vcl.sm.debug", " getenv( SESSION_MANAGER ) = false"); + } +} + +const OString& SessionManagerClient::getSessionID() +{ + SAL_INFO("vcl.sm", "SessionManagerClient::getSessionID"); + + m_aClientTimeID = m_aClientID + m_aTimeID; + + SAL_INFO("vcl.sm", " SessionID = " << m_aClientTimeID); + + return m_aClientTimeID; +} + +void SessionManagerClient::close() +{ + SAL_INFO("vcl.sm", "SessionManagerClient::close"); + + if( !m_pSmcConnection ) + return; + + SAL_INFO("vcl.sm.debug", " attempting SmcCloseConnection"); + assert(m_xICEConnectionObserver); + { + osl::MutexGuard g(m_xICEConnectionObserver->m_ICEMutex); + SmcCloseConnection( m_pSmcConnection, 0, nullptr ); + SAL_INFO("vcl.sm", " SmcCloseConnection closed"); + } + m_xICEConnectionObserver->deactivate(); + m_xICEConnectionObserver.reset(); + m_pSmcConnection = nullptr; +} + +bool SessionManagerClient::queryInteraction() +{ + SAL_INFO("vcl.sm", "SessionManagerClient::queryInteraction"); + + bool bRet = false; + if( m_pSmcConnection ) + { + assert(m_xICEConnectionObserver); + osl::MutexGuard g(m_xICEConnectionObserver->m_ICEMutex); + SAL_INFO("vcl.sm.debug", " SmcInteractRequest" ); + if( SmcInteractRequest( m_pSmcConnection, SmDialogNormal, InteractProc, nullptr ) ) + bRet = true; + } + return bRet; +} + +void SessionManagerClient::interactionDone( bool bCancelShutdown ) +{ + SAL_INFO("vcl.sm", "SessionManagerClient::interactionDone"); + + if( m_pSmcConnection ) + { + assert(m_xICEConnectionObserver); + osl::MutexGuard g(m_xICEConnectionObserver->m_ICEMutex); + SAL_INFO("vcl.sm.debug", " SmcInteractDone = " << (bCancelShutdown ? "true" : "false") ); + SmcInteractDone( m_pSmcConnection, bCancelShutdown ? True : False ); + } +} + +OUString SessionManagerClient::getExecName() +{ + SAL_INFO("vcl.sm", "SessionManagerClient::getExecName"); + + OUString aExec, aSysExec; + osl_getExecutableFile( &aExec.pData ); + osl_getSystemPathFromFileURL( aExec.pData, &aSysExec.pData ); + + if( aSysExec.endsWith(".bin") ) + aSysExec = aSysExec.copy( 0, aSysExec.getLength() - RTL_CONSTASCII_LENGTH(".bin") ); + + SAL_INFO("vcl.sm.debug", " aSysExec = " << aSysExec); + return aSysExec; +} + +OString SessionManagerClient::getPreviousSessionID() +{ + SAL_INFO("vcl.sm", "SessionManagerClient::getPreviousSessionID"); + + OString aPrevId; + + sal_uInt32 n = rtl_getAppCommandArgCount(); + for (sal_uInt32 i = 0; i != n; ++i) + { + OUString aArg; + rtl_getAppCommandArg( i, &aArg.pData ); + if(aArg.match("--session=")) + { + aPrevId = OUStringToOString( + aArg.subView(RTL_CONSTASCII_LENGTH("--session=")), + osl_getThreadTextEncoding()); + break; + } + } + + SAL_INFO("vcl.sm.debug", " previous ID = " << aPrevId); + return aPrevId; +} + +void ICEConnectionObserver::activate() +{ + SAL_INFO("vcl.sm", "ICEConnectionObserver::activate"); + + /* + * Default handlers call exit, we don't care that strongly if something + * happens to fail + */ + m_origIOErrorHandler = IceSetIOErrorHandler( IgnoreIceIOErrors ); + m_origErrorHandler = IceSetErrorHandler( IgnoreIceErrors ); + IceAddConnectionWatch( ICEWatchProc, this ); +} + +void ICEConnectionObserver::deactivate() +{ + SAL_INFO("vcl.sm", "ICEConnectionObserver::deactivate"); + + oslThread t; + { + osl::MutexGuard g(m_ICEMutex); + IceRemoveConnectionWatch( ICEWatchProc, this ); + IceSetErrorHandler( m_origErrorHandler ); + IceSetIOErrorHandler( m_origIOErrorHandler ); + m_nConnections = 0; + t = m_ICEThread; + m_ICEThread = nullptr; + } + if (t) + { + SAL_INFO("vcl.sm.debug", " terminate"); + terminate(t); + } +} + +void ICEConnectionObserver::wakeup() +{ + SAL_INFO("vcl.sm", "ICEConnectionObserver::wakeup"); + + char cChar = 'w'; + OSL_VERIFY(write(m_nWakeupFiles[1], &cChar, 1) == 1); +} + +void ICEConnectionObserver::terminate(oslThread iceThread) +{ + SAL_INFO("vcl.sm", "ICEConnectionObserver::terminate"); + + osl_terminateThread(iceThread); + wakeup(); + osl_joinWithThread(iceThread); + osl_destroyThread(iceThread); + close(m_nWakeupFiles[1]); + close(m_nWakeupFiles[0]); +} + +void ICEConnectionWorker(void * data) +{ + SAL_INFO("vcl.sm", "ICEConnectionWorker"); + + osl::Thread::setName("ICEConnectionWorker"); + ICEConnectionObserver * pThis = static_cast< ICEConnectionObserver * >( + data); + for (;;) + { + oslThread t; + { + osl::MutexGuard g(pThis->m_ICEMutex); + if (pThis->m_ICEThread == nullptr || pThis->m_nConnections == 0) + { + break; + } + t = pThis->m_ICEThread; + } + if (!osl_scheduleThread(t)) + { + break; + } + + int nConnectionsBefore; + struct pollfd* pLocalFD; + { + osl::MutexGuard g(pThis->m_ICEMutex); + nConnectionsBefore = pThis->m_nConnections; + int nBytes = sizeof( struct pollfd )*(nConnectionsBefore+1); + pLocalFD = static_cast(std::malloc( nBytes )); + memcpy( pLocalFD, pThis->m_pFilehandles, nBytes ); + } + + int nRet = poll( pLocalFD,nConnectionsBefore+1,-1 ); + bool bWakeup = (pLocalFD[0].revents & POLLIN); + std::free( pLocalFD ); + + if( nRet < 1 ) + continue; + + // clear wakeup pipe + if( bWakeup ) + { + char buf[4]; + while( read( pThis->m_nWakeupFiles[0], buf, sizeof( buf ) ) > 0 ) + ; + SAL_INFO("vcl.sm.debug", " file handles active in wakeup: " << nRet); + if( nRet == 1 ) + continue; + } + + // check fd's after we obtained the lock + osl::MutexGuard g(pThis->m_ICEMutex); + if( pThis->m_nConnections > 0 && pThis->m_nConnections == nConnectionsBefore ) + { + nRet = poll( pThis->m_pFilehandles+1, pThis->m_nConnections, 0 ); + if( nRet > 0 ) + { + SAL_INFO("vcl.sm.debug", " IceProcessMessages"); + Bool bReply; + for( int i = 0; i < pThis->m_nConnections; i++ ) + if( pThis->m_pFilehandles[i+1].revents & POLLIN ) + IceProcessMessages( pThis->m_pConnections[i], nullptr, &bReply ); + } + } + } + + SAL_INFO("vcl.sm.debug", " shutting down ICE dispatch thread"); +} + +void ICEWatchProc( + IceConn ice_conn, IcePointer client_data, Bool opening, + SAL_UNUSED_PARAMETER IcePointer *) +{ + SAL_INFO("vcl.sm", "ICEWatchProc"); + + // Note: This is a callback function for ICE; this implicitly means that a + // call into ICE lib is calling this, so the m_ICEMutex MUST already be + // locked by the caller. + ICEConnectionObserver * pThis = static_cast< ICEConnectionObserver * >( + client_data); + if( opening ) + { + SAL_INFO("vcl.sm.debug", " opening"); + int fd = IceConnectionNumber( ice_conn ); + pThis->m_nConnections++; + pThis->m_pConnections = static_cast(std::realloc( pThis->m_pConnections, sizeof( IceConn )*pThis->m_nConnections )); + pThis->m_pFilehandles = static_cast(std::realloc( pThis->m_pFilehandles, sizeof( struct pollfd )*(pThis->m_nConnections+1) )); + pThis->m_pConnections[ pThis->m_nConnections-1 ] = ice_conn; + pThis->m_pFilehandles[ pThis->m_nConnections ].fd = fd; + pThis->m_pFilehandles[ pThis->m_nConnections ].events = POLLIN; + if( pThis->m_nConnections == 1 ) + { + SAL_INFO("vcl.sm.debug", " First connection"); + if (!pipe(pThis->m_nWakeupFiles)) + { + int flags; + pThis->m_pFilehandles[0].fd = pThis->m_nWakeupFiles[0]; + pThis->m_pFilehandles[0].events = POLLIN; + // set close-on-exec and nonblock descriptor flag. + if ((flags = fcntl(pThis->m_nWakeupFiles[0], F_GETFD)) != -1) + { + flags |= FD_CLOEXEC; + (void)fcntl(pThis->m_nWakeupFiles[0], F_SETFD, flags); + } + if ((flags = fcntl(pThis->m_nWakeupFiles[0], F_GETFL)) != -1) + { + flags |= O_NONBLOCK; + (void)fcntl(pThis->m_nWakeupFiles[0], F_SETFL, flags); + } + // set close-on-exec and nonblock descriptor flag. + if ((flags = fcntl(pThis->m_nWakeupFiles[1], F_GETFD)) != -1) + { + flags |= FD_CLOEXEC; + (void)fcntl(pThis->m_nWakeupFiles[1], F_SETFD, flags); + } + if ((flags = fcntl(pThis->m_nWakeupFiles[1], F_GETFL)) != -1) + { + flags |= O_NONBLOCK; + (void)fcntl(pThis->m_nWakeupFiles[1], F_SETFL, flags); + } + pThis->m_ICEThread = osl_createThread( + ICEConnectionWorker, pThis); + } + } + } + else // closing + { + SAL_INFO("vcl.sm.debug", " closing"); + for( int i = 0; i < pThis->m_nConnections; i++ ) + { + if( pThis->m_pConnections[i] == ice_conn ) + { + if( i < pThis->m_nConnections-1 ) + { + memmove( pThis->m_pConnections+i, pThis->m_pConnections+i+1, sizeof( IceConn )*(pThis->m_nConnections-i-1) ); + memmove( pThis->m_pFilehandles+i+1, pThis->m_pFilehandles+i+2, sizeof( struct pollfd )*(pThis->m_nConnections-i-1) ); + } + pThis->m_nConnections--; + pThis->m_pConnections = static_cast(std::realloc( pThis->m_pConnections, sizeof( IceConn )*pThis->m_nConnections )); + pThis->m_pFilehandles = static_cast(std::realloc( pThis->m_pFilehandles, sizeof( struct pollfd )*(pThis->m_nConnections+1) )); + break; + } + } + if( pThis->m_nConnections == 0 && pThis->m_ICEThread ) + { + SAL_INFO("vcl.sm.debug", " terminating ICEThread"); + oslThread t = pThis->m_ICEThread; + pThis->m_ICEThread = nullptr; + + // must release the mutex here + pThis->m_ICEMutex.release(); + + pThis->terminate(t); + + // acquire the mutex again, because the caller does not expect + // it to be released when calling into SM + pThis->m_ICEMutex.acquire(); + } + } + + SAL_INFO( "vcl.sm.debug", " ICE connection on " << IceConnectionNumber( ice_conn ) ); + SAL_INFO( "vcl.sm.debug", " Display connection is " << ConnectionNumber( vcl_sal::getSalDisplay(GetGenericUnixSalData())->GetDisplay() ) ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/app/wmadaptor.cxx b/vcl/unx/generic/app/wmadaptor.cxx new file mode 100644 index 0000000000..240517d7aa --- /dev/null +++ b/vcl/unx/generic/app/wmadaptor.cxx @@ -0,0 +1,2196 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace vcl_sal { + +class NetWMAdaptor : public WMAdaptor +{ + void setNetWMState( X11SalFrame* pFrame ) const; + void initAtoms(); + virtual bool isValid() const override; +public: + explicit NetWMAdaptor( SalDisplay* ); + + virtual void setWMName( X11SalFrame* pFrame, const OUString& rWMName ) const override; + virtual void maximizeFrame( X11SalFrame* pFrame, bool bHorizontal = true, bool bVertical = true ) const override; + virtual void setFrameTypeAndDecoration( X11SalFrame* pFrame, WMWindowType eType, int nDecorationFlags, X11SalFrame* pTransientFrame ) const override; + virtual void enableAlwaysOnTop( X11SalFrame* pFrame, bool bEnable ) const override; + virtual int handlePropertyNotify( X11SalFrame* pFrame, XPropertyEvent* pEvent ) const override; + virtual void showFullScreen( X11SalFrame* pFrame, bool bFullScreen ) const override; + virtual void frameIsMapping( X11SalFrame* pFrame ) const override; + virtual void setUserTime( X11SalFrame* i_pFrame, tools::Long i_nUserTime ) const override; +}; + +class GnomeWMAdaptor : public WMAdaptor +{ + bool m_bValid; + + void setGnomeWMState( X11SalFrame* pFrame ) const; + void initAtoms(); + virtual bool isValid() const override; +public: + explicit GnomeWMAdaptor( SalDisplay * ); + + virtual void maximizeFrame( X11SalFrame* pFrame, bool bHorizontal = true, bool bVertical = true ) const override; + virtual void enableAlwaysOnTop( X11SalFrame* pFrame, bool bEnable ) const override; + virtual int handlePropertyNotify( X11SalFrame* pFrame, XPropertyEvent* pEvent ) const override; +}; + +} + +using namespace vcl_sal; + +namespace { + +struct WMAdaptorProtocol +{ + const char* pProtocol; + int nProtocol; +}; + +} + +/* + * table must be sorted ascending in strings + * since it is use with bsearch + */ +const WMAdaptorProtocol aProtocolTab[] = +{ + { "_KDE_NET_WM_WINDOW_TYPE_OVERRIDE", WMAdaptor::KDE_NET_WM_WINDOW_TYPE_OVERRIDE }, + { "_NET_ACTIVE_WINDOW", WMAdaptor::NET_ACTIVE_WINDOW }, + { "_NET_CURRENT_DESKTOP", WMAdaptor::NET_CURRENT_DESKTOP }, + { "_NET_NUMBER_OF_DESKTOPS", WMAdaptor::NET_NUMBER_OF_DESKTOPS }, + { "_NET_WM_DESKTOP", WMAdaptor::NET_WM_DESKTOP }, + { "_NET_WM_ICON", WMAdaptor::NET_WM_ICON }, + { "_NET_WM_ICON_NAME", WMAdaptor::NET_WM_ICON_NAME }, + { "_NET_WM_PING", WMAdaptor::NET_WM_PING }, + { "_NET_WM_STATE", WMAdaptor::NET_WM_STATE }, + { "_NET_WM_STATE_ABOVE", WMAdaptor::NET_WM_STATE_STAYS_ON_TOP }, + { "_NET_WM_STATE_FULLSCREEN", WMAdaptor::NET_WM_STATE_FULLSCREEN }, + { "_NET_WM_STATE_MAXIMIZED_HORIZ", WMAdaptor::NET_WM_STATE_MAXIMIZED_HORZ }, // common bug in e.g. older kwin and sawfish implementations + { "_NET_WM_STATE_MAXIMIZED_HORZ", WMAdaptor::NET_WM_STATE_MAXIMIZED_HORZ }, + { "_NET_WM_STATE_MAXIMIZED_VERT", WMAdaptor::NET_WM_STATE_MAXIMIZED_VERT }, + { "_NET_WM_STATE_MODAL", WMAdaptor::NET_WM_STATE_MODAL }, + { "_NET_WM_STATE_SKIP_PAGER", WMAdaptor::NET_WM_STATE_SKIP_PAGER }, + { "_NET_WM_STATE_SKIP_TASKBAR", WMAdaptor::NET_WM_STATE_SKIP_TASKBAR }, + { "_NET_WM_STATE_STAYS_ON_TOP", WMAdaptor::NET_WM_STATE_STAYS_ON_TOP }, + { "_NET_WM_STATE_STICKY", WMAdaptor::NET_WM_STATE_STICKY }, + { "_NET_WM_STRUT", WMAdaptor::NET_WM_STRUT }, + { "_NET_WM_STRUT_PARTIAL", WMAdaptor::NET_WM_STRUT_PARTIAL }, + { "_NET_WM_WINDOW_TYPE", WMAdaptor::NET_WM_WINDOW_TYPE }, + { "_NET_WM_WINDOW_TYPE_DESKTOP", WMAdaptor::NET_WM_WINDOW_TYPE_DESKTOP }, + { "_NET_WM_WINDOW_TYPE_DIALOG", WMAdaptor::NET_WM_WINDOW_TYPE_DIALOG }, + { "_NET_WM_WINDOW_TYPE_DOCK", WMAdaptor::NET_WM_WINDOW_TYPE_DOCK }, + { "_NET_WM_WINDOW_TYPE_MENU", WMAdaptor::NET_WM_WINDOW_TYPE_MENU }, + { "_NET_WM_WINDOW_TYPE_NORMAL", WMAdaptor::NET_WM_WINDOW_TYPE_NORMAL }, + { "_NET_WM_WINDOW_TYPE_SPLASH", WMAdaptor::NET_WM_WINDOW_TYPE_SPLASH }, + { "_NET_WM_WINDOW_TYPE_SPLASHSCREEN", WMAdaptor::NET_WM_WINDOW_TYPE_SPLASH }, // bug in Metacity 2.4.1 + { "_NET_WM_WINDOW_TYPE_TOOLBAR", WMAdaptor::NET_WM_WINDOW_TYPE_TOOLBAR }, + { "_NET_WM_WINDOW_TYPE_UTILITY", WMAdaptor::NET_WM_WINDOW_TYPE_UTILITY }, + { "_NET_WORKAREA", WMAdaptor::NET_WORKAREA }, + { "_WIN_APP_STATE", WMAdaptor::WIN_APP_STATE }, + { "_WIN_CLIENT_LIST", WMAdaptor::WIN_CLIENT_LIST }, + { "_WIN_EXPANDED_SIZE", WMAdaptor::WIN_EXPANDED_SIZE }, + { "_WIN_HINTS", WMAdaptor::WIN_HINTS }, + { "_WIN_ICONS", WMAdaptor::WIN_ICONS }, + { "_WIN_LAYER", WMAdaptor::WIN_LAYER }, + { "_WIN_STATE", WMAdaptor::WIN_STATE }, + { "_WIN_WORKSPACE", WMAdaptor::WIN_WORKSPACE }, + { "_WIN_WORKSPACE_COUNT", WMAdaptor::WIN_WORKSPACE_COUNT } +}; + +/* + * table containing atoms to get anyway + */ + +const WMAdaptorProtocol aAtomTab[] = +{ + { "WM_STATE", WMAdaptor::WM_STATE }, + { "_MOTIF_WM_HINTS", WMAdaptor::MOTIF_WM_HINTS }, + { "WM_PROTOCOLS", WMAdaptor::WM_PROTOCOLS }, + { "WM_DELETE_WINDOW", WMAdaptor::WM_DELETE_WINDOW }, + { "WM_TAKE_FOCUS", WMAdaptor::WM_TAKE_FOCUS }, + { "WM_COMMAND", WMAdaptor::WM_COMMAND }, + { "WM_CLIENT_LEADER", WMAdaptor::WM_CLIENT_LEADER }, + { "WM_LOCALE_NAME", WMAdaptor::WM_LOCALE_NAME }, + { "WM_TRANSIENT_FOR", WMAdaptor::WM_TRANSIENT_FOR }, + { "SAL_QUITEVENT", WMAdaptor::SAL_QUITEVENT }, + { "SAL_USEREVENT", WMAdaptor::SAL_USEREVENT }, + { "SAL_EXTTEXTEVENT", WMAdaptor::SAL_EXTTEXTEVENT }, + { "SAL_GETTIMEEVENT", WMAdaptor::SAL_GETTIMEEVENT }, + { "VCL_SYSTEM_SETTINGS", WMAdaptor::VCL_SYSTEM_SETTINGS }, + { "_XSETTINGS_SETTINGS", WMAdaptor::XSETTINGS }, + { "_XEMBED", WMAdaptor::XEMBED }, + { "_XEMBED_INFO", WMAdaptor::XEMBED_INFO }, + { "_NET_WM_USER_TIME", WMAdaptor::NET_WM_USER_TIME }, + { "_NET_WM_PID", WMAdaptor::NET_WM_PID } +}; + +extern "C" { +static int compareProtocol( const void* pLeft, const void* pRight ) +{ + return strcmp( static_cast(pLeft)->pProtocol, static_cast(pRight)->pProtocol ); +} +} + +std::unique_ptr WMAdaptor::createWMAdaptor( SalDisplay* pSalDisplay ) +{ + std::unique_ptr pAdaptor; + + // try a NetWM + pAdaptor.reset(new NetWMAdaptor( pSalDisplay )); + if( ! pAdaptor->isValid() ) + { + pAdaptor.reset(); + } +#if OSL_DEBUG_LEVEL > 1 + else + SAL_INFO("vcl.app", "WM supports extended WM hints."); +#endif + + // try a GnomeWM + if( ! pAdaptor ) + { + pAdaptor.reset(new GnomeWMAdaptor( pSalDisplay )); + if( ! pAdaptor->isValid() ) + { + pAdaptor.reset(); + } +#if OSL_DEBUG_LEVEL > 1 + else + SAL_INFO("vcl.app", "WM supports GNOME WM hints."); +#endif + } + + if( ! pAdaptor ) + pAdaptor.reset(new WMAdaptor( pSalDisplay )); + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.app", "Window Manager's name is \"" + << pAdaptor->getWindowManagerName() + << "\"."); +#endif + return pAdaptor; +} + +/* + * WMAdaptor constructor + */ + +WMAdaptor::WMAdaptor( SalDisplay* pDisplay ) : + m_pSalDisplay( pDisplay ), + m_bEnableAlwaysOnTopWorks( false ), + m_bLegacyPartialFullscreen( false ), + m_nWinGravity( StaticGravity ), + m_nInitWinGravity( StaticGravity ), + m_bWMshouldSwitchWorkspace( true ), + m_bWMshouldSwitchWorkspaceInit( false ) +{ + Atom aRealType = None; + int nFormat = 8; + unsigned long nItems = 0; + unsigned long nBytesLeft = 0; + unsigned char* pProperty = nullptr; + + // default desktops + m_nDesktops = 1; + m_aWMWorkAreas = ::std::vector< AbsoluteScreenPixelRectangle > + ( 1, AbsoluteScreenPixelRectangle( AbsoluteScreenPixelPoint(), m_pSalDisplay->GetScreenSize( m_pSalDisplay->GetDefaultXScreen() ) ) ); + m_bEqualWorkAreas = true; + + memset( m_aWMAtoms, 0, sizeof( m_aWMAtoms ) ); + m_pDisplay = m_pSalDisplay->GetDisplay(); + + initAtoms(); + getNetWmName(); // try to discover e.g. Sawfish + + if( m_aWMName.isEmpty() ) + { + // check for ReflectionX wm (as it needs a workaround in Windows mode + Atom aRwmRunning = XInternAtom( m_pDisplay, "RWM_RUNNING", True ); + if( aRwmRunning != None && + XGetWindowProperty( m_pDisplay, + m_pSalDisplay->GetRootWindow( m_pSalDisplay->GetDefaultXScreen() ), + aRwmRunning, + 0, 32, + False, + aRwmRunning, + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty ) == 0 ) + { + if( aRealType == aRwmRunning ) + m_aWMName = "ReflectionX"; + XFree( pProperty ); + } + else + { + aRwmRunning = XInternAtom( m_pDisplay, "_WRQ_WM_RUNNING", True ); + if( aRwmRunning != None && + XGetWindowProperty( m_pDisplay, + m_pSalDisplay->GetRootWindow( m_pSalDisplay->GetDefaultXScreen() ), + aRwmRunning, + 0, 32, + False, + XA_STRING, + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty ) == 0 ) + { + if( aRealType == XA_STRING ) + m_aWMName = "ReflectionX Windows"; + XFree( pProperty ); + } + } + } + if( !m_aWMName.isEmpty() ) + return; + + Atom aTTAPlatform = XInternAtom( m_pDisplay, "TTA_CLIENT_PLATFORM", True ); + if( aTTAPlatform == None || + XGetWindowProperty( m_pDisplay, + m_pSalDisplay->GetRootWindow( m_pSalDisplay->GetDefaultXScreen() ), + aTTAPlatform, + 0, 32, + False, + XA_STRING, + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty ) != 0 ) + return; + + if( aRealType == XA_STRING ) + { + m_aWMName = "Tarantella"; + // #i62319# pretend that AlwaysOnTop works since + // the alwaysontop workaround in salframe.cxx results + // in a raise/lower loop on a Windows tarantella client + // FIXME: this property contains an identification string that + // in theory should be good enough to recognize running on a + // Windows client; however this string does not seem to be + // documented as well as the property itself. + m_bEnableAlwaysOnTopWorks = true; + } + XFree( pProperty ); +} + +/* + * WMAdaptor destructor + */ + +WMAdaptor::~WMAdaptor() +{ +} + +/* + * NetWMAdaptor constructor + */ + +NetWMAdaptor::NetWMAdaptor( SalDisplay* pSalDisplay ) : + WMAdaptor( pSalDisplay ) +{ + // currently all _NET WMs do transient like expected + + Atom aRealType = None; + int nFormat = 8; + unsigned long nItems = 0; + unsigned long nBytesLeft = 0; + unsigned char* pProperty = nullptr; + + initAtoms(); + + // check for NetWM + bool bNetWM = getNetWmName(); + if( bNetWM + && XGetWindowProperty( m_pDisplay, + m_pSalDisplay->GetRootWindow( m_pSalDisplay->GetDefaultXScreen() ), + m_aWMAtoms[ NET_SUPPORTED ], + 0, 0, + False, + XA_ATOM, + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty ) == 0 + && aRealType == XA_ATOM + && nFormat == 32 + ) + { + if( pProperty ) + { + XFree( pProperty ); + pProperty = nullptr; + } + // collect supported protocols + if( XGetWindowProperty( m_pDisplay, + m_pSalDisplay->GetRootWindow( m_pSalDisplay->GetDefaultXScreen() ), + m_aWMAtoms[ NET_SUPPORTED ], + 0, nBytesLeft/4, + False, + XA_ATOM, + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty ) == 0 + && nItems + ) + { + Atom* pAtoms = reinterpret_cast(pProperty); + char** pAtomNames = static_cast(alloca( sizeof(char*)*nItems )); + if( XGetAtomNames( m_pDisplay, pAtoms, nItems, pAtomNames ) ) + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.app", "supported protocols:"); +#endif + for( unsigned long i = 0; i < nItems; i++ ) + { + // #i80971# protect against invalid atoms + if( pAtomNames[i] == nullptr ) + continue; + + WMAdaptorProtocol aSearch; + aSearch.pProtocol = pAtomNames[i]; + WMAdaptorProtocol* pMatch = static_cast( + bsearch( &aSearch, + aProtocolTab, + SAL_N_ELEMENTS( aProtocolTab ), + sizeof( struct WMAdaptorProtocol ), + compareProtocol )); + if( pMatch ) + { + m_aWMAtoms[ pMatch->nProtocol ] = pAtoms[ i ]; + if( pMatch->nProtocol == NET_WM_STATE_STAYS_ON_TOP ) + m_bEnableAlwaysOnTopWorks = true; + } +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.app", " " + << pAtomNames[i] + << (((pMatch)&&(pMatch->nProtocol != -1)) ? + "" : " (unsupported)")); +#endif + XFree( pAtomNames[i] ); + } + } + XFree( pProperty ); + pProperty = nullptr; + } + else if( pProperty ) + { + XFree( pProperty ); + pProperty = nullptr; + } + + // get number of desktops + if( m_aWMAtoms[ NET_NUMBER_OF_DESKTOPS ] + && XGetWindowProperty( m_pDisplay, + m_pSalDisplay->GetRootWindow( m_pSalDisplay->GetDefaultXScreen() ), + m_aWMAtoms[ NET_NUMBER_OF_DESKTOPS ], + 0, 1, + False, + XA_CARDINAL, + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty ) == 0 + && pProperty + ) + { + m_nDesktops = *reinterpret_cast(pProperty); + XFree( pProperty ); + pProperty = nullptr; + // get work areas + if( m_aWMAtoms[ NET_WORKAREA ] + && XGetWindowProperty( m_pDisplay, + m_pSalDisplay->GetRootWindow( m_pSalDisplay->GetDefaultXScreen() ), + m_aWMAtoms[ NET_WORKAREA ], + 0, 4*m_nDesktops, + False, + XA_CARDINAL, + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty + ) == 0 + && nItems == 4*static_cast(m_nDesktops) + ) + { + m_aWMWorkAreas = ::std::vector< AbsoluteScreenPixelRectangle > ( m_nDesktops ); + tools::Long* pValues = reinterpret_cast(pProperty); + for( int i = 0; i < m_nDesktops; i++ ) + { + AbsoluteScreenPixelPoint aPoint( pValues[4*i], + pValues[4*i+1] ); + AbsoluteScreenPixelSize aSize( pValues[4*i+2], + pValues[4*i+3] ); + AbsoluteScreenPixelRectangle aWorkArea( aPoint, aSize ); + m_aWMWorkAreas[i] = aWorkArea; + if( aWorkArea != m_aWMWorkAreas[0] ) + m_bEqualWorkAreas = false; +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.app", "workarea " << i + << ": " << m_aWMWorkAreas[i].GetWidth() + << "x" << m_aWMWorkAreas[i].GetHeight() + << "+" << m_aWMWorkAreas[i].Left() + << "+" << m_aWMWorkAreas[i].Top()); +#endif + } + XFree( pProperty ); + } + else + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.app", nItems/4 << " workareas for " + << m_nDesktops << " desktops !"); +#endif + if( pProperty ) + { + XFree(pProperty); + pProperty = nullptr; + } + } + } + else if( pProperty ) + { + XFree( pProperty ); + pProperty = nullptr; + } + } + else if( pProperty ) + { + XFree( pProperty ); + pProperty = nullptr; + } +} + +/* + * GnomeWMAdaptor constructor + */ + +GnomeWMAdaptor::GnomeWMAdaptor( SalDisplay* pSalDisplay ) : + WMAdaptor( pSalDisplay ), + m_bValid( false ) +{ + // currently all Gnome WMs do transient like expected + + Atom aRealType = None; + int nFormat = 8; + unsigned long nItems = 0; + unsigned long nBytesLeft = 0; + unsigned char* pProperty = nullptr; + + initAtoms(); + + // check for GnomeWM + if( m_aWMAtoms[ WIN_SUPPORTING_WM_CHECK ] && m_aWMAtoms[ WIN_PROTOCOLS ] ) + { + if( XGetWindowProperty( m_pDisplay, + m_pSalDisplay->GetRootWindow( m_pSalDisplay->GetDefaultXScreen() ), + m_aWMAtoms[ WIN_SUPPORTING_WM_CHECK ], + 0, 1, + False, + XA_CARDINAL, + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty ) == 0 + && aRealType == XA_CARDINAL + && nFormat == 32 + && nItems != 0 + ) + { + ::Window aWMChild = *reinterpret_cast< ::Window* >(pProperty); + XFree( pProperty ); + pProperty = nullptr; + GetGenericUnixSalData()->ErrorTrapPush(); + if( XGetWindowProperty( m_pDisplay, + aWMChild, + m_aWMAtoms[ WIN_SUPPORTING_WM_CHECK ], + 0, 1, + False, + XA_CARDINAL, + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty ) == 0 + && aRealType == XA_CARDINAL + && nFormat == 32 + && nItems != 0 ) + { + if (! GetGenericUnixSalData()->ErrorTrapPop( false ) ) + { + GetGenericUnixSalData()->ErrorTrapPush(); + + ::Window aCheckWindow = *reinterpret_cast< ::Window* >(pProperty); + XFree( pProperty ); + pProperty = nullptr; + if( aCheckWindow == aWMChild ) + { + m_bValid = true; + /* + * get name of WM + * this is NOT part of the GNOME WM hints, but e.g. Sawfish + * already supports this part of the extended WM hints + */ + m_aWMAtoms[ UTF8_STRING ] = XInternAtom( m_pDisplay, "UTF8_STRING", False ); + getNetWmName(); + } + } + else + GetGenericUnixSalData()->ErrorTrapPush(); + } + GetGenericUnixSalData()->ErrorTrapPop(); + } + else if( pProperty ) + { + XFree( pProperty ); + pProperty = nullptr; + } + } + if( m_bValid + && XGetWindowProperty( m_pDisplay, + m_pSalDisplay->GetRootWindow( m_pSalDisplay->GetDefaultXScreen() ), + m_aWMAtoms[ WIN_PROTOCOLS ], + 0, 0, + False, + XA_ATOM, + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty ) == 0 + && aRealType == XA_ATOM + && nFormat == 32 + ) + { + if( pProperty ) + { + XFree( pProperty ); + pProperty = nullptr; + } + // collect supported protocols + if( XGetWindowProperty( m_pDisplay, + m_pSalDisplay->GetRootWindow( m_pSalDisplay->GetDefaultXScreen() ), + m_aWMAtoms[ WIN_PROTOCOLS ], + 0, nBytesLeft/4, + False, + XA_ATOM, + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty ) == 0 + && pProperty + ) + { + Atom* pAtoms = reinterpret_cast(pProperty); + char** pAtomNames = static_cast(alloca( sizeof(char*)*nItems )); + if( XGetAtomNames( m_pDisplay, pAtoms, nItems, pAtomNames ) ) + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.app", "supported protocols:"); +#endif + for( unsigned long i = 0; i < nItems; i++ ) + { + // #i80971# protect against invalid atoms + if( pAtomNames[i] == nullptr ) + continue; + + WMAdaptorProtocol aSearch; + aSearch.pProtocol = pAtomNames[i]; + WMAdaptorProtocol* pMatch = static_cast( + bsearch( &aSearch, + aProtocolTab, + SAL_N_ELEMENTS( aProtocolTab ), + sizeof( struct WMAdaptorProtocol ), + compareProtocol )); + if( pMatch ) + { + m_aWMAtoms[ pMatch->nProtocol ] = pAtoms[ i ]; + if( pMatch->nProtocol == WIN_LAYER ) + m_bEnableAlwaysOnTopWorks = true; + } + if( strncmp( "_ICEWM_TRAY", pAtomNames[i], 11 ) == 0 ) + { + m_aWMName = "IceWM"; + m_nWinGravity = NorthWestGravity; + m_nInitWinGravity = NorthWestGravity; + } +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.app", " " + << pAtomNames[i] + << (((pMatch) && (pMatch->nProtocol != -1)) ? + "" : " (unsupported)")); +#endif + XFree( pAtomNames[i] ); + } + } + XFree( pProperty ); + pProperty = nullptr; + } + else if( pProperty ) + { + XFree( pProperty ); + pProperty = nullptr; + } + + // get number of desktops + if( m_aWMAtoms[ WIN_WORKSPACE_COUNT ] + && XGetWindowProperty( m_pDisplay, + m_pSalDisplay->GetRootWindow( m_pSalDisplay->GetDefaultXScreen() ), + m_aWMAtoms[ WIN_WORKSPACE_COUNT ], + 0, 1, + False, + XA_CARDINAL, + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty ) == 0 + && pProperty + ) + { + m_nDesktops = *reinterpret_cast(pProperty); + XFree( pProperty ); + pProperty = nullptr; + } + else if( pProperty ) + { + XFree( pProperty ); + pProperty = nullptr; + } + } + else if( pProperty ) + { + XFree( pProperty ); + pProperty = nullptr; + } +} + +/* + * getNetWmName() + */ +bool WMAdaptor::getNetWmName() +{ + Atom aRealType = None; + int nFormat = 8; + unsigned long nItems = 0; + unsigned long nBytesLeft = 0; + unsigned char* pProperty = nullptr; + bool bNetWM = false; + + if( m_aWMAtoms[ NET_SUPPORTING_WM_CHECK ] && m_aWMAtoms[ NET_WM_NAME ] ) + { + if( XGetWindowProperty( m_pDisplay, + m_pSalDisplay->GetRootWindow( m_pSalDisplay->GetDefaultXScreen() ), + m_aWMAtoms[ NET_SUPPORTING_WM_CHECK ], + 0, 1, + False, + XA_WINDOW, + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty ) == 0 + && aRealType == XA_WINDOW + && nFormat == 32 + && nItems != 0 + ) + { + ::Window aWMChild = *reinterpret_cast< ::Window* >(pProperty); + XFree( pProperty ); + pProperty = nullptr; + GetGenericUnixSalData()->ErrorTrapPush(); + if( XGetWindowProperty( m_pDisplay, + aWMChild, + m_aWMAtoms[ NET_SUPPORTING_WM_CHECK ], + 0, 1, + False, + XA_WINDOW, + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty ) == 0 + && aRealType == XA_WINDOW + && nFormat == 32 + && nItems != 0 ) + { + if ( ! GetGenericUnixSalData()->ErrorTrapPop( false ) ) + { + GetGenericUnixSalData()->ErrorTrapPush(); + ::Window aCheckWindow = *reinterpret_cast< ::Window* >(pProperty); + XFree( pProperty ); + pProperty = nullptr; + if( aCheckWindow == aWMChild ) + { + bNetWM = true; + // get name of WM + m_aWMAtoms[ UTF8_STRING ] = XInternAtom( m_pDisplay, "UTF8_STRING", False ); + if( XGetWindowProperty( m_pDisplay, + aWMChild, + m_aWMAtoms[ NET_WM_NAME ], + 0, 256, + False, + AnyPropertyType, /* m_aWMAtoms[ UTF8_STRING ],*/ + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty ) == 0 + && nItems != 0 + ) + { + if (aRealType == m_aWMAtoms[ UTF8_STRING ]) + m_aWMName = OUString( reinterpret_cast(pProperty), nItems, RTL_TEXTENCODING_UTF8 ); + else if (aRealType == XA_STRING) + m_aWMName = OUString( reinterpret_cast(pProperty), nItems, RTL_TEXTENCODING_ISO_8859_1 ); + + XFree( pProperty ); + pProperty = nullptr; + } + else if( pProperty ) + { + XFree( pProperty ); + pProperty = nullptr; + } + + // if this is metacity, check for version to enable a legacy workaround + if( m_aWMName == "Metacity" ) + { + int nVersionMajor = 0, nVersionMinor = 0; + Atom nVersionAtom = XInternAtom( m_pDisplay, "_METACITY_VERSION", True ); + if( nVersionAtom ) + { + if( XGetWindowProperty( m_pDisplay, + aWMChild, + nVersionAtom, + 0, 256, + False, + m_aWMAtoms[ UTF8_STRING ], + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty ) == 0 + && nItems != 0 + ) + { + OUString aMetaVersion( reinterpret_cast(pProperty), nItems, RTL_TEXTENCODING_UTF8 ); + sal_Int32 nIdx {0}; + nVersionMajor = o3tl::toInt32(o3tl::getToken(aMetaVersion, 0, '.', nIdx)); + nVersionMinor = o3tl::toInt32(o3tl::getToken(aMetaVersion, 0, '.', nIdx)); + } + if( pProperty ) + { + XFree( pProperty ); + pProperty = nullptr; + } + } + if( nVersionMajor < 2 || (nVersionMajor == 2 && nVersionMinor < 12) ) + m_bLegacyPartialFullscreen = true; + } + } + } + else + { + if( pProperty ) + { + XFree( pProperty ); + pProperty = nullptr; + } + GetGenericUnixSalData()->ErrorTrapPush(); + } + } + + GetGenericUnixSalData()->ErrorTrapPop(); + } + else if( pProperty ) + { + XFree( pProperty ); + pProperty = nullptr; + } + } + return bNetWM; +} + +bool WMAdaptor::getWMshouldSwitchWorkspace() const +{ + if( ! m_bWMshouldSwitchWorkspaceInit ) + { + WMAdaptor * pWMA = const_cast(this); + + pWMA->m_bWMshouldSwitchWorkspace = true; + vcl::SettingsConfigItem* pItem = vcl::SettingsConfigItem::get(); + OUString aSetting( pItem->getValue( "WM", + "ShouldSwitchWorkspace" ) ); + if( aSetting.isEmpty() ) + { + if( m_aWMName == "awesome" ) + { + pWMA->m_bWMshouldSwitchWorkspace = false; + } + } + else + pWMA->m_bWMshouldSwitchWorkspace = aSetting.toBoolean(); + pWMA->m_bWMshouldSwitchWorkspaceInit = true; + } + return m_bWMshouldSwitchWorkspace; +} + +/* + * WMAdaptor::isValid() + */ +bool WMAdaptor::isValid() const +{ + return true; +} + +/* + * NetWMAdaptor::isValid() + */ +bool NetWMAdaptor::isValid() const +{ + // some necessary sanity checks; there are WMs out there + // which implement some of the WM hints spec without + // real functionality + return + m_aWMAtoms[ NET_SUPPORTED ] + && m_aWMAtoms[ NET_SUPPORTING_WM_CHECK ] + && m_aWMAtoms[ NET_WM_NAME ] + && m_aWMAtoms[ NET_WM_WINDOW_TYPE_NORMAL ] + && m_aWMAtoms[ NET_WM_WINDOW_TYPE_DIALOG ] + ; +} + +/* + * GnomeWMAdaptor::isValid() + */ +bool GnomeWMAdaptor::isValid() const +{ + return m_bValid; +} + +/* + * WMAdaptor::initAtoms + */ + +void WMAdaptor::initAtoms() +{ + // get basic atoms + for(const WMAdaptorProtocol & i : aAtomTab) + m_aWMAtoms[ i.nProtocol ] = XInternAtom( m_pDisplay, i.pProtocol, False ); + m_aWMAtoms[ NET_SUPPORTING_WM_CHECK ] = XInternAtom( m_pDisplay, "_NET_SUPPORTING_WM_CHECK", True ); + m_aWMAtoms[ NET_WM_NAME ] = XInternAtom( m_pDisplay, "_NET_WM_NAME", True ); +} + +/* + * NetWMAdaptor::initAtoms + */ + +void NetWMAdaptor::initAtoms() +{ + WMAdaptor::initAtoms(); + + m_aWMAtoms[ NET_SUPPORTED ] = XInternAtom( m_pDisplay, "_NET_SUPPORTED", True ); +} + +/* + * GnomeWMAdaptor::initAtoms + */ + +void GnomeWMAdaptor::initAtoms() +{ + WMAdaptor::initAtoms(); + + m_aWMAtoms[ WIN_PROTOCOLS ] = XInternAtom( m_pDisplay, "_WIN_PROTOCOLS", True ); + m_aWMAtoms[ WIN_SUPPORTING_WM_CHECK ] = XInternAtom( m_pDisplay, "_WIN_SUPPORTING_WM_CHECK", True ); +} + +/* + * WMAdaptor::setWMName + * sets WM_NAME + * WM_ICON_NAME + */ + +void WMAdaptor::setWMName( X11SalFrame* pFrame, const OUString& rWMName ) const +{ + OString aTitle(OUStringToOString(rWMName, + osl_getThreadTextEncoding())); + + OString aWMLocale; + rtl_Locale* pLocale = nullptr; + osl_getProcessLocale( &pLocale ); + if( pLocale ) + { + OUString aLocaleString( + LanguageTag( *pLocale).getGlibcLocaleString( std::u16string_view())); + aWMLocale = OUStringToOString( aLocaleString, RTL_TEXTENCODING_ISO_8859_1 ); + } + else + { + static const char* pLang = getenv( "LANG" ); + aWMLocale = pLang ? pLang : "C"; + } + + char* pT = const_cast(aTitle.getStr()); + XTextProperty aProp = { nullptr, None, 0, 0 }; + XmbTextListToTextProperty( m_pDisplay, + &pT, + 1, + XStdICCTextStyle, + &aProp ); + + unsigned char const * pData = aProp.nitems ? aProp.value : reinterpret_cast(aTitle.getStr()); + Atom nType = aProp.nitems ? aProp.encoding : XA_STRING; + int nFormat = aProp.nitems ? aProp.format : 8; + int nBytes = aProp.nitems ? aProp.nitems : aTitle.getLength(); + const SystemEnvData* pEnv = pFrame->GetSystemData(); + XChangeProperty( m_pDisplay, + static_cast<::Window>(pEnv->aShellWindow), + XA_WM_NAME, + nType, + nFormat, + PropModeReplace, + pData, + nBytes ); + XChangeProperty( m_pDisplay, + static_cast<::Window>(pEnv->aShellWindow), + XA_WM_ICON_NAME, + nType, + nFormat, + PropModeReplace, + pData, + nBytes ); + XChangeProperty( m_pDisplay, + static_cast<::Window>(pEnv->aShellWindow), + m_aWMAtoms[ WM_LOCALE_NAME ], + XA_STRING, + 8, + PropModeReplace, + reinterpret_cast(aWMLocale.getStr()), + aWMLocale.getLength() ); + if (aProp.value != nullptr) + XFree( aProp.value ); +} + +/* + * NetWMAdaptor::setWMName + * sets WM_NAME + * _NET_WM_NAME + * WM_ICON_NAME + * _NET_WM_ICON_NAME + */ +void NetWMAdaptor::setWMName( X11SalFrame* pFrame, const OUString& rWMName ) const +{ + WMAdaptor::setWMName( pFrame, rWMName ); + + OString aTitle(OUStringToOString(rWMName, RTL_TEXTENCODING_UTF8)); + const SystemEnvData* pEnv = pFrame->GetSystemData(); + if( m_aWMAtoms[ NET_WM_NAME ] ) + XChangeProperty( m_pDisplay, + static_cast<::Window>(pEnv->aShellWindow), + m_aWMAtoms[ NET_WM_NAME ], + m_aWMAtoms[ UTF8_STRING ], + 8, + PropModeReplace, + reinterpret_cast(aTitle.getStr()), + aTitle.getLength() ); + if( m_aWMAtoms[ NET_WM_ICON_NAME ] ) + XChangeProperty( m_pDisplay, + static_cast<::Window>(pEnv->aShellWindow), + m_aWMAtoms[ NET_WM_ICON_NAME ], + m_aWMAtoms[ UTF8_STRING ], + 8, + PropModeReplace, + reinterpret_cast(aTitle.getStr()), + aTitle.getLength() ); +} + +/* + * NetWMAdaptor::setNetWMState + * sets _NET_WM_STATE + */ +void NetWMAdaptor::setNetWMState( X11SalFrame* pFrame ) const +{ + if( !(m_aWMAtoms[ NET_WM_STATE ]) ) + return; + + Atom aStateAtoms[ 10 ]; + int nStateAtoms = 0; + + // set NET_WM_STATE_MODAL + if( pFrame->mbMaximizedVert + && m_aWMAtoms[ NET_WM_STATE_MAXIMIZED_VERT ] ) + aStateAtoms[ nStateAtoms++ ] = m_aWMAtoms[ NET_WM_STATE_MAXIMIZED_VERT ]; + if( pFrame->mbMaximizedHorz + && m_aWMAtoms[ NET_WM_STATE_MAXIMIZED_HORZ ] ) + aStateAtoms[ nStateAtoms++ ] = m_aWMAtoms[ NET_WM_STATE_MAXIMIZED_HORZ ]; + if( pFrame->bAlwaysOnTop_ && m_aWMAtoms[ NET_WM_STATE_STAYS_ON_TOP ] ) + aStateAtoms[ nStateAtoms++ ] = m_aWMAtoms[ NET_WM_STATE_STAYS_ON_TOP ]; + if( pFrame->mbFullScreen && m_aWMAtoms[ NET_WM_STATE_FULLSCREEN ] ) + aStateAtoms[ nStateAtoms++ ] = m_aWMAtoms[ NET_WM_STATE_FULLSCREEN ]; + if( pFrame->meWindowType == WMWindowType::Utility && m_aWMAtoms[ NET_WM_STATE_SKIP_TASKBAR ] ) + aStateAtoms[ nStateAtoms++ ] = m_aWMAtoms[ NET_WM_STATE_SKIP_TASKBAR ]; + + if( nStateAtoms ) + { + XChangeProperty( m_pDisplay, + pFrame->GetShellWindow(), + m_aWMAtoms[ NET_WM_STATE ], + XA_ATOM, + 32, + PropModeReplace, + reinterpret_cast(aStateAtoms), + nStateAtoms + ); + } + else + XDeleteProperty( m_pDisplay, + pFrame->GetShellWindow(), + m_aWMAtoms[ NET_WM_STATE ] ); + if( !pFrame->mbMaximizedHorz + || !pFrame->mbMaximizedVert + || ( pFrame->nStyle_ & SalFrameStyleFlags::SIZEABLE ) ) + return; + + /* + * for maximizing use NorthWestGravity (including decoration) + */ + XSizeHints hints; + tools::Long supplied; + bool bHint = false; + if( XGetWMNormalHints( m_pDisplay, + pFrame->GetShellWindow(), + &hints, + &supplied ) ) + { + bHint = true; + hints.flags |= PWinGravity; + hints.win_gravity = NorthWestGravity; + XSetWMNormalHints( m_pDisplay, + pFrame->GetShellWindow(), + &hints ); + XSync( m_pDisplay, False ); + } + + // SetPosSize necessary to set width/height, min/max w/h + sal_Int32 nCurrent = 0; + /* + * get current desktop here if work areas have different size + * (does this happen on any platform ?) + */ + if( ! m_bEqualWorkAreas ) + { + nCurrent = getCurrentWorkArea(); + if( nCurrent < 0 ) + nCurrent = 0; + } + AbsoluteScreenPixelRectangle aPosSize = m_aWMWorkAreas[nCurrent]; + const SalFrameGeometry& rGeom( pFrame->GetUnmirroredGeometry() ); + aPosSize = AbsoluteScreenPixelRectangle( AbsoluteScreenPixelPoint( aPosSize.Left() + rGeom.leftDecoration(), + aPosSize.Top() + rGeom.topDecoration() ), + AbsoluteScreenPixelSize( aPosSize.GetWidth() + - rGeom.leftDecoration() + - rGeom.rightDecoration(), + aPosSize.GetHeight() + - rGeom.topDecoration() + - rGeom.bottomDecoration() ) + ); + pFrame->SetPosSize( aPosSize ); + + /* + * reset gravity hint to static gravity + * (this should not move window according to ICCCM) + */ + if( bHint && pFrame->nShowState_ != X11ShowState::Unknown ) + { + hints.win_gravity = StaticGravity; + XSetWMNormalHints( m_pDisplay, + pFrame->GetShellWindow(), + &hints ); + } +} + +/* + * GnomeWMAdaptor::setNetWMState + * sets _WIN_STATE + */ +void GnomeWMAdaptor::setGnomeWMState( X11SalFrame* pFrame ) const +{ + if( !(m_aWMAtoms[ WIN_STATE ]) ) + return; + + sal_uInt32 nWinWMState = 0; + + if( pFrame->mbMaximizedVert ) + nWinWMState |= 1 << 2; + if( pFrame->mbMaximizedHorz ) + nWinWMState |= 1 << 3; + + XChangeProperty( m_pDisplay, + pFrame->GetShellWindow(), + m_aWMAtoms[ WIN_STATE ], + XA_CARDINAL, + 32, + PropModeReplace, + reinterpret_cast(&nWinWMState), + 1 + ); + if( !pFrame->mbMaximizedHorz + || !pFrame->mbMaximizedVert + || ( pFrame->nStyle_ & SalFrameStyleFlags::SIZEABLE ) ) + return; + + /* + * for maximizing use NorthWestGravity (including decoration) + */ + XSizeHints hints; + tools::Long supplied; + bool bHint = false; + if( XGetWMNormalHints( m_pDisplay, + pFrame->GetShellWindow(), + &hints, + &supplied ) ) + { + bHint = true; + hints.flags |= PWinGravity; + hints.win_gravity = NorthWestGravity; + XSetWMNormalHints( m_pDisplay, + pFrame->GetShellWindow(), + &hints ); + XSync( m_pDisplay, False ); + } + + // SetPosSize necessary to set width/height, min/max w/h + sal_Int32 nCurrent = 0; + /* + * get current desktop here if work areas have different size + * (does this happen on any platform ?) + */ + if( ! m_bEqualWorkAreas ) + { + nCurrent = getCurrentWorkArea(); + if( nCurrent < 0 ) + nCurrent = 0; + } + AbsoluteScreenPixelRectangle aPosSize = m_aWMWorkAreas[nCurrent]; + const SalFrameGeometry& rGeom( pFrame->GetUnmirroredGeometry() ); + aPosSize = AbsoluteScreenPixelRectangle( AbsoluteScreenPixelPoint( aPosSize.Left() + rGeom.leftDecoration(), + aPosSize.Top() + rGeom.topDecoration() ), + AbsoluteScreenPixelSize( aPosSize.GetWidth() + - rGeom.leftDecoration() + - rGeom.rightDecoration(), + aPosSize.GetHeight() + - rGeom.topDecoration() + - rGeom.bottomDecoration() ) + ); + pFrame->SetPosSize( aPosSize ); + + /* + * reset gravity hint to static gravity + * (this should not move window according to ICCCM) + */ + if( bHint && pFrame->nShowState_ != X11ShowState::Unknown ) + { + hints.win_gravity = StaticGravity; + XSetWMNormalHints( m_pDisplay, + pFrame->GetShellWindow(), + &hints ); + } +} + +/* + * WMAdaptor::setFrameDecoration + * sets _MOTIF_WM_HINTS + * WM_TRANSIENT_FOR + */ + +void WMAdaptor::setFrameTypeAndDecoration( X11SalFrame* pFrame, WMWindowType eType, int nDecorationFlags, X11SalFrame* pReferenceFrame ) const +{ + pFrame->meWindowType = eType; + + if( ! pFrame->mbFullScreen ) + { + // set mwm hints + struct _mwmhints { + unsigned long flags, func, deco; + tools::Long input_mode; + unsigned long status; + } aHint; + + aHint.flags = 15; /* flags for functions, decoration, input mode and status */ + aHint.deco = 0; + aHint.func = 1 << 2; + aHint.status = 0; + aHint.input_mode = 0; + + // evaluate decoration flags + if( nDecorationFlags & decoration_All ) + { + aHint.deco = 1; + aHint.func = 1; + } + else + { + if( nDecorationFlags & decoration_Title ) + aHint.deco |= 1 << 3; + if( nDecorationFlags & decoration_Border ) + aHint.deco |= 1 << 1; + if( nDecorationFlags & decoration_Resize ) + { + aHint.deco |= 1 << 2; + aHint.func |= 1 << 1; + } + if( nDecorationFlags & decoration_MinimizeBtn ) + { + aHint.deco |= 1 << 5; + aHint.func |= 1 << 3; + } + if( nDecorationFlags & decoration_MaximizeBtn ) + { + aHint.deco |= 1 << 6; + aHint.func |= 1 << 4; + } + if( nDecorationFlags & decoration_CloseBtn ) + { + aHint.deco |= 1 << 4; + aHint.func |= 1 << 5; + } + } + + // set the hint + XChangeProperty( m_pDisplay, + pFrame->GetShellWindow(), + m_aWMAtoms[ MOTIF_WM_HINTS ], + m_aWMAtoms[ MOTIF_WM_HINTS ], + 32, + PropModeReplace, + reinterpret_cast(&aHint), + 5 ); + } + + // set transientFor hint + /* #91030# dtwm will not map a dialogue if the transient + * window is iconified. This is deemed undesirable because + * message boxes do not get mapped, so use the root as transient + * instead. + */ + if( pReferenceFrame ) + { + XSetTransientForHint( m_pDisplay, + pFrame->GetShellWindow(), + pReferenceFrame->bMapped_ ? + pReferenceFrame->GetShellWindow() : + m_pSalDisplay->GetRootWindow( pFrame->GetScreenNumber() ) + ); + if( ! pReferenceFrame->bMapped_ ) + pFrame->mbTransientForRoot = true; + } +} + +/* + * NetWMAdaptor::setFrameDecoration + * sets _MOTIF_WM_HINTS + * _NET_WM_WINDOW_TYPE + * _NET_WM_STATE + * WM_TRANSIENT_FOR + */ + +void NetWMAdaptor::setFrameTypeAndDecoration( X11SalFrame* pFrame, WMWindowType eType, int nDecorationFlags, X11SalFrame* pReferenceFrame ) const +{ + WMAdaptor::setFrameTypeAndDecoration( pFrame, eType, nDecorationFlags, pReferenceFrame ); + + setNetWMState( pFrame ); + + // set NET_WM_WINDOW_TYPE + if( m_aWMAtoms[ NET_WM_WINDOW_TYPE ] ) + { + Atom aWindowTypes[4]; + int nWindowTypes = 0; + switch( eType ) + { + case WMWindowType::Utility: + aWindowTypes[nWindowTypes++] = + m_aWMAtoms[ NET_WM_WINDOW_TYPE_UTILITY ] ? + m_aWMAtoms[ NET_WM_WINDOW_TYPE_UTILITY ] : + m_aWMAtoms[ NET_WM_WINDOW_TYPE_DIALOG ]; + break; + case WMWindowType::ModelessDialogue: + aWindowTypes[nWindowTypes++] = + m_aWMAtoms[ NET_WM_WINDOW_TYPE_DIALOG ]; + break; + case WMWindowType::Splash: + aWindowTypes[nWindowTypes++] = + m_aWMAtoms[ NET_WM_WINDOW_TYPE_SPLASH ] ? + m_aWMAtoms[ NET_WM_WINDOW_TYPE_SPLASH ] : + m_aWMAtoms[ NET_WM_WINDOW_TYPE_NORMAL ]; + break; + case WMWindowType::Toolbar: + if( m_aWMAtoms[ KDE_NET_WM_WINDOW_TYPE_OVERRIDE ] ) + aWindowTypes[nWindowTypes++] = m_aWMAtoms[ KDE_NET_WM_WINDOW_TYPE_OVERRIDE ]; + aWindowTypes[nWindowTypes++] = + m_aWMAtoms[ NET_WM_WINDOW_TYPE_TOOLBAR ] ? + m_aWMAtoms[ NET_WM_WINDOW_TYPE_TOOLBAR ] : + m_aWMAtoms[ NET_WM_WINDOW_TYPE_NORMAL]; + break; + case WMWindowType::Dock: + aWindowTypes[nWindowTypes++] = + m_aWMAtoms[ NET_WM_WINDOW_TYPE_DOCK ] ? + m_aWMAtoms[ NET_WM_WINDOW_TYPE_DOCK ] : + m_aWMAtoms[ NET_WM_WINDOW_TYPE_NORMAL]; + break; + default: + aWindowTypes[nWindowTypes++] = m_aWMAtoms[ NET_WM_WINDOW_TYPE_NORMAL ]; + break; + } + XChangeProperty( m_pDisplay, + pFrame->GetShellWindow(), + m_aWMAtoms[ NET_WM_WINDOW_TYPE ], + XA_ATOM, + 32, + PropModeReplace, + reinterpret_cast(aWindowTypes), + nWindowTypes ); + } + if( ( eType == WMWindowType::ModelessDialogue ) + && ! pReferenceFrame ) + { + XSetTransientForHint( m_pDisplay, + pFrame->GetShellWindow(), + m_pSalDisplay->GetRootWindow( pFrame->GetScreenNumber() ) ); + pFrame->mbTransientForRoot = true; + } +} + +/* + * WMAdaptor::maximizeFrame + */ + +void WMAdaptor::maximizeFrame( X11SalFrame* pFrame, bool bHorizontal, bool bVertical ) const +{ + pFrame->mbMaximizedVert = bVertical; + pFrame->mbMaximizedHorz = bHorizontal; + + const SalFrameGeometry& rGeom( pFrame->GetUnmirroredGeometry() ); + + // discard pending configure notifies for this frame + XSync( m_pDisplay, False ); + XEvent aDiscard; + while( XCheckTypedWindowEvent( m_pDisplay, + pFrame->GetShellWindow(), + ConfigureNotify, + &aDiscard ) ) + ; + while( XCheckTypedWindowEvent( m_pDisplay, + pFrame->GetWindow(), + ConfigureNotify, + &aDiscard ) ) + ; + + if( bHorizontal || bVertical ) + { + AbsoluteScreenPixelSize aScreenSize( m_pSalDisplay->GetScreenSize( pFrame->GetScreenNumber() ) ); + AbsoluteScreenPixelPoint aTL( rGeom.leftDecoration(), rGeom.topDecoration() ); + if( m_pSalDisplay->IsXinerama() ) + { + AbsoluteScreenPixelPoint aMed( aTL.X() + rGeom.width()/2, aTL.Y() + rGeom.height()/2 ); + const std::vector< AbsoluteScreenPixelRectangle >& rScreens = m_pSalDisplay->GetXineramaScreens(); + for(const auto & rScreen : rScreens) + if( rScreen.Contains( aMed ) ) + { + aTL += rScreen.TopLeft(); + aScreenSize = rScreen.GetSize(); + break; + } + } + AbsoluteScreenPixelRectangle aTarget( aTL, + AbsoluteScreenPixelSize( aScreenSize.Width() - rGeom.leftDecoration() - rGeom.topDecoration(), + aScreenSize.Height() - rGeom.topDecoration() - rGeom.bottomDecoration() ) + ); + + const AbsoluteScreenPixelRectangle aReferenceGeometry = !pFrame->maRestorePosSize.IsEmpty() ? + pFrame->maRestorePosSize : AbsoluteScreenPixelRectangle(rGeom.posSize()); + if( ! bHorizontal ) + { + aTarget.SetSize({ aReferenceGeometry.GetWidth(), aTarget.GetHeight() }); + aTarget.SetLeft(aReferenceGeometry.Left()); + } + else if( ! bVertical ) + { + aTarget.SetSize({ aTarget.GetWidth(), aReferenceGeometry.GetHeight() }); + aTarget.SetTop(aReferenceGeometry.Top()); + } + + AbsoluteScreenPixelRectangle aRestore(rGeom.posSize()); + if( pFrame->bMapped_ ) + { + XSetInputFocus( m_pDisplay, + pFrame->GetShellWindow(), + RevertToNone, + CurrentTime + ); + } + + if( pFrame->maRestorePosSize.IsEmpty() ) + pFrame->maRestorePosSize = aRestore; + + pFrame->SetPosSize( aTarget ); + pFrame->nWidth_ = aTarget.GetWidth(); + pFrame->nHeight_ = aTarget.GetHeight(); + XRaiseWindow( m_pDisplay, + pFrame->GetShellWindow() + ); + if( pFrame->GetStackingWindow() ) + XRaiseWindow( m_pDisplay, + pFrame->GetStackingWindow() + ); + + } + else + { + pFrame->SetPosSize( pFrame->maRestorePosSize ); + pFrame->maRestorePosSize = AbsoluteScreenPixelRectangle(); + pFrame->nWidth_ = rGeom.width(); + pFrame->nHeight_ = rGeom.height(); + } +} + +/* + * NetWMAdaptor::maximizeFrame + * changes _NET_WM_STATE by sending a client message + */ + +void NetWMAdaptor::maximizeFrame( X11SalFrame* pFrame, bool bHorizontal, bool bVertical ) const +{ + pFrame->mbMaximizedVert = bVertical; + pFrame->mbMaximizedHorz = bHorizontal; + + if( m_aWMAtoms[ NET_WM_STATE ] + && m_aWMAtoms[ NET_WM_STATE_MAXIMIZED_VERT ] + && m_aWMAtoms[ NET_WM_STATE_MAXIMIZED_HORZ ] + && ( pFrame->nStyle_ & ~SalFrameStyleFlags::DEFAULT ) + ) + { + if( pFrame->bMapped_ ) + { + // window already mapped, send WM a message + XEvent aEvent; + aEvent.type = ClientMessage; + aEvent.xclient.display = m_pDisplay; + aEvent.xclient.window = pFrame->GetShellWindow(); + aEvent.xclient.message_type = m_aWMAtoms[ NET_WM_STATE ]; + aEvent.xclient.format = 32; + aEvent.xclient.data.l[0] = bHorizontal ? 1 : 0; + aEvent.xclient.data.l[1] = m_aWMAtoms[ NET_WM_STATE_MAXIMIZED_HORZ ]; + aEvent.xclient.data.l[2] = bHorizontal == bVertical ? m_aWMAtoms[ NET_WM_STATE_MAXIMIZED_VERT ] : 0; + aEvent.xclient.data.l[3] = 0; + aEvent.xclient.data.l[4] = 0; + XSendEvent( m_pDisplay, + m_pSalDisplay->GetRootWindow( pFrame->GetScreenNumber() ), + False, + SubstructureNotifyMask | SubstructureRedirectMask, + &aEvent + ); + if( bHorizontal != bVertical ) + { + aEvent.xclient.data.l[0]= bVertical ? 1 : 0; + aEvent.xclient.data.l[1]= m_aWMAtoms[ NET_WM_STATE_MAXIMIZED_VERT ]; + aEvent.xclient.data.l[2]= 0; + XSendEvent( m_pDisplay, + m_pSalDisplay->GetRootWindow( pFrame->GetScreenNumber() ), + False, + SubstructureNotifyMask | SubstructureRedirectMask, + &aEvent + ); + } + } + else + { + // window not mapped yet, set _NET_WM_STATE directly + setNetWMState( pFrame ); + } + if( !bHorizontal && !bVertical ) + pFrame->maRestorePosSize = AbsoluteScreenPixelRectangle(); + else if( pFrame->maRestorePosSize.IsEmpty() ) + { + const SalFrameGeometry& rGeom( pFrame->GetUnmirroredGeometry() ); + pFrame->maRestorePosSize = + AbsoluteScreenPixelRectangle( AbsoluteScreenPixelPoint( rGeom.x(), rGeom.y() ), AbsoluteScreenPixelSize( rGeom.width(), rGeom.height() ) ); + } + } + else + WMAdaptor::maximizeFrame( pFrame, bHorizontal, bVertical ); +} + +/* + * GnomeWMAdaptor::maximizeFrame + * changes _WIN_STATE by sending a client message + */ + +void GnomeWMAdaptor::maximizeFrame( X11SalFrame* pFrame, bool bHorizontal, bool bVertical ) const +{ + pFrame->mbMaximizedVert = bVertical; + pFrame->mbMaximizedHorz = bHorizontal; + + if( m_aWMAtoms[ WIN_STATE ] + && ( pFrame->nStyle_ & ~SalFrameStyleFlags::DEFAULT ) + ) + { + if( pFrame->bMapped_ ) + { + // window already mapped, send WM a message + XEvent aEvent; + aEvent.type = ClientMessage; + aEvent.xclient.display = m_pDisplay; + aEvent.xclient.window = pFrame->GetShellWindow(); + aEvent.xclient.message_type = m_aWMAtoms[ WIN_STATE ]; + aEvent.xclient.format = 32; + aEvent.xclient.data.l[0] = (1<<2)|(1<<3); + aEvent.xclient.data.l[1] = + (bVertical ? (1<<2) : 0) + | (bHorizontal ? (1<<3) : 0); + aEvent.xclient.data.l[2] = 0; + aEvent.xclient.data.l[3] = 0; + aEvent.xclient.data.l[4] = 0; + XSendEvent( m_pDisplay, + m_pSalDisplay->GetRootWindow( pFrame->GetScreenNumber() ), + False, + SubstructureNotifyMask, + &aEvent + ); + } + else + // window not mapped yet, set _WIN_STATE directly + setGnomeWMState( pFrame ); + + if( !bHorizontal && !bVertical ) + pFrame->maRestorePosSize = AbsoluteScreenPixelRectangle(); + else if( pFrame->maRestorePosSize.IsEmpty() ) + { + const SalFrameGeometry& rGeom( pFrame->GetUnmirroredGeometry() ); + pFrame->maRestorePosSize = + AbsoluteScreenPixelRectangle( AbsoluteScreenPixelPoint( rGeom.x(), rGeom.y() ), AbsoluteScreenPixelSize( rGeom.width(), rGeom.height() ) ); + } + } + else + WMAdaptor::maximizeFrame( pFrame, bHorizontal, bVertical ); +} + +/* + * WMAdaptor::enableAlwaysOnTop + */ +void WMAdaptor::enableAlwaysOnTop( X11SalFrame*, bool /*bEnable*/ ) const +{ +} + +/* + * NetWMAdaptor::enableAlwaysOnTop + */ +void NetWMAdaptor::enableAlwaysOnTop( X11SalFrame* pFrame, bool bEnable ) const +{ + pFrame->bAlwaysOnTop_ = bEnable; + if( !(m_aWMAtoms[ NET_WM_STATE_STAYS_ON_TOP ]) ) + return; + + if( pFrame->bMapped_ ) + { + // window already mapped, send WM a message + XEvent aEvent; + aEvent.type = ClientMessage; + aEvent.xclient.display = m_pDisplay; + aEvent.xclient.window = pFrame->GetShellWindow(); + aEvent.xclient.message_type = m_aWMAtoms[ NET_WM_STATE ]; + aEvent.xclient.format = 32; + aEvent.xclient.data.l[0] = bEnable ? 1 : 0; + aEvent.xclient.data.l[1] = m_aWMAtoms[ NET_WM_STATE_STAYS_ON_TOP ]; + aEvent.xclient.data.l[2] = 0; + aEvent.xclient.data.l[3] = 0; + aEvent.xclient.data.l[4] = 0; + XSendEvent( m_pDisplay, + m_pSalDisplay->GetRootWindow( pFrame->GetScreenNumber() ), + False, + SubstructureNotifyMask | SubstructureRedirectMask, + &aEvent + ); + } + else + setNetWMState( pFrame ); +} + +/* + * GnomeWMAdaptor::enableAlwaysOnTop + */ +void GnomeWMAdaptor::enableAlwaysOnTop( X11SalFrame* pFrame, bool bEnable ) const +{ + pFrame->bAlwaysOnTop_ = bEnable; + if( !(m_aWMAtoms[ WIN_LAYER ]) ) + return; + + if( pFrame->bMapped_ ) + { + // window already mapped, send WM a message + XEvent aEvent; + aEvent.type = ClientMessage; + aEvent.xclient.display = m_pDisplay; + aEvent.xclient.window = pFrame->GetShellWindow(); + aEvent.xclient.message_type = m_aWMAtoms[ WIN_LAYER ]; + aEvent.xclient.format = 32; + aEvent.xclient.data.l[0] = bEnable ? 6 : 4; + aEvent.xclient.data.l[1] = 0; + aEvent.xclient.data.l[2] = 0; + aEvent.xclient.data.l[3] = 0; + aEvent.xclient.data.l[4] = 0; + XSendEvent( m_pDisplay, + m_pSalDisplay->GetRootWindow( pFrame->GetScreenNumber() ), + False, + SubstructureNotifyMask | SubstructureRedirectMask, + &aEvent + ); + } + else + { + sal_uInt32 nNewLayer = bEnable ? 6 : 4; + XChangeProperty( m_pDisplay, + pFrame->GetShellWindow(), + m_aWMAtoms[ WIN_LAYER ], + XA_CARDINAL, + 32, + PropModeReplace, + reinterpret_cast(&nNewLayer), + 1 + ); + } +} + +/* + * WMAdaptor::changeReferenceFrame + */ +void WMAdaptor::changeReferenceFrame( X11SalFrame* pFrame, X11SalFrame const * pReferenceFrame ) const +{ + if( ( pFrame->nStyle_ & SalFrameStyleFlags::PLUG ) + || pFrame->IsOverrideRedirect() + || pFrame->IsFloatGrabWindow() + ) + return; + + ::Window aTransient = pFrame->pDisplay_->GetRootWindow( pFrame->GetScreenNumber() ); + pFrame->mbTransientForRoot = true; + if( pReferenceFrame ) + { + aTransient = pReferenceFrame->GetShellWindow(); + pFrame->mbTransientForRoot = false; + } + XSetTransientForHint( m_pDisplay, + pFrame->GetShellWindow(), + aTransient ); +} + +/* + * WMAdaptor::handlePropertyNotify + */ +int WMAdaptor::handlePropertyNotify( X11SalFrame*, XPropertyEvent* ) const +{ + return 0; +} + +/* + * NetWMAdaptor::handlePropertyNotify + */ +int NetWMAdaptor::handlePropertyNotify( X11SalFrame* pFrame, XPropertyEvent* pEvent ) const +{ + int nHandled = 1; + if( pEvent->atom == m_aWMAtoms[ NET_WM_STATE ] ) + { + pFrame->mbMaximizedHorz = pFrame->mbMaximizedVert = false; + + if( pEvent->state == PropertyNewValue ) + { + Atom nType, *pStates; + int nFormat; + unsigned long nItems, nBytesLeft; + unsigned char* pData = nullptr; + tools::Long nOffset = 0; + do + { + XGetWindowProperty( m_pDisplay, + pEvent->window, + m_aWMAtoms[ NET_WM_STATE ], + nOffset, 64, + False, + XA_ATOM, + &nType, + &nFormat, + &nItems, &nBytesLeft, + &pData ); + if( pData ) + { + if( nType == XA_ATOM && nFormat == 32 && nItems > 0 ) + { + pStates = reinterpret_cast(pData); + for( unsigned long i = 0; i < nItems; i++ ) + { + if( pStates[i] == m_aWMAtoms[ NET_WM_STATE_MAXIMIZED_VERT ] && m_aWMAtoms[ NET_WM_STATE_MAXIMIZED_VERT ] ) + pFrame->mbMaximizedVert = true; + else if( pStates[i] == m_aWMAtoms[ NET_WM_STATE_MAXIMIZED_HORZ ] && m_aWMAtoms[ NET_WM_STATE_MAXIMIZED_HORZ ] ) + pFrame->mbMaximizedHorz = true; + } + } + XFree( pData ); + pData = nullptr; + nOffset += nItems * nFormat / 32; + } + else + break; + } while( nBytesLeft > 0 ); + } + + if( ! (pFrame->mbMaximizedHorz || pFrame->mbMaximizedVert ) ) + pFrame->maRestorePosSize = AbsoluteScreenPixelRectangle(); + else + { + const SalFrameGeometry& rGeom = pFrame->GetUnmirroredGeometry(); + // the current geometry may already be changed by the corresponding + // ConfigureNotify, but this cannot be helped + pFrame->maRestorePosSize = + AbsoluteScreenPixelRectangle( AbsoluteScreenPixelPoint( rGeom.x(), rGeom.y() ), + AbsoluteScreenPixelSize( rGeom.width(), rGeom.height() ) ); + } + } + else if( pEvent->atom == m_aWMAtoms[ NET_WM_DESKTOP ] ) + { + pFrame->m_nWorkArea = getWindowWorkArea( pFrame->GetShellWindow() ); + } + else + nHandled = 0; + + return nHandled; +} + +/* + * GnomeWMAdaptor::handlePropertyNotify + */ +int GnomeWMAdaptor::handlePropertyNotify( X11SalFrame* pFrame, XPropertyEvent* pEvent ) const +{ + int nHandled = 1; + if( pEvent->atom == m_aWMAtoms[ WIN_STATE ] ) + { + pFrame->mbMaximizedHorz = pFrame->mbMaximizedVert = false; + + if( pEvent->state == PropertyNewValue ) + { + Atom nType; + int nFormat = 0; + unsigned long nItems = 0; + unsigned long nBytesLeft = 0; + unsigned char* pData = nullptr; + XGetWindowProperty( m_pDisplay, + pEvent->window, + m_aWMAtoms[ WIN_STATE ], + 0, 1, + False, + XA_CARDINAL, + &nType, + &nFormat, + &nItems, &nBytesLeft, + &pData ); + if( pData ) + { + if( nType == XA_CARDINAL && nFormat == 32 && nItems == 1 ) + { + sal_uInt32 nWinState = *reinterpret_cast(pData); + if( nWinState & (1<<2) ) + pFrame->mbMaximizedVert = true; + if( nWinState & (1<<3) ) + pFrame->mbMaximizedHorz = true; + } + XFree( pData ); + } + } + + if( ! (pFrame->mbMaximizedHorz || pFrame->mbMaximizedVert ) ) + pFrame->maRestorePosSize = AbsoluteScreenPixelRectangle(); + else + { + const SalFrameGeometry& rGeom = pFrame->GetUnmirroredGeometry(); + // the current geometry may already be changed by the corresponding + // ConfigureNotify, but this cannot be helped + pFrame->maRestorePosSize = + AbsoluteScreenPixelRectangle( AbsoluteScreenPixelPoint( rGeom.x(), rGeom.y() ), + AbsoluteScreenPixelSize( rGeom.width(), rGeom.height() ) ); + } + } + else if( pEvent->atom == m_aWMAtoms[ NET_WM_DESKTOP ] ) + { + pFrame->m_nWorkArea = getWindowWorkArea( pFrame->GetShellWindow() ); + } + else + nHandled = 0; + + return nHandled; +} + +/* + * WMAdaptor::showFullScreen + */ +void WMAdaptor::showFullScreen( X11SalFrame* pFrame, bool bFullScreen ) const +{ + pFrame->mbFullScreen = bFullScreen; + maximizeFrame( pFrame, bFullScreen, bFullScreen ); +} + +/* + * NetWMAdaptor::showFullScreen + */ +void NetWMAdaptor::showFullScreen( X11SalFrame* pFrame, bool bFullScreen ) const +{ + if( m_aWMAtoms[ NET_WM_STATE_FULLSCREEN ] ) + { + pFrame->mbFullScreen = bFullScreen; + if( bFullScreen ) + { + if( m_aWMAtoms[ MOTIF_WM_HINTS ] ) + { + XDeleteProperty( m_pDisplay, + pFrame->GetShellWindow(), + m_aWMAtoms[ MOTIF_WM_HINTS ] ); + } + } + if( pFrame->bMapped_ ) + { + // window already mapped, send WM a message + XEvent aEvent; + aEvent.type = ClientMessage; + aEvent.xclient.display = m_pDisplay; + aEvent.xclient.window = pFrame->GetShellWindow(); + aEvent.xclient.message_type = m_aWMAtoms[ NET_WM_STATE ]; + aEvent.xclient.format = 32; + aEvent.xclient.data.l[0] = bFullScreen ? 1 : 0; + aEvent.xclient.data.l[1] = m_aWMAtoms[ NET_WM_STATE_FULLSCREEN ]; + aEvent.xclient.data.l[2] = 0; + aEvent.xclient.data.l[3] = 0; + aEvent.xclient.data.l[4] = 0; + XSendEvent( m_pDisplay, + m_pSalDisplay->GetRootWindow( pFrame->GetScreenNumber() ), + False, + SubstructureNotifyMask | SubstructureRedirectMask, + &aEvent + ); + } + else + { + // window not mapped yet, set _NET_WM_STATE directly + setNetWMState( pFrame ); + } + // #i42750# guess size before resize event shows up + if( bFullScreen ) + { + if( m_pSalDisplay->IsXinerama() ) + { + ::Window aRoot, aChild; + int root_x = 0, root_y = 0, lx, ly; + unsigned int mask; + XQueryPointer( m_pDisplay, + m_pSalDisplay->GetRootWindow( pFrame->GetScreenNumber() ), + &aRoot, &aChild, + &root_x, &root_y, &lx, &ly, &mask ); + const std::vector< AbsoluteScreenPixelRectangle >& rScreens = m_pSalDisplay->GetXineramaScreens(); + AbsoluteScreenPixelPoint aMousePoint( root_x, root_y ); + for(const auto & rScreen : rScreens) + { + if( rScreen.Contains( aMousePoint ) ) + { + pFrame->maGeometry.setPosSize(tools::Rectangle(rScreen)); + break; + } + } + } + else + { + AbsoluteScreenPixelSize aSize = m_pSalDisplay->GetScreenSize( pFrame->GetScreenNumber() ); + pFrame->maGeometry.setPosSize({ { 0, 0 }, aSize }); + } + pFrame->CallCallback( SalEvent::MoveResize, nullptr ); + } + } + else WMAdaptor::showFullScreen( pFrame, bFullScreen ); +} + +/* + * WMAdaptor::getCurrentWorkArea + */ +// FIXME: multiscreen case +int WMAdaptor::getCurrentWorkArea() const +{ + int nCurrent = -1; + if( m_aWMAtoms[ NET_CURRENT_DESKTOP ] ) + { + Atom aRealType = None; + int nFormat = 8; + unsigned long nItems = 0; + unsigned long nBytesLeft = 0; + unsigned char* pProperty = nullptr; + if( XGetWindowProperty( m_pDisplay, + m_pSalDisplay->GetRootWindow( m_pSalDisplay->GetDefaultXScreen() ), + m_aWMAtoms[ NET_CURRENT_DESKTOP ], + 0, 1, + False, + XA_CARDINAL, + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty ) == 0 + && pProperty + ) + { + nCurrent = int(*reinterpret_cast(pProperty)); + XFree( pProperty ); + } + else if( pProperty ) + { + XFree( pProperty ); + pProperty = nullptr; + } + } + return nCurrent; +} + +/* + * WMAdaptor::getWindowWorkArea + */ +int WMAdaptor::getWindowWorkArea( ::Window aWindow ) const +{ + int nCurrent = -1; + if( m_aWMAtoms[ NET_WM_DESKTOP ] ) + { + Atom aRealType = None; + int nFormat = 8; + unsigned long nItems = 0; + unsigned long nBytesLeft = 0; + unsigned char* pProperty = nullptr; + if( XGetWindowProperty( m_pDisplay, + aWindow, + m_aWMAtoms[ NET_WM_DESKTOP ], + 0, 1, + False, + XA_CARDINAL, + &aRealType, + &nFormat, + &nItems, + &nBytesLeft, + &pProperty ) == 0 + && pProperty + ) + { + nCurrent = int(*reinterpret_cast(pProperty)); + XFree( pProperty ); + } + else if( pProperty ) + { + XFree( pProperty ); + pProperty = nullptr; + } + } + return nCurrent; +} + +/* + * WMAdaptor::getCurrentWorkArea + */ +// fixme: multi screen case +void WMAdaptor::switchToWorkArea( int nWorkArea ) const +{ + if( ! getWMshouldSwitchWorkspace() ) + return; + + if( !m_aWMAtoms[ NET_CURRENT_DESKTOP ] ) + return; + + XEvent aEvent; + aEvent.type = ClientMessage; + aEvent.xclient.display = m_pDisplay; + aEvent.xclient.window = m_pSalDisplay->GetRootWindow( m_pSalDisplay->GetDefaultXScreen() ); + aEvent.xclient.message_type = m_aWMAtoms[ NET_CURRENT_DESKTOP ]; + aEvent.xclient.format = 32; + aEvent.xclient.data.l[0] = nWorkArea; + aEvent.xclient.data.l[1] = 0; + aEvent.xclient.data.l[2] = 0; + aEvent.xclient.data.l[3] = 0; + aEvent.xclient.data.l[4] = 0; + XSendEvent( m_pDisplay, + m_pSalDisplay->GetRootWindow( m_pSalDisplay->GetDefaultXScreen() ), + False, + SubstructureNotifyMask | SubstructureRedirectMask, + &aEvent + ); + +} + +/* + * WMAdaptor::frameIsMapping + */ +void WMAdaptor::frameIsMapping( X11SalFrame* ) const +{ +} + +/* + * NetWMAdaptor::frameIsMapping + */ +void NetWMAdaptor::frameIsMapping( X11SalFrame* pFrame ) const +{ + setNetWMState( pFrame ); +} + +/* + * WMAdaptor::setUserTime + */ +void WMAdaptor::setUserTime( X11SalFrame*, tools::Long ) const +{ +} + +/* + * NetWMAdaptor::setUserTime + */ +void NetWMAdaptor::setUserTime( X11SalFrame* i_pFrame, tools::Long i_nUserTime ) const +{ + if( m_aWMAtoms[NET_WM_USER_TIME] ) + { + XChangeProperty( m_pDisplay, + i_pFrame->GetShellWindow(), + m_aWMAtoms[NET_WM_USER_TIME], + XA_CARDINAL, + 32, + PropModeReplace, + reinterpret_cast(&i_nUserTime), + 1 + ); + } +} + +/* + * WMAdaptor::setPID + */ +void WMAdaptor::setPID( X11SalFrame const * i_pFrame ) const +{ + if( !(m_aWMAtoms[NET_WM_PID]) ) + return; + + tools::Long nPID = static_cast(getpid()); + XChangeProperty( m_pDisplay, + i_pFrame->GetShellWindow(), + m_aWMAtoms[NET_WM_PID], + XA_CARDINAL, + 32, + PropModeReplace, + reinterpret_cast(&nPID), + 1 + ); +} + +/* +* WMAdaptor::setClientMachine +*/ +void WMAdaptor::setClientMachine( X11SalFrame const * i_pFrame ) const +{ + OString aWmClient( OUStringToOString( GetGenericUnixSalData()->GetHostname(), RTL_TEXTENCODING_ASCII_US ) ); + XTextProperty aClientProp = { reinterpret_cast(const_cast(aWmClient.getStr())), XA_STRING, 8, sal::static_int_cast( aWmClient.getLength() ) }; + XSetWMClientMachine( m_pDisplay, i_pFrame->GetShellWindow(), &aClientProp ); +} + +void WMAdaptor::answerPing( X11SalFrame const * i_pFrame, XClientMessageEvent const * i_pEvent ) const +{ + if( !m_aWMAtoms[NET_WM_PING] || + i_pEvent->message_type != m_aWMAtoms[ WM_PROTOCOLS ] || + static_cast(i_pEvent->data.l[0]) != m_aWMAtoms[ NET_WM_PING ] ) + return; + + XEvent aEvent; + aEvent.xclient = *i_pEvent; + aEvent.xclient.window = m_pSalDisplay->GetRootWindow( i_pFrame->GetScreenNumber() ); + XSendEvent( m_pDisplay, + m_pSalDisplay->GetRootWindow( i_pFrame->GetScreenNumber() ), + False, + SubstructureNotifyMask | SubstructureRedirectMask, + &aEvent + ); + XFlush( m_pDisplay ); +} + +void WMAdaptor::activateWindow( X11SalFrame const *pFrame, Time nTimestamp ) +{ + if (!pFrame->bMapped_) + return; + + XEvent aEvent; + + aEvent.xclient.type = ClientMessage; + aEvent.xclient.window = pFrame->GetShellWindow(); + aEvent.xclient.message_type = m_aWMAtoms[ NET_ACTIVE_WINDOW ]; + aEvent.xclient.format = 32; + aEvent.xclient.data.l[0] = 1; + aEvent.xclient.data.l[1] = nTimestamp; + aEvent.xclient.data.l[2] = None; + aEvent.xclient.data.l[3] = 0; + aEvent.xclient.data.l[4] = 0; + + XSendEvent( m_pDisplay, + m_pSalDisplay->GetRootWindow( pFrame->GetScreenNumber() ), + False, + SubstructureNotifyMask | SubstructureRedirectMask, + &aEvent ); + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/desktopdetect/desktopdetector.cxx b/vcl/unx/generic/desktopdetect/desktopdetector.cxx new file mode 100644 index 0000000000..8baeb303fe --- /dev/null +++ b/vcl/unx/generic/desktopdetect/desktopdetector.cxx @@ -0,0 +1,264 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include + +#include +#include +#include + +#include + +#include +#include + +static bool is_gnome_desktop( Display* pDisplay ) +{ + + // warning: these checks are coincidental, GNOME does not + // explicitly advertise itself + if ( getenv( "GNOME_DESKTOP_SESSION_ID" ) ) + return true; + + bool ret = false; + + Atom nAtom1 = XInternAtom( pDisplay, "GNOME_SM_PROXY", True ); + Atom nAtom2 = XInternAtom( pDisplay, "NAUTILUS_DESKTOP_WINDOW_ID", True ); + if( nAtom1 || nAtom2 ) + { + int nProperties = 0; + Atom* pProperties = XListProperties( pDisplay, DefaultRootWindow( pDisplay ), &nProperties ); + if( pProperties && nProperties ) + { + for( int i = 0; i < nProperties; i++ ) + if( pProperties[ i ] == nAtom1 || + pProperties[ i ] == nAtom2 ) + { + ret = true; + break; + } + XFree( pProperties ); + } + } + if (ret) + return true; + + Atom nUTFAtom = XInternAtom( pDisplay, "UTF8_STRING", True ); + Atom nNetWMNameAtom = XInternAtom( pDisplay, "_NET_WM_NAME", True ); + if( nUTFAtom && nNetWMNameAtom ) + { + // another, more expensive check: search for a gnome-panel + ::Window aRoot, aParent, *pChildren = nullptr; + unsigned int nChildren = 0; + XQueryTree( pDisplay, DefaultRootWindow( pDisplay ), + &aRoot, &aParent, &pChildren, &nChildren ); + if( pChildren && nChildren ) + { + for( unsigned int i = 0; i < nChildren && ! ret; i++ ) + { + Atom nType = None; + int nFormat = 0; + unsigned long nItems = 0, nBytes = 0; + unsigned char* pProp = nullptr; + XGetWindowProperty( pDisplay, + pChildren[i], + nNetWMNameAtom, + 0, 8, + False, + nUTFAtom, + &nType, + &nFormat, + &nItems, + &nBytes, + &pProp ); + if( pProp && nType == nUTFAtom ) + { + OString aWMName( reinterpret_cast(pProp) ); + if ( + (aWMName.equalsIgnoreAsciiCase("gnome-shell")) || + (aWMName.equalsIgnoreAsciiCase("gnome-panel")) + ) + { + ret = true; + } + } + if( pProp ) + XFree( pProp ); + } + XFree( pChildren ); + } + } + + return ret; +} + +static bool is_plasma5_desktop() +{ + static const char* pFullVersion = getenv("KDE_FULL_SESSION"); + static const char* pSessionVersion = getenv("KDE_SESSION_VERSION"); + return pFullVersion && pSessionVersion && (0 == strcmp(pSessionVersion, "5")); +} + +static bool is_plasma6_desktop() +{ + static const char* pFullVersion = getenv("KDE_FULL_SESSION"); + static const char* pSessionVersion = getenv("KDE_SESSION_VERSION"); + return pFullVersion && pSessionVersion && (0 == strcmp(pSessionVersion, "6")); +} + +extern "C" +{ + +DESKTOP_DETECTOR_PUBLIC DesktopType get_desktop_environment() +{ + static const char *pOverride = getenv( "OOO_FORCE_DESKTOP" ); + + if ( pOverride && *pOverride ) + { + OString aOver( pOverride ); + + if ( aOver.equalsIgnoreAsciiCase( "lxqt" ) ) + return DESKTOP_LXQT; + if (aOver.equalsIgnoreAsciiCase("plasma5") || aOver.equalsIgnoreAsciiCase("plasma")) + return DESKTOP_PLASMA5; + if (aOver.equalsIgnoreAsciiCase("plasma6")) + return DESKTOP_PLASMA6; + if ( aOver.equalsIgnoreAsciiCase( "gnome" ) ) + return DESKTOP_GNOME; + if ( aOver.equalsIgnoreAsciiCase( "gnome-wayland" ) ) + return DESKTOP_GNOME; + if ( aOver.equalsIgnoreAsciiCase( "unity" ) ) + return DESKTOP_UNITY; + if ( aOver.equalsIgnoreAsciiCase( "xfce" ) ) + return DESKTOP_XFCE; + if ( aOver.equalsIgnoreAsciiCase( "mate" ) ) + return DESKTOP_MATE; + if ( aOver.equalsIgnoreAsciiCase( "none" ) ) + return DESKTOP_UNKNOWN; + } + + OUString plugin; + rtl::Bootstrap::get("SAL_USE_VCLPLUGIN", plugin); + + if (plugin == "svp") + return DESKTOP_NONE; + + const char *pDesktop = getenv( "XDG_CURRENT_DESKTOP" ); + if ( pDesktop ) + { + OString aCurrentDesktop( pDesktop, strlen( pDesktop ) ); + + //it may be separated by colon ( e.g. unity:unity7:ubuntu ) + std::vector aSplitCurrentDesktop = comphelper::string::split( + OStringToOUString( aCurrentDesktop, RTL_TEXTENCODING_UTF8), ':'); + for (const auto& rCurrentDesktopStr : aSplitCurrentDesktop) + { + if ( rCurrentDesktopStr.equalsIgnoreAsciiCase( "unity" ) ) + return DESKTOP_UNITY; + else if ( rCurrentDesktopStr.equalsIgnoreAsciiCase( "gnome" ) ) + return DESKTOP_GNOME; + else if ( rCurrentDesktopStr.equalsIgnoreAsciiCase( "lxqt" ) ) + return DESKTOP_LXQT; + } + } + + const char *pSession = getenv( "DESKTOP_SESSION" ); + OString aDesktopSession; + if ( pSession ) + aDesktopSession = OString( pSession, strlen( pSession ) ); + + // fast environment variable checks + if ( aDesktopSession.equalsIgnoreAsciiCase( "gnome" ) ) + return DESKTOP_GNOME; + else if ( aDesktopSession.equalsIgnoreAsciiCase( "gnome-wayland" ) ) + return DESKTOP_GNOME; + else if ( aDesktopSession.equalsIgnoreAsciiCase( "mate" ) ) + return DESKTOP_MATE; + else if ( aDesktopSession.equalsIgnoreAsciiCase( "xfce" ) ) + return DESKTOP_XFCE; + else if ( aDesktopSession.equalsIgnoreAsciiCase( "lxqt" ) ) + return DESKTOP_LXQT; + + if (is_plasma5_desktop()) + return DESKTOP_PLASMA5; + if (is_plasma6_desktop()) + return DESKTOP_PLASMA6; + + // tdf#121275 if we still can't tell, and WAYLAND_DISPLAY + // is set, default to gtk3 + const char* pWaylandStr = getenv("WAYLAND_DISPLAY"); + if (pWaylandStr && *pWaylandStr) + return DESKTOP_GNOME; + + // these guys can be slower, with X property fetches, + // round-trips etc. and so are done later. + + // get display to connect to + const char* pDisplayStr = getenv( "DISPLAY" ); + + int nParams = rtl_getAppCommandArgCount(); + OUString aParam; + OString aBParm; + for( int i = 0; i < nParams; i++ ) + { + rtl_getAppCommandArg( i, &aParam.pData ); + if( i < nParams-1 && (aParam == "-display" || aParam == "--display" ) ) + { + rtl_getAppCommandArg( i+1, &aParam.pData ); + aBParm = OUStringToOString( aParam, osl_getThreadTextEncoding() ); + pDisplayStr = aBParm.getStr(); + break; + } + } + + // no server at all + if( ! pDisplayStr || !*pDisplayStr ) + return DESKTOP_NONE; + + + /* #i92121# workaround deadlocks in the X11 implementation + */ + static const char* pNoXInitThreads = getenv( "SAL_NO_XINITTHREADS" ); + /* #i90094# + from now on we know that an X connection will be + established, so protect X against itself + */ + if( ! ( pNoXInitThreads && *pNoXInitThreads ) ) + XInitThreads(); + + Display* pDisplay = XOpenDisplay( pDisplayStr ); + if( pDisplay == nullptr ) + return DESKTOP_NONE; + + DesktopType ret; + if ( is_gnome_desktop( pDisplay ) ) + ret = DESKTOP_GNOME; + else + ret = DESKTOP_UNKNOWN; + + XCloseDisplay( pDisplay ); + + return ret; +} + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/X11_clipboard.cxx b/vcl/unx/generic/dtrans/X11_clipboard.cxx new file mode 100644 index 0000000000..f595de0d57 --- /dev/null +++ b/vcl/unx/generic/dtrans/X11_clipboard.cxx @@ -0,0 +1,231 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include "X11_clipboard.hxx" +#include "X11_transferable.hxx" +#include +#include +#include +#include + +#if OSL_DEBUG_LEVEL > 1 +#include +#endif + +using namespace com::sun::star::datatransfer; +using namespace com::sun::star::datatransfer::clipboard; +using namespace com::sun::star::lang; +using namespace com::sun::star::uno; +using namespace com::sun::star::awt; +using namespace cppu; +using namespace osl; +using namespace x11; + +X11Clipboard::X11Clipboard( SelectionManager& rManager, Atom aSelection ) : + ::cppu::WeakComponentImplHelper< + css::datatransfer::clipboard::XSystemClipboard, + css::lang::XServiceInfo + >( rManager.getMutex() ), + + m_xSelectionManager( &rManager ), + m_aSelection( aSelection ) +{ +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "creating instance of X11Clipboard (this=" + << this << ")."); +#endif +} + +css::uno::Reference +X11Clipboard::create( SelectionManager& rManager, Atom aSelection ) +{ + rtl::Reference cb(new X11Clipboard(rManager, aSelection)); + if( aSelection != None ) + { + rManager.registerHandler(aSelection, *cb); + } + else + { + rManager.registerHandler(XA_PRIMARY, *cb); + rManager.registerHandler(rManager.getAtom("CLIPBOARD"), *cb); + } + return cb; +} + +X11Clipboard::~X11Clipboard() +{ + MutexGuard aGuard( *Mutex::getGlobalMutex() ); + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "shutting down instance of X11Clipboard (this=" + << this + << ", Selection=\"" + << m_xSelectionManager->getString( m_aSelection ) + << "\")."); +#endif + + if( m_aSelection != None ) + m_xSelectionManager->deregisterHandler( m_aSelection ); + else + { + m_xSelectionManager->deregisterHandler( XA_PRIMARY ); + m_xSelectionManager->deregisterHandler( m_xSelectionManager->getAtom( "CLIPBOARD" ) ); + } +} + +void X11Clipboard::fireChangedContentsEvent() +{ + ClearableMutexGuard aGuard( m_xSelectionManager->getMutex() ); +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "X11Clipboard::fireChangedContentsEvent for " + << m_xSelectionManager->getString( m_aSelection ) + << " (" << m_aListeners.size() << " listeners)."); +#endif + ::std::vector< Reference< XClipboardListener > > listeners( m_aListeners ); + aGuard.clear(); + + ClipboardEvent aEvent(getXWeak(), m_aContents); + for (auto const& listener : listeners) + { + if( listener.is() ) + listener->changedContents(aEvent); + } +} + +void X11Clipboard::clearContents() +{ + ClearableMutexGuard aGuard(m_xSelectionManager->getMutex()); + // protect against deletion during outside call + Reference< XClipboard > xThis( static_cast(this)); + // copy member references on stack so they can be called + // without having the mutex + Reference< XClipboardOwner > xOwner( m_aOwner ); + Reference< XTransferable > xKeepAlive( m_aContents ); + // clear members + m_aOwner.clear(); + m_aContents.clear(); + + // release the mutex + aGuard.clear(); + + // inform previous owner of lost ownership + if ( xOwner.is() ) + xOwner->lostOwnership(xThis, m_aContents); +} + +Reference< XTransferable > SAL_CALL X11Clipboard::getContents() +{ + MutexGuard aGuard(m_xSelectionManager->getMutex()); + + if( ! m_aContents.is() ) + m_aContents = new X11Transferable( SelectionManager::get(), m_aSelection ); + return m_aContents; +} + +void SAL_CALL X11Clipboard::setContents( + const Reference< XTransferable >& xTrans, + const Reference< XClipboardOwner >& xClipboardOwner ) +{ + // remember old values for callbacks before setting the new ones. + ClearableMutexGuard aGuard(m_xSelectionManager->getMutex()); + + Reference< XClipboardOwner > oldOwner( m_aOwner ); + m_aOwner = xClipboardOwner; + + Reference< XTransferable > oldContents( m_aContents ); + m_aContents = xTrans; + + aGuard.clear(); + + // for now request ownership for both selections + if( m_aSelection != None ) + m_xSelectionManager->requestOwnership( m_aSelection ); + else + { + m_xSelectionManager->requestOwnership( XA_PRIMARY ); + m_xSelectionManager->requestOwnership( m_xSelectionManager->getAtom( "CLIPBOARD" ) ); + } + + // notify old owner on loss of ownership + if( oldOwner.is() ) + oldOwner->lostOwnership(static_cast < XClipboard * > (this), oldContents); + + // notify all listeners on content changes + fireChangedContentsEvent(); +} + +OUString SAL_CALL X11Clipboard::getName() +{ + return m_xSelectionManager->getString( m_aSelection ); +} + +sal_Int8 SAL_CALL X11Clipboard::getRenderingCapabilities() +{ + return RenderingCapabilities::Delayed; +} + +void SAL_CALL X11Clipboard::addClipboardListener( const Reference< XClipboardListener >& listener ) +{ + MutexGuard aGuard( m_xSelectionManager->getMutex() ); + m_aListeners.push_back( listener ); +} + +void SAL_CALL X11Clipboard::removeClipboardListener( const Reference< XClipboardListener >& listener ) +{ + MutexGuard aGuard( m_xSelectionManager->getMutex() ); + std::erase(m_aListeners, listener); +} + +Reference< XTransferable > X11Clipboard::getTransferable() +{ + return getContents(); +} + +void X11Clipboard::clearTransferable() +{ + clearContents(); +} + +void X11Clipboard::fireContentsChanged() +{ + fireChangedContentsEvent(); +} + +Reference< XInterface > X11Clipboard::getReference() noexcept +{ + return getXWeak(); +} + +OUString SAL_CALL X11Clipboard::getImplementationName( ) +{ + return X11_CLIPBOARD_IMPLEMENTATION_NAME; +} + +sal_Bool SAL_CALL X11Clipboard::supportsService( const OUString& ServiceName ) +{ + return cppu::supportsService(this, ServiceName); +} + +Sequence< OUString > SAL_CALL X11Clipboard::getSupportedServiceNames( ) +{ + return X11Clipboard_getSupportedServiceNames(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/X11_clipboard.hxx b/vcl/unx/generic/dtrans/X11_clipboard.hxx new file mode 100644 index 0000000000..4fd492154c --- /dev/null +++ b/vcl/unx/generic/dtrans/X11_clipboard.hxx @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include "X11_selection.hxx" + +#include +#include + +inline constexpr OUString X11_CLIPBOARD_IMPLEMENTATION_NAME = u"com.sun.star.datatransfer.X11ClipboardSupport"_ustr; + +namespace x11 { + + class X11Clipboard : + public ::cppu::WeakComponentImplHelper < + css::datatransfer::clipboard::XSystemClipboard, + css::lang::XServiceInfo + >, + public SelectionAdaptor + { + css::uno::Reference< css::datatransfer::XTransferable > m_aContents; + css::uno::Reference< css::datatransfer::clipboard::XClipboardOwner > m_aOwner; + + rtl::Reference m_xSelectionManager; + ::std::vector< css::uno::Reference< css::datatransfer::clipboard::XClipboardListener > > m_aListeners; + Atom m_aSelection; + + X11Clipboard( SelectionManager& rManager, Atom aSelection ); + + friend class SelectionManager; + + void fireChangedContentsEvent(); + void clearContents(); + + public: + + static css::uno::Reference + create( SelectionManager& rManager, Atom aSelection ); + + virtual ~X11Clipboard() override; + + /* + * XServiceInfo + */ + + virtual OUString SAL_CALL getImplementationName( ) override; + + virtual sal_Bool SAL_CALL supportsService( const OUString& ServiceName ) override; + + virtual css::uno::Sequence< OUString > SAL_CALL getSupportedServiceNames( ) override; + + /* + * XClipboard + */ + + virtual css::uno::Reference< css::datatransfer::XTransferable > SAL_CALL getContents() override; + + virtual void SAL_CALL setContents( + const css::uno::Reference< css::datatransfer::XTransferable >& xTrans, + const css::uno::Reference< css::datatransfer::clipboard::XClipboardOwner >& xClipboardOwner ) override; + + virtual OUString SAL_CALL getName() override; + + /* + * XClipboardEx + */ + + virtual sal_Int8 SAL_CALL getRenderingCapabilities() override; + + /* + * XClipboardNotifier + */ + virtual void SAL_CALL addClipboardListener( + const css::uno::Reference< css::datatransfer::clipboard::XClipboardListener >& listener ) override; + + virtual void SAL_CALL removeClipboardListener( + const css::uno::Reference< css::datatransfer::clipboard::XClipboardListener >& listener ) override; + + /* + * SelectionAdaptor + */ + virtual css::uno::Reference< css::datatransfer::XTransferable > getTransferable() override; + virtual void clearTransferable() override; + virtual void fireContentsChanged() override; + virtual css::uno::Reference< css::uno::XInterface > getReference() noexcept override; + }; + + css::uno::Sequence< OUString > X11Clipboard_getSupportedServiceNames(); + css::uno::Reference< css::uno::XInterface > SAL_CALL X11Clipboard_createInstance( + const css::uno::Reference< css::lang::XMultiServiceFactory > & xMultiServiceFactory); + +} // namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/X11_dndcontext.cxx b/vcl/unx/generic/dtrans/X11_dndcontext.cxx new file mode 100644 index 0000000000..638c47387d --- /dev/null +++ b/vcl/unx/generic/dtrans/X11_dndcontext.cxx @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "X11_dndcontext.hxx" +#include "X11_selection.hxx" + +using namespace cppu; +using namespace x11; + +/* + * DropTargetDropContext + */ + +DropTargetDropContext::DropTargetDropContext( + ::Window aDropWindow, + SelectionManager& rManager ) : + m_aDropWindow( aDropWindow ), + m_xManager( &rManager ) +{ +} + +DropTargetDropContext::~DropTargetDropContext() +{ +} + +void DropTargetDropContext::acceptDrop( sal_Int8 dragOperation ) +{ + m_xManager->accept( dragOperation, m_aDropWindow ); +} + +void DropTargetDropContext::rejectDrop() +{ + m_xManager->reject( m_aDropWindow ); +} + +void DropTargetDropContext::dropComplete( sal_Bool success ) +{ + m_xManager->dropComplete( success, m_aDropWindow ); +} + +/* + * DropTargetDragContext + */ + +DropTargetDragContext::DropTargetDragContext( + ::Window aDropWindow, + SelectionManager& rManager ) : + m_aDropWindow( aDropWindow ), + m_xManager( &rManager ) +{ +} + +DropTargetDragContext::~DropTargetDragContext() +{ +} + +void DropTargetDragContext::acceptDrag( sal_Int8 dragOperation ) +{ + m_xManager->accept( dragOperation, m_aDropWindow ); +} + +void DropTargetDragContext::rejectDrag() +{ + m_xManager->reject( m_aDropWindow ); +} + +/* + * DragSourceContext + */ + +DragSourceContext::DragSourceContext( + ::Window aDropWindow, + SelectionManager& rManager ) : + m_aDropWindow( aDropWindow ), + m_xManager( &rManager ) +{ +} + +DragSourceContext::~DragSourceContext() +{ +} + +sal_Int32 DragSourceContext::getCurrentCursor() +{ + return m_xManager->getCurrentCursor(); +} + +void DragSourceContext::setCursor( sal_Int32 cursorId ) +{ + m_xManager->setCursor( cursorId, m_aDropWindow ); +} + +void DragSourceContext::setImage( sal_Int32 ) +{ +} + +void DragSourceContext::transferablesFlavorsChanged() +{ + m_xManager->transferablesFlavorsChanged(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/X11_dndcontext.hxx b/vcl/unx/generic/dtrans/X11_dndcontext.hxx new file mode 100644 index 0000000000..71283ea641 --- /dev/null +++ b/vcl/unx/generic/dtrans/X11_dndcontext.hxx @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace x11 { + + class SelectionManager; + + class DropTargetDropContext : + public ::cppu::WeakImplHelper + { + ::Window m_aDropWindow; + rtl::Reference m_xManager; + public: + DropTargetDropContext( ::Window, SelectionManager& ); + virtual ~DropTargetDropContext() override; + + // XDropTargetDropContext + virtual void SAL_CALL acceptDrop( sal_Int8 dragOperation ) override; + virtual void SAL_CALL rejectDrop() override; + virtual void SAL_CALL dropComplete( sal_Bool success ) override; + }; + + class DropTargetDragContext : + public ::cppu::WeakImplHelper + { + ::Window m_aDropWindow; + rtl::Reference m_xManager; + public: + DropTargetDragContext( ::Window, SelectionManager& ); + virtual ~DropTargetDragContext() override; + + // XDropTargetDragContext + virtual void SAL_CALL acceptDrag( sal_Int8 dragOperation ) override; + virtual void SAL_CALL rejectDrag() override; + }; + + class DragSourceContext : + public ::cppu::WeakImplHelper + { + ::Window m_aDropWindow; + rtl::Reference m_xManager; + public: + DragSourceContext( ::Window, SelectionManager& ); + virtual ~DragSourceContext() override; + + // XDragSourceContext + virtual sal_Int32 SAL_CALL getCurrentCursor() override; + virtual void SAL_CALL setCursor( sal_Int32 cursorId ) override; + virtual void SAL_CALL setImage( sal_Int32 imageId ) override; + virtual void SAL_CALL transferablesFlavorsChanged() override; + }; +} // namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/X11_droptarget.cxx b/vcl/unx/generic/dtrans/X11_droptarget.cxx new file mode 100644 index 0000000000..e026ad1ab1 --- /dev/null +++ b/vcl/unx/generic/dtrans/X11_droptarget.cxx @@ -0,0 +1,177 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include "X11_selection.hxx" + +using namespace x11; +using namespace com::sun::star::uno; +using namespace com::sun::star::lang; +using namespace com::sun::star::awt; +using namespace com::sun::star::datatransfer; +using namespace com::sun::star::datatransfer::dnd; + +DropTarget::DropTarget() : + ::cppu::WeakComponentImplHelper< + XDropTarget, + XInitialization, + XServiceInfo + >( m_aMutex ), + m_bActive( false ), + m_nDefaultActions( 0 ), + m_aTargetWindow( None ) +{ +} + +DropTarget::~DropTarget() +{ + if( m_xSelectionManager.is() ) + m_xSelectionManager->deregisterDropTarget( m_aTargetWindow ); +} + +void DropTarget::initialize( const Sequence< Any >& arguments ) +{ + if( arguments.getLength() <= 1 ) + return; + + OUString aDisplayName; + Reference< XDisplayConnection > xConn; + arguments.getConstArray()[0] >>= xConn; + if( xConn.is() ) + { + Any aIdentifier; + aIdentifier >>= aDisplayName; + } + + m_xSelectionManager = &SelectionManager::get( aDisplayName ); + m_xSelectionManager->initialize( arguments ); + + if( m_xSelectionManager->getDisplay() ) // #136582# sanity check + { + sal_IntPtr aWindow = None; + arguments.getConstArray()[1] >>= aWindow; + m_xSelectionManager->registerDropTarget( aWindow, this ); + m_aTargetWindow = aWindow; + m_bActive = true; + } +} + +void DropTarget::addDropTargetListener( const Reference< XDropTargetListener >& xListener ) +{ + ::osl::Guard< ::osl::Mutex > aGuard( m_aMutex ); + + m_aListeners.push_back( xListener ); +} + +void DropTarget::removeDropTargetListener( const Reference< XDropTargetListener >& xListener ) +{ + ::osl::Guard< ::osl::Mutex > aGuard( m_aMutex ); + + std::erase(m_aListeners, xListener); +} + +sal_Bool DropTarget::isActive() +{ + return m_bActive; +} + +void DropTarget::setActive( sal_Bool active ) +{ + ::osl::Guard< ::osl::Mutex > aGuard( m_aMutex ); + + m_bActive = active; +} + +sal_Int8 DropTarget::getDefaultActions() +{ + return m_nDefaultActions; +} + +void DropTarget::setDefaultActions( sal_Int8 actions ) +{ + ::osl::Guard< ::osl::Mutex > aGuard( m_aMutex ); + + m_nDefaultActions = actions; +} + +void DropTarget::drop( const DropTargetDropEvent& dtde ) noexcept +{ + osl::ClearableGuard< ::osl::Mutex > aGuard( m_aMutex ); + std::vector< Reference< XDropTargetListener > > aListeners( m_aListeners ); + aGuard.clear(); + + for (auto const& listener : aListeners) + { + listener->drop(dtde); + } +} + +void DropTarget::dragEnter( const DropTargetDragEnterEvent& dtde ) noexcept +{ + osl::ClearableGuard< ::osl::Mutex > aGuard( m_aMutex ); + std::vector< Reference< XDropTargetListener > > aListeners( m_aListeners ); + aGuard.clear(); + + for (auto const& listener : aListeners) + { + listener->dragEnter(dtde); + } +} + +void DropTarget::dragExit( const DropTargetEvent& dte ) noexcept +{ + osl::ClearableGuard< ::osl::Mutex > aGuard( m_aMutex ); + std::vector< Reference< XDropTargetListener > > aListeners( m_aListeners ); + aGuard.clear(); + + for (auto const& listener : aListeners) + { + listener->dragExit(dte); + } +} + +void DropTarget::dragOver( const DropTargetDragEvent& dtde ) noexcept +{ + osl::ClearableGuard< ::osl::Mutex > aGuard( m_aMutex ); + std::vector< Reference< XDropTargetListener > > aListeners( m_aListeners ); + aGuard.clear(); + + for (auto const& listener : aListeners) + { + listener->dragOver(dtde); + } +} + +// XServiceInfo +OUString DropTarget::getImplementationName() +{ + return "com.sun.star.datatransfer.dnd.XdndDropTarget"; +} + +sal_Bool DropTarget::supportsService( const OUString& ServiceName ) +{ + return cppu::supportsService(this, ServiceName); +} + +Sequence< OUString > DropTarget::getSupportedServiceNames() +{ + return Xdnd_dropTarget_getSupportedServiceNames(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/X11_selection.cxx b/vcl/unx/generic/dtrans/X11_selection.cxx new file mode 100644 index 0000000000..823b77982f --- /dev/null +++ b/vcl/unx/generic/dtrans/X11_selection.cxx @@ -0,0 +1,4157 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include + +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +#include "X11_selection.hxx" +#include "X11_clipboard.hxx" +#include "X11_transferable.hxx" +#include "X11_dndcontext.hxx" +#include "bmp.hxx" + +#include +#include + +// pointer bitmaps +#include "copydata_curs.h" +#include "copydata_mask.h" +#include "movedata_curs.h" +#include "movedata_mask.h" +#include "linkdata_curs.h" +#include "linkdata_mask.h" +#include "nodrop_curs.h" +#include "nodrop_mask.h" +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +constexpr auto DRAG_EVENT_MASK = ButtonPressMask | + ButtonReleaseMask | + PointerMotionMask | + EnterWindowMask | + LeaveWindowMask; + +using namespace com::sun::star::datatransfer; +using namespace com::sun::star::datatransfer::dnd; +using namespace com::sun::star::lang; +using namespace com::sun::star::awt; +using namespace com::sun::star::uno; +using namespace com::sun::star::frame; +using namespace cppu; + +using namespace x11; + +// stubs to satisfy solaris compiler's rather rigid linking warning +extern "C" +{ + static void call_SelectionManager_run( void * pMgr ) + { + SelectionManager::run( pMgr ); + } + + static void call_SelectionManager_runDragExecute( void * pMgr ) + { + osl_setThreadName("SelectionManager::runDragExecute()"); + SelectionManager::runDragExecute( pMgr ); + } +} + +const tools::Long nXdndProtocolRevision = 5; + +namespace { + +// mapping between mime types (or what the office thinks of mime types) +// and X convention types +struct NativeTypeEntry +{ + Atom nAtom; + const char* pType; // Mime encoding on our side + const char* pNativeType; // string corresponding to nAtom for the case of nAtom being uninitialized + int nFormat; // the corresponding format +}; + +} + +// the convention for Xdnd is mime types as specified by the corresponding +// RFC's with the addition that text/plain without charset tag contains iso8859-1 +// sadly some applications (e.g. gtk) do not honor the mimetype only rule, +// so for compatibility add UTF8_STRING +static NativeTypeEntry aXdndConversionTab[] = +{ + { 0, "text/plain;charset=iso8859-1", "text/plain", 8 }, + { 0, "text/plain;charset=utf-8", "UTF8_STRING", 8 } +}; + +// for clipboard and primary selections there is only a convention for text +// that the encoding name of the text is taken as type in all capitalized letters +static NativeTypeEntry aNativeConversionTab[] = +{ + { 0, "text/plain;charset=utf-16", "ISO10646-1", 16 }, + { 0, "text/plain;charset=utf-8", "UTF8_STRING", 8 }, + { 0, "text/plain;charset=utf-8", "UTF-8", 8 }, + { 0, "text/plain;charset=utf-8", "text/plain;charset=UTF-8", 8 }, + // ISO encodings + { 0, "text/plain;charset=iso8859-2", "ISO8859-2", 8 }, + { 0, "text/plain;charset=iso8859-3", "ISO8859-3", 8 }, + { 0, "text/plain;charset=iso8859-4", "ISO8859-4", 8 }, + { 0, "text/plain;charset=iso8859-5", "ISO8859-5", 8 }, + { 0, "text/plain;charset=iso8859-6", "ISO8859-6", 8 }, + { 0, "text/plain;charset=iso8859-7", "ISO8859-7", 8 }, + { 0, "text/plain;charset=iso8859-8", "ISO8859-8", 8 }, + { 0, "text/plain;charset=iso8859-9", "ISO8859-9", 8 }, + { 0, "text/plain;charset=iso8859-10", "ISO8859-10", 8 }, + { 0, "text/plain;charset=iso8859-13", "ISO8859-13", 8 }, + { 0, "text/plain;charset=iso8859-14", "ISO8859-14", 8 }, + { 0, "text/plain;charset=iso8859-15", "ISO8859-15", 8 }, + // asian encodings + { 0, "text/plain;charset=jisx0201.1976-0", "JISX0201.1976-0", 8 }, + { 0, "text/plain;charset=jisx0208.1983-0", "JISX0208.1983-0", 8 }, + { 0, "text/plain;charset=jisx0208.1990-0", "JISX0208.1990-0", 8 }, + { 0, "text/plain;charset=jisx0212.1990-0", "JISX0212.1990-0", 8 }, + { 0, "text/plain;charset=gb2312.1980-0", "GB2312.1980-0", 8 }, + { 0, "text/plain;charset=ksc5601.1992-0", "KSC5601.1992-0", 8 }, + // eastern european encodings + { 0, "text/plain;charset=koi8-r", "KOI8-R", 8 }, + { 0, "text/plain;charset=koi8-u", "KOI8-U", 8 }, + // String (== iso8859-1) + { XA_STRING, "text/plain;charset=iso8859-1", "STRING", 8 }, + // special for compound text + { 0, "text/plain;charset=compound_text", "COMPOUND_TEXT", 8 }, + + // PIXMAP + { XA_PIXMAP, "image/bmp", "PIXMAP", 32 } +}; + +rtl_TextEncoding x11::getTextPlainEncoding( const OUString& rMimeType ) +{ + rtl_TextEncoding aEncoding = RTL_TEXTENCODING_DONTKNOW; + OUString aMimeType( rMimeType.toAsciiLowerCase() ); + sal_Int32 nIndex = 0; + if( o3tl::getToken(aMimeType, 0, ';', nIndex ) == u"text/plain" ) + { + if( aMimeType.getLength() == 10 ) // only "text/plain" + aEncoding = RTL_TEXTENCODING_ISO_8859_1; + else + { + while( nIndex != -1 ) + { + OUString aToken = aMimeType.getToken( 0, ';', nIndex ); + sal_Int32 nPos = 0; + if( o3tl::getToken(aToken, 0, '=', nPos ) == u"charset" ) + { + OString aEncToken = OUStringToOString( o3tl::getToken(aToken, 0, '=', nPos ), RTL_TEXTENCODING_ISO_8859_1 ); + aEncoding = rtl_getTextEncodingFromUnixCharset( aEncToken.getStr() ); + if( aEncoding == RTL_TEXTENCODING_DONTKNOW ) + { + if( aEncToken.equalsIgnoreAsciiCase( "utf-8" ) ) + aEncoding = RTL_TEXTENCODING_UTF8; + } + if( aEncoding != RTL_TEXTENCODING_DONTKNOW ) + break; + } + } + } + } +#if OSL_DEBUG_LEVEL > 1 + SAL_WARN_IF(aEncoding == RTL_TEXTENCODING_DONTKNOW, + "vcl.unx.dtrans", "getTextPlainEncoding( " + << rMimeType << " ) failed."); +#endif + return aEncoding; +} + +std::unordered_map< OUString, SelectionManager* >& SelectionManager::getInstances() +{ + static std::unordered_map< OUString, SelectionManager* > aInstances; + return aInstances; +} + +SelectionManager::SelectionManager() : + m_nIncrementalThreshold( 15*1024 ), + m_pDisplay( nullptr ), + m_aThread( nullptr ), + m_aDragExecuteThread( nullptr ), + m_aWindow( None ), + m_nSelectionTimeout( 0 ), + m_nSelectionTimestamp( CurrentTime ), + m_bDropEnterSent( true ), + m_aCurrentDropWindow( None ), + m_nDropTime( None ), + m_nLastDropAction( 0 ), + m_nLastX( 0 ), + m_nLastY( 0 ), + m_bDropWaitingForCompletion( false ), + m_aDropWindow( None ), + m_aDropProxy( None ), + m_aDragSourceWindow( None ), + m_nLastDragX( 0 ), + m_nLastDragY( 0 ), + m_nNoPosX( 0 ), + m_nNoPosY( 0 ), + m_nNoPosWidth( 0 ), + m_nNoPosHeight( 0 ), + m_nDragButton( 0 ), + m_nUserDragAction( 0 ), + m_nTargetAcceptAction( 0 ), + m_nSourceActions( 0 ), + m_bLastDropAccepted( false ), + m_bDropSuccess( false ), + m_bDropSent( false ), + m_nDropTimeout( 0 ), + m_bWaitingForPrimaryConversion( false ), + m_aMoveCursor( None ), + m_aCopyCursor( None ), + m_aLinkCursor( None ), + m_aNoneCursor( None ), + m_aCurrentCursor( None ), + m_nCurrentProtocolVersion( nXdndProtocolRevision ), + m_nTARGETSAtom( None ), + m_nTIMESTAMPAtom( None ), + m_nTEXTAtom( None ), + m_nINCRAtom( None ), + m_nCOMPOUNDAtom( None ), + m_nMULTIPLEAtom( None ), + m_nImageBmpAtom( None ), + m_nXdndAware( None ), + m_nXdndEnter( None ), + m_nXdndLeave( None ), + m_nXdndPosition( None ), + m_nXdndStatus( None ), + m_nXdndDrop( None ), + m_nXdndFinished( None ), + m_nXdndSelection( None ), + m_nXdndTypeList( None ), + m_nXdndProxy( None ), + m_nXdndActionCopy( None ), + m_nXdndActionMove( None ), + m_nXdndActionLink( None ), + m_nXdndActionAsk( None ), + m_bShutDown( false ) +{ + memset(&m_aDropEnterEvent, 0, sizeof(m_aDropEnterEvent)); + m_EndThreadPipe[0] = 0; + m_EndThreadPipe[1] = 0; + m_aDragRunning.reset(); +} + +Cursor SelectionManager::createCursor( const unsigned char* pPointerData, const unsigned char* pMaskData, int width, int height, int hotX, int hotY ) +{ + Pixmap aPointer; + Pixmap aMask; + XColor aBlack, aWhite; + + aBlack.pixel = BlackPixel( m_pDisplay, 0 ); + aBlack.red = aBlack.green = aBlack.blue = 0; + aBlack.flags = DoRed | DoGreen | DoBlue; + + aWhite.pixel = WhitePixel( m_pDisplay, 0 ); + aWhite.red = aWhite.green = aWhite.blue = 0xffff; + aWhite.flags = DoRed | DoGreen | DoBlue; + + aPointer = + XCreateBitmapFromData( m_pDisplay, + m_aWindow, + reinterpret_cast(pPointerData), + width, + height ); + aMask + = XCreateBitmapFromData( m_pDisplay, + m_aWindow, + reinterpret_cast(pMaskData), + width, + height ); + Cursor aCursor = + XCreatePixmapCursor( m_pDisplay, aPointer, aMask, + &aBlack, &aWhite, + hotX, + hotY ); + XFreePixmap( m_pDisplay, aPointer ); + XFreePixmap( m_pDisplay, aMask ); + + return aCursor; +} + +void SelectionManager::initialize( const Sequence< Any >& arguments ) +{ + osl::MutexGuard aGuard(m_aMutex); + + if( ! m_xDisplayConnection.is() ) + { + /* + * first argument must be a css::awt::XDisplayConnection + * from this we will get the XEvents of the vcl event loop by + * registering us as XEventHandler on it. + * + * implementor's note: + * FIXME: + * finally the clipboard and XDND service is back in the module it belongs + * now cleanup and sharing of resources with the normal vcl event loop + * needs to be added. The display used would be that of the normal event loop + * and synchronization should be done via the SolarMutex. + */ + if( arguments.hasElements() ) + arguments.getConstArray()[0] >>= m_xDisplayConnection; + if( ! m_xDisplayConnection.is() ) + { + } + else + m_xDisplayConnection->addEventHandler( Any(), this, ~0 ); + } + + if( m_pDisplay ) + return; + + OUString aUDisplay; + if( m_xDisplayConnection.is() ) + { + Any aIdentifier = m_xDisplayConnection->getIdentifier(); + aIdentifier >>= aUDisplay; + } + + OString aDisplayName( OUStringToOString( aUDisplay, RTL_TEXTENCODING_ISO_8859_1 ) ); + + m_pDisplay = XOpenDisplay( aDisplayName.isEmpty() ? nullptr : aDisplayName.getStr()); + + if( !m_pDisplay ) + return; + +#ifdef SYNCHRONIZE + XSynchronize( m_pDisplay, True ); +#endif + // special targets + m_nTARGETSAtom = getAtom( "TARGETS" ); + m_nTIMESTAMPAtom = getAtom( "TIMESTAMP" ); + m_nTEXTAtom = getAtom( "TEXT" ); + m_nINCRAtom = getAtom( "INCR" ); + m_nCOMPOUNDAtom = getAtom( "COMPOUND_TEXT" ); + m_nMULTIPLEAtom = getAtom( "MULTIPLE" ); + m_nImageBmpAtom = getAtom( "image/bmp" ); + + // Atoms for Xdnd protocol + m_nXdndAware = getAtom( "XdndAware" ); + m_nXdndEnter = getAtom( "XdndEnter" ); + m_nXdndLeave = getAtom( "XdndLeave" ); + m_nXdndPosition = getAtom( "XdndPosition" ); + m_nXdndStatus = getAtom( "XdndStatus" ); + m_nXdndDrop = getAtom( "XdndDrop" ); + m_nXdndFinished = getAtom( "XdndFinished" ); + m_nXdndSelection = getAtom( "XdndSelection" ); + m_nXdndTypeList = getAtom( "XdndTypeList" ); + m_nXdndProxy = getAtom( "XdndProxy" ); + m_nXdndActionCopy = getAtom( "XdndActionCopy" ); + m_nXdndActionMove = getAtom( "XdndActionMove" ); + m_nXdndActionLink = getAtom( "XdndActionLink" ); + m_nXdndActionAsk = getAtom( "XdndActionAsk" ); + + // initialize map with member none + m_aAtomToString[ 0 ]= "None"; + m_aAtomToString[ XA_PRIMARY ] = "PRIMARY"; + + // create a (invisible) message window + m_aWindow = XCreateSimpleWindow( m_pDisplay, DefaultRootWindow( m_pDisplay ), + 10, 10, 10, 10, 0, 0, 1 ); + + // initialize threshold for incremental transfers + // ICCCM says it should be smaller that the max request size + // which in turn is guaranteed to be at least 16k bytes + m_nIncrementalThreshold = XMaxRequestSize( m_pDisplay ) - 1024; + + if( !m_aWindow ) + return; + + // initialize default cursors + m_aMoveCursor = createCursor( movedata_curs_bits, + movedata_mask_bits, + movedata_curs_width, + movedata_curs_height, + movedata_curs_x_hot, + movedata_curs_y_hot ); + m_aCopyCursor = createCursor( copydata_curs_bits, + copydata_mask_bits, + copydata_curs_width, + copydata_curs_height, + copydata_curs_x_hot, + copydata_curs_y_hot ); + m_aLinkCursor = createCursor( linkdata_curs_bits, + linkdata_mask_bits, + linkdata_curs_width, + linkdata_curs_height, + linkdata_curs_x_hot, + linkdata_curs_y_hot ); + m_aNoneCursor = createCursor( nodrop_curs_bits, + nodrop_mask_bits, + nodrop_curs_width, + nodrop_curs_height, + nodrop_curs_x_hot, + nodrop_curs_y_hot ); + + // just interested in SelectionClear/Notify/Request and PropertyChange + XSelectInput( m_pDisplay, m_aWindow, PropertyChangeMask ); + // create the transferable for Drag operations + m_xDropTransferable = new X11Transferable( *this, m_nXdndSelection ); + registerHandler( m_nXdndSelection, *this ); + + m_aThread = osl_createSuspendedThread( call_SelectionManager_run, this ); + if( m_aThread ) + osl_resumeThread( m_aThread ); +#if OSL_DEBUG_LEVEL > 1 + else + SAL_WARN("vcl.unx.dtrans", "SelectionManager::initialize: " + << "creation of dispatch thread failed !."); +#endif + + if (pipe(m_EndThreadPipe) != 0) { +#if OSL_DEBUG_LEVEL > 1 + SAL_WARN("vcl.unx.dtrans", "Failed to create endThreadPipe."); +#endif + m_EndThreadPipe[0] = m_EndThreadPipe[1] = 0; + } +} + +SelectionManager::~SelectionManager() +{ +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "SelectionManager::~SelectionManager (" + << (m_pDisplay ? DisplayString(m_pDisplay) : "no display") + << ")."); +#endif + { + osl::MutexGuard aGuard( *osl::Mutex::getGlobalMutex() ); + + auto it = std::find_if(getInstances().begin(), getInstances().end(), + [&](const std::pair< OUString, SelectionManager* >& rInstance) { return rInstance.second == this; }); + if( it != getInstances().end() ) + getInstances().erase( it ); + } + + if( m_aThread ) + { + osl_terminateThread( m_aThread ); + osl_joinWithThread( m_aThread ); + osl_destroyThread( m_aThread ); + } + + if( m_aDragExecuteThread ) + { + osl_terminateThread( m_aDragExecuteThread ); + osl_joinWithThread( m_aDragExecuteThread ); + m_aDragExecuteThread = nullptr; + // thread handle is freed in dragDoDispatch() + } + + osl::MutexGuard aGuard(m_aMutex); + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "shutting down SelectionManager."); +#endif + + if( !m_pDisplay ) + return; + + deregisterHandler( m_nXdndSelection ); + // destroy message window + if( m_aWindow ) + XDestroyWindow( m_pDisplay, m_aWindow ); + // release cursors + if (m_aMoveCursor != None) + XFreeCursor(m_pDisplay, m_aMoveCursor); + if (m_aCopyCursor != None) + XFreeCursor(m_pDisplay, m_aCopyCursor); + if (m_aLinkCursor != None) + XFreeCursor(m_pDisplay, m_aLinkCursor); + if (m_aNoneCursor != None) + XFreeCursor(m_pDisplay, m_aNoneCursor); + + // paranoia setting, the drag thread should have + // done that already + XUngrabPointer( m_pDisplay, CurrentTime ); + XUngrabKeyboard( m_pDisplay, CurrentTime ); + + XCloseDisplay( m_pDisplay ); +} + +SelectionAdaptor* SelectionManager::getAdaptor( Atom selection ) +{ + std::unordered_map< Atom, Selection* >::iterator it = + m_aSelections.find( selection ); + return it != m_aSelections.end() ? it->second->m_pAdaptor : nullptr; +} + +OUString SelectionManager::convertFromCompound( const char* pText, int nLen ) +{ + osl::MutexGuard aGuard( m_aMutex ); + OUStringBuffer aRet; + if( nLen < 0 ) + nLen = strlen( pText ); + + char** pTextList = nullptr; + int nTexts = 0; + + XTextProperty aProp; + aProp.value = reinterpret_cast(const_cast(pText)); + aProp.encoding = m_nCOMPOUNDAtom; + aProp.format = 8; + aProp.nitems = nLen; + XmbTextPropertyToTextList( m_pDisplay, + &aProp, + &pTextList, + &nTexts ); + rtl_TextEncoding aEncoding = osl_getThreadTextEncoding(); + for( int i = 0; i < nTexts; i++ ) + aRet.append(OStringToOUString( pTextList[i], aEncoding )); + + if( pTextList ) + XFreeStringList( pTextList ); + + return aRet.makeStringAndClear(); +} + +OString SelectionManager::convertToCompound( const OUString& rText ) +{ + osl::MutexGuard aGuard( m_aMutex ); + XTextProperty aProp; + aProp.value = nullptr; + aProp.encoding = XA_STRING; + aProp.format = 8; + aProp.nitems = 0; + + OString aRet( rText.getStr(), rText.getLength(), osl_getThreadTextEncoding() ); + char* pT = const_cast(aRet.getStr()); + + XmbTextListToTextProperty( m_pDisplay, + &pT, + 1, + XCompoundTextStyle, + &aProp ); + if( aProp.value ) + { + aRet = reinterpret_cast(aProp.value); + XFree( aProp.value ); +#ifdef __sun + /* + * for currently unknown reasons XmbTextListToTextProperty on Solaris returns + * no data in ISO8859-n encodings (at least for n = 1, 15) + * in these encodings the directly converted text does the + * trick, also. + */ + if( aRet.isEmpty() && !rText.isEmpty() ) + aRet = OUStringToOString( rText, osl_getThreadTextEncoding() ); +#endif + } + else + aRet.clear(); + + return aRet; +} + +bool SelectionManager::convertData( + const css::uno::Reference< XTransferable >& xTransferable, + Atom nType, + Atom nSelection, + int& rFormat, + Sequence< sal_Int8 >& rData ) +{ + bool bSuccess = false; + + if( ! xTransferable.is() ) + return bSuccess; + + try + { + + DataFlavor aFlavor; + aFlavor.MimeType = convertTypeFromNative( nType, nSelection, rFormat ); + + sal_Int32 nIndex = 0; + if( o3tl::getToken(aFlavor.MimeType, 0, ';', nIndex ) == u"text/plain" ) + { + if( o3tl::getToken(aFlavor.MimeType, 0, ';', nIndex ) == u"charset=utf-16" ) + aFlavor.DataType = cppu::UnoType::get(); + else + aFlavor.DataType = cppu::UnoType>::get(); + } + else + aFlavor.DataType = cppu::UnoType>::get(); + + if( xTransferable->isDataFlavorSupported( aFlavor ) ) + { + Any aValue( xTransferable->getTransferData( aFlavor ) ); + if( aValue.getValueTypeClass() == TypeClass_STRING ) + { + OUString aString; + aValue >>= aString; + rData = Sequence< sal_Int8 >( reinterpret_cast(aString.getStr()), aString.getLength() * sizeof( sal_Unicode ) ); + bSuccess = true; + } + else if( aValue.getValueType() == cppu::UnoType>::get() ) + { + aValue >>= rData; + bSuccess = true; + } + } + else if( aFlavor.MimeType.startsWith("text/plain") ) + { + rtl_TextEncoding aEncoding = RTL_TEXTENCODING_DONTKNOW; + bool bCompoundText = false; + if( nType == m_nCOMPOUNDAtom ) + bCompoundText = true; + else + aEncoding = getTextPlainEncoding( aFlavor.MimeType ); + if( aEncoding != RTL_TEXTENCODING_DONTKNOW || bCompoundText ) + { + aFlavor.MimeType = "text/plain;charset=utf-16"; + aFlavor.DataType = cppu::UnoType::get(); + if( xTransferable->isDataFlavorSupported( aFlavor ) ) + { + Any aValue( xTransferable->getTransferData( aFlavor ) ); + OUString aString; + aValue >>= aString; + OString aByteString( bCompoundText ? convertToCompound( aString ) : OUStringToOString( aString, aEncoding ) ); + rData = Sequence< sal_Int8 >( reinterpret_cast(aByteString.getStr()), aByteString.getLength() * sizeof( char ) ); + bSuccess = true; + } + } + } + } + // various exceptions possible ... which all lead to a failed conversion + // so simplify here to a catch all + catch(...) + { + } + + return bSuccess; +} + +SelectionManager& SelectionManager::get( const OUString& rDisplayName ) +{ + osl::MutexGuard aGuard( *osl::Mutex::getGlobalMutex() ); + + OUString aDisplayName( rDisplayName ); + if( aDisplayName.isEmpty() ) + if (auto const env = getenv( "DISPLAY" )) { + aDisplayName = OStringToOUString( env, RTL_TEXTENCODING_ISO_8859_1 ); + } + SelectionManager* pInstance = nullptr; + + std::unordered_map< OUString, SelectionManager* >::iterator it = getInstances().find( aDisplayName ); + if( it != getInstances().end() ) + pInstance = it->second; + else pInstance = getInstances()[ aDisplayName ] = new SelectionManager(); + + return *pInstance; +} + +OUString SelectionManager::getString( Atom aAtom ) +{ + osl::MutexGuard aGuard(m_aMutex); + + if( m_aAtomToString.find( aAtom ) == m_aAtomToString.end() ) + { + char* pAtom = m_pDisplay ? XGetAtomName( m_pDisplay, aAtom ) : nullptr; + if( ! pAtom ) + return OUString(); + OUString aString( OStringToOUString( pAtom, RTL_TEXTENCODING_ISO_8859_1 ) ); + XFree( pAtom ); + m_aStringToAtom[ aString ] = aAtom; + m_aAtomToString[ aAtom ] = aString; + } + return m_aAtomToString[ aAtom ]; +} + +Atom SelectionManager::getAtom( const OUString& rString ) +{ + osl::MutexGuard aGuard(m_aMutex); + + if( m_aStringToAtom.find( rString ) == m_aStringToAtom.end() ) + { + static Atom nNoDisplayAtoms = 1; + Atom aAtom = m_pDisplay ? XInternAtom( m_pDisplay, OUStringToOString( rString, RTL_TEXTENCODING_ISO_8859_1 ).getStr(), False ) : nNoDisplayAtoms++; + m_aStringToAtom[ rString ] = aAtom; + m_aAtomToString[ aAtom ] = rString; + } + return m_aStringToAtom[ rString ]; +} + +bool SelectionManager::requestOwnership( Atom selection ) +{ + bool bSuccess = false; + if( m_pDisplay && m_aWindow ) + { + osl::MutexGuard aGuard(m_aMutex); + + SelectionAdaptor* pAdaptor = getAdaptor( selection ); + if( pAdaptor ) + { + XSetSelectionOwner( m_pDisplay, selection, m_aWindow, CurrentTime ); + if( XGetSelectionOwner( m_pDisplay, selection ) == m_aWindow ) + bSuccess = true; + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", + (bSuccess ? "acquired" : "failed to acquire") + << " ownership for selection " + << getString( selection )); +#endif + + Selection* pSel = m_aSelections[ selection ]; + pSel->m_bOwner = bSuccess; + delete pSel->m_pPixmap; + pSel->m_pPixmap = nullptr; + pSel->m_nOrigTimestamp = m_nSelectionTimestamp; + } +#if OSL_DEBUG_LEVEL > 1 + else + SAL_WARN("vcl.unx.dtrans", "no adaptor for selection " + << getString( selection )); + + if( pAdaptor->getTransferable().is() ) + { + Sequence< DataFlavor > aTypes = pAdaptor->getTransferable()->getTransferDataFlavors(); + for( int i = 0; i < aTypes.getLength(); i++ ) + { + SAL_INFO("vcl.unx.dtrans", " " << aTypes.getConstArray()[i].MimeType); + } + } +#endif + } + return bSuccess; +} + +void SelectionManager::convertTypeToNative( const OUString& rType, Atom selection, int& rFormat, ::std::list< Atom >& rConversions, bool bPushFront ) +{ + NativeTypeEntry* pTab = selection == m_nXdndSelection ? aXdndConversionTab : aNativeConversionTab; + int nTabEntries = selection == m_nXdndSelection ? SAL_N_ELEMENTS(aXdndConversionTab) : SAL_N_ELEMENTS(aNativeConversionTab); + + OString aType( OUStringToOString( rType, RTL_TEXTENCODING_ISO_8859_1 ) ); + SAL_INFO( "vcl.unx.dtrans", "convertTypeToNative " << aType ); + rFormat = 0; + for( int i = 0; i < nTabEntries; i++ ) + { + if( aType.equalsIgnoreAsciiCase( pTab[i].pType ) ) + { + if( ! pTab[i].nAtom ) + pTab[i].nAtom = getAtom( OStringToOUString( pTab[i].pNativeType, RTL_TEXTENCODING_ISO_8859_1 ) ); + rFormat = pTab[i].nFormat; + if( bPushFront ) + rConversions.push_front( pTab[i].nAtom ); + else + rConversions.push_back( pTab[i].nAtom ); + if( pTab[i].nFormat == XA_PIXMAP ) + { + if( bPushFront ) + { + rConversions.push_front( XA_VISUALID ); + rConversions.push_front( XA_COLORMAP ); + } + else + { + rConversions.push_back( XA_VISUALID ); + rConversions.push_back( XA_COLORMAP ); + } + } + } + } + if( ! rFormat ) + rFormat = 8; // byte buffer + if( bPushFront ) + rConversions.push_front( getAtom( rType ) ); + else + rConversions.push_back( getAtom( rType ) ); +}; + +void SelectionManager::getNativeTypeList( const Sequence< DataFlavor >& rTypes, std::list< Atom >& rOutTypeList, Atom targetselection ) +{ + rOutTypeList.clear(); + + int nFormat; + bool bHaveText = false; + for( const auto& rFlavor : rTypes ) + { + if( rFlavor.MimeType.startsWith("text/plain")) + bHaveText = true; + else + convertTypeToNative( rFlavor.MimeType, targetselection, nFormat, rOutTypeList ); + } + if( bHaveText ) + { + if( targetselection != m_nXdndSelection ) + { + // only mimetypes should go into Xdnd type list + rOutTypeList.push_front( XA_STRING ); + rOutTypeList.push_front( m_nCOMPOUNDAtom ); + } + convertTypeToNative( "text/plain;charset=utf-8", targetselection, nFormat, rOutTypeList, true ); + } + if( targetselection != m_nXdndSelection ) + rOutTypeList.push_back( m_nMULTIPLEAtom ); +} + +OUString SelectionManager::convertTypeFromNative( Atom nType, Atom selection, int& rFormat ) +{ + NativeTypeEntry* pTab = (selection == m_nXdndSelection) ? aXdndConversionTab : aNativeConversionTab; + int nTabEntries = (selection == m_nXdndSelection) ? SAL_N_ELEMENTS(aXdndConversionTab) : SAL_N_ELEMENTS(aNativeConversionTab); + + for( int i = 0; i < nTabEntries; i++ ) + { + if( ! pTab[i].nAtom ) + pTab[i].nAtom = getAtom( OStringToOUString( pTab[i].pNativeType, RTL_TEXTENCODING_ISO_8859_1 ) ); + if( nType == pTab[i].nAtom ) + { + rFormat = pTab[i].nFormat; + return OStringToOUString( pTab[i].pType, RTL_TEXTENCODING_ISO_8859_1 ); + } + } + rFormat = 8; + return getString( nType ); +} + +bool SelectionManager::getPasteData( Atom selection, Atom type, Sequence< sal_Int8 >& rData ) +{ + osl::ResettableMutexGuard aGuard(m_aMutex); + std::unordered_map< Atom, Selection* >::iterator it; + bool bSuccess = false; + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "getPasteData( " << getString( selection ) + << ", native: " << getString( type ) << " )."); +#endif + + if( ! m_pDisplay ) + return false; + + it = m_aSelections.find( selection ); + if( it == m_aSelections.end() ) + return false; + + ::Window aSelectionOwner = XGetSelectionOwner( m_pDisplay, selection ); + if( aSelectionOwner == None ) + return false; + if( aSelectionOwner == m_aWindow ) + { + // probably bad timing led us here +#if OSL_DEBUG_LEVEL > 1 + SAL_WARN("vcl.unx.dtrans", "Innere Nabelschau."); +#endif + return false; + } + + // ICCCM recommends to destroy property before convert request unless + // parameters are transported; we do only in case of MULTIPLE, + // so destroy property unless target is MULTIPLE + if( type != m_nMULTIPLEAtom ) + XDeleteProperty( m_pDisplay, m_aWindow, selection ); + + XConvertSelection( m_pDisplay, selection, type, selection, m_aWindow, selection == m_nXdndSelection ? m_nDropTime : CurrentTime ); + it->second->m_eState = Selection::WaitingForResponse; + it->second->m_aRequestedType = type; + it->second->m_aData = Sequence< sal_Int8 >(); + it->second->m_aDataArrived.reset(); + // really start the request; if we don't flush the + // queue the request won't leave it because there are no more + // X calls after this until the data arrived or timeout + XFlush( m_pDisplay ); + + // do a reschedule + struct timeval tv_last, tv_current; + gettimeofday( &tv_last, nullptr ); + tv_current = tv_last; + + XEvent aEvent; + do + { + bool bAdjustTime = false; + { + bool bHandle = false; + + if( XCheckTypedEvent( m_pDisplay, + PropertyNotify, + &aEvent + ) ) + { + bHandle = true; + if( aEvent.xproperty.window == m_aWindow + && aEvent.xproperty.atom == selection ) + bAdjustTime = true; + } + else if( XCheckTypedEvent( m_pDisplay, + SelectionClear, + &aEvent + ) ) + { + bHandle = true; + } + else if( XCheckTypedEvent( m_pDisplay, + SelectionRequest, + &aEvent + ) ) + { + bHandle = true; + } + else if( XCheckTypedEvent( m_pDisplay, + SelectionNotify, + &aEvent + ) ) + { + bHandle = true; + if( aEvent.xselection.selection == selection + && ( aEvent.xselection.requestor == m_aWindow || + aEvent.xselection.requestor == m_aCurrentDropWindow ) + ) + bAdjustTime = true; + } + else + { + aGuard.clear(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + aGuard.reset(); + } + if( bHandle ) + { + aGuard.clear(); + handleXEvent( aEvent ); + aGuard.reset(); + } + } + gettimeofday( &tv_current, nullptr ); + if( bAdjustTime ) + tv_last = tv_current; + } while( ! it->second->m_aDataArrived.check() && (tv_current.tv_sec - tv_last.tv_sec) < getSelectionTimeout() ); + +#if OSL_DEBUG_LEVEL > 1 + SAL_WARN_IF((tv_current.tv_sec - tv_last.tv_sec) > getSelectionTimeout(), + "vcl.unx.dtrans", "timed out."); +#endif + + if( it->second->m_aDataArrived.check() && + it->second->m_aData.getLength() ) + { + rData = it->second->m_aData; + bSuccess = true; + } +#if OSL_DEBUG_LEVEL > 1 + else + SAL_WARN("vcl.unx.dtrans", "conversion unsuccessful."); +#endif + return bSuccess; +} + +bool SelectionManager::getPasteData( Atom selection, const OUString& rType, Sequence< sal_Int8 >& rData ) +{ + bool bSuccess = false; + + std::unordered_map< Atom, Selection* >::iterator it; + { + osl::MutexGuard aGuard(m_aMutex); + + it = m_aSelections.find( selection ); + if( it == m_aSelections.end() ) + return false; + } + + if( it->second->m_aTypes.getLength() == 0 ) + { + Sequence< DataFlavor > aFlavors; + getPasteDataTypes( selection, aFlavors ); + if( it->second->m_aTypes.getLength() == 0 ) + return false; + } + + const Sequence< DataFlavor >& rTypes( it->second->m_aTypes ); + const std::vector< Atom >& rNativeTypes( it->second->m_aNativeTypes ); + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "getPasteData( \"" + << getString( selection ) + << "\", \"" + << rType << "\" )."); +#endif + + if( rType == "text/plain;charset=utf-16" ) + { + // lets see if we have UTF16 else try to find something convertible + if( it->second->m_aTypes.getLength() && ! it->second->m_bHaveUTF16 ) + { + Sequence< sal_Int8 > aData; + if( it->second->m_aUTF8Type != None && + getPasteData( selection, + it->second->m_aUTF8Type, + aData ) + ) + { + OUString aRet( reinterpret_cast(aData.getConstArray()), aData.getLength(), RTL_TEXTENCODING_UTF8 ); + rData = Sequence< sal_Int8 >( reinterpret_cast(aRet.getStr()), (aRet.getLength()+1)*sizeof( sal_Unicode ) ); + bSuccess = true; + } + else if( it->second->m_bHaveCompound && + getPasteData( selection, + m_nCOMPOUNDAtom, + aData ) + ) + { + OUString aRet( convertFromCompound( reinterpret_cast(aData.getConstArray()), aData.getLength() ) ); + rData = Sequence< sal_Int8 >( reinterpret_cast(aRet.getStr()), (aRet.getLength()+1)*sizeof( sal_Unicode ) ); + bSuccess = true; + } + else + { + for( int i = 0; i < rTypes.getLength(); i++ ) + { + rtl_TextEncoding aEncoding = getTextPlainEncoding( rTypes.getConstArray()[i].MimeType ); + if( aEncoding != RTL_TEXTENCODING_DONTKNOW && + aEncoding != RTL_TEXTENCODING_UNICODE && + getPasteData( selection, + rNativeTypes[i], + aData ) + ) + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "using \"" + << rTypes.getConstArray()[i].MimeType + << "\" instead of \"" + << rType + << "\"."); +#endif + + OString aConvert( reinterpret_cast(aData.getConstArray()), aData.getLength() ); + OUString aUTF( OStringToOUString( aConvert, aEncoding ) ); + rData = Sequence< sal_Int8 >( reinterpret_cast(aUTF.getStr()), (aUTF.getLength()+1)*sizeof( sal_Unicode ) ); + bSuccess = true; + break; + } + } + } + } + } + else if( rType == "image/bmp" ) + { + // #i83376# try if someone has the data in image/bmp already before + // doing the PIXMAP stuff (e.g. the Gimp has this) + bSuccess = getPasteData( selection, m_nImageBmpAtom, rData ); +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO_IF(bSuccess, "vcl.unx.dtrans", + "got " << (int) rData.getLength() << " bytes of image/bmp."); +#endif + if( ! bSuccess ) + { + Pixmap aPixmap = None; + Colormap aColormap = None; + + // prepare property for MULTIPLE request + Sequence< sal_Int8 > aData; + Atom const pTypes[4] = { XA_PIXMAP, XA_PIXMAP, XA_COLORMAP, XA_COLORMAP }; + { + osl::MutexGuard aGuard(m_aMutex); + + XChangeProperty( m_pDisplay, + m_aWindow, + selection, + XA_ATOM, + 32, + PropModeReplace, + reinterpret_cast(pTypes), + 4 ); + } + + // try MULTIPLE request + if( getPasteData( selection, m_nMULTIPLEAtom, aData ) ) + { + Atom* pReturnedTypes = reinterpret_cast(aData.getArray()); + if( pReturnedTypes[0] == XA_PIXMAP && pReturnedTypes[1] == XA_PIXMAP ) + { + osl::MutexGuard aGuard(m_aMutex); + + Atom type = None; + int format = 0; + unsigned long nItems = 0; + unsigned long nBytes = 0; + unsigned char* pReturn = nullptr; + XGetWindowProperty( m_pDisplay, m_aWindow, XA_PIXMAP, 0, 1, True, XA_PIXMAP, &type, &format, &nItems, &nBytes, &pReturn ); + if( pReturn ) + { + if( type == XA_PIXMAP ) + aPixmap = *reinterpret_cast(pReturn); + XFree( pReturn ); + pReturn = nullptr; + if( pReturnedTypes[2] == XA_COLORMAP && pReturnedTypes[3] == XA_COLORMAP ) + { + XGetWindowProperty( m_pDisplay, m_aWindow, XA_COLORMAP, 0, 1, True, XA_COLORMAP, &type, &format, &nItems, &nBytes, &pReturn ); + if( pReturn ) + { + if( type == XA_COLORMAP ) + aColormap = *reinterpret_cast(pReturn); + XFree( pReturn ); + } + } + } +#if OSL_DEBUG_LEVEL > 1 + else + { + SAL_WARN("vcl.unx.dtrans", "could not get PIXMAP property: type=" + << getString( type ) + << ", format=" << format + << ", items=" << nItems + << ", bytes=" << nBytes + << ", ret=0x" << pReturn); + } +#endif + } + } + + if( aPixmap == None ) + { + // perhaps two normal requests will work + if( getPasteData( selection, XA_PIXMAP, aData ) ) + { + aPixmap = *reinterpret_cast(aData.getArray()); + if( aColormap == None && getPasteData( selection, XA_COLORMAP, aData ) ) + aColormap = *reinterpret_cast(aData.getArray()); + } + } + + // convert data if possible + if( aPixmap != None ) + { + osl::MutexGuard aGuard(m_aMutex); + + sal_Int32 nOutSize = 0; + sal_uInt8* pBytes = X11_getBmpFromPixmap( m_pDisplay, aPixmap, aColormap, nOutSize ); + if( pBytes ) + { + if( nOutSize ) + { + rData = Sequence< sal_Int8 >( nOutSize ); + memcpy( rData.getArray(), pBytes, nOutSize ); + bSuccess = true; + } + std::free( pBytes ); + } + } + } + } + + if( ! bSuccess ) + { + int nFormat; + ::std::list< Atom > aTypes; + convertTypeToNative( rType, selection, nFormat, aTypes ); + Atom nSelectedType = None; + for (auto const& type : aTypes) + { + for( auto const & nativeType: rNativeTypes ) + if(nativeType == type) + { + nSelectedType = type; + if (nSelectedType != None) + break; + } + } + if( nSelectedType != None ) + bSuccess = getPasteData( selection, nSelectedType, rData ); + } +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "getPasteData for selection " + << getString( selection ) + << " and data type " + << rType + << " returns " + << ( bSuccess ? "true" : "false") + << ", returned sequence has length " + << rData.getLength() << "."); +#endif + return bSuccess; +} + +bool SelectionManager::getPasteDataTypes( Atom selection, Sequence< DataFlavor >& rTypes ) +{ + std::unordered_map< Atom, Selection* >::iterator it; + { + osl::MutexGuard aGuard(m_aMutex); + + it = m_aSelections.find( selection ); + if( it != m_aSelections.end() && + it->second->m_aTypes.getLength() && + std::abs( it->second->m_nLastTimestamp - time( nullptr ) ) < 2 + ) + { + rTypes = it->second->m_aTypes; + return true; + } + } + + bool bSuccess = false; + bool bHaveUTF16 = false; + Atom aUTF8Type = None; + bool bHaveCompound = false; + Sequence< sal_Int8 > aAtoms; + + if( selection == m_nXdndSelection ) + { + // xdnd sends first three types with XdndEnter + // if more than three types are supported then the XDndTypeList + // property on the source window is used + if( m_aDropEnterEvent.data.l[0] && m_aCurrentDropWindow ) + { + if( m_aDropEnterEvent.data.l[1] & 1 ) + { + const unsigned int atomcount = 256; + // more than three types; look in property + osl::MutexGuard aGuard(m_aMutex); + + Atom nType; + int nFormat; + unsigned long nItems, nBytes; + unsigned char* pBytes = nullptr; + + XGetWindowProperty( m_pDisplay, m_aDropEnterEvent.data.l[0], + m_nXdndTypeList, 0, atomcount, False, + XA_ATOM, + &nType, &nFormat, &nItems, &nBytes, &pBytes ); +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "have " + << nItems + << " data types in XdndTypeList."); +#endif + if( nItems == atomcount && nBytes > 0 ) + { + // wow ... more than 256 types ! + aAtoms.realloc( sizeof( Atom )*atomcount+nBytes ); + memcpy( aAtoms.getArray(), pBytes, sizeof( Atom )*atomcount ); + XFree( pBytes ); + pBytes = nullptr; + XGetWindowProperty( m_pDisplay, m_aDropEnterEvent.data.l[0], + m_nXdndTypeList, atomcount, nBytes/sizeof(Atom), + False, XA_ATOM, + &nType, &nFormat, &nItems, &nBytes, &pBytes ); + { + memcpy( aAtoms.getArray()+atomcount*sizeof(Atom), pBytes, nItems*sizeof(Atom) ); + XFree( pBytes ); + } + } + else + { + aAtoms.realloc( sizeof(Atom)*nItems ); + memcpy( aAtoms.getArray(), pBytes, nItems*sizeof(Atom) ); + XFree( pBytes ); + } + } + else + { + // one to three types + int n = 0, i; + for( i = 0; i < 3; i++ ) + if( m_aDropEnterEvent.data.l[2+i] ) + n++; +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "have " + << n + << " data types in XdndEnter."); +#endif + aAtoms.realloc( sizeof(Atom)*n ); + for( i = 0, n = 0; i < 3; i++ ) + if( m_aDropEnterEvent.data.l[2+i] ) + reinterpret_cast(aAtoms.getArray())[n++] = m_aDropEnterEvent.data.l[2+i]; + } + } + } + // get data of type TARGETS + else if( ! getPasteData( selection, m_nTARGETSAtom, aAtoms ) ) + aAtoms = Sequence< sal_Int8 >(); + + std::vector< Atom > aNativeTypes; + if( aAtoms.hasElements() ) + { + sal_Int32 nAtoms = aAtoms.getLength() / sizeof(Atom); + Atom* pAtoms = reinterpret_cast(aAtoms.getArray()); + rTypes.realloc( nAtoms ); + aNativeTypes.resize( nAtoms ); + DataFlavor* pFlavors = rTypes.getArray(); + sal_Int32 nNativeTypesIndex = 0; + bool bHaveText = false; + while( nAtoms-- ) + { + SAL_INFO_IF(*pAtoms && *pAtoms < 0x01000000, "vcl.unx.dtrans", + "getPasteDataTypes: available: \"" << getString(*pAtoms) << "\""); + if( *pAtoms == m_nCOMPOUNDAtom ) + bHaveText = bHaveCompound = true; + else if( *pAtoms && *pAtoms < 0x01000000 ) + { + int nFormat; + pFlavors->MimeType = convertTypeFromNative( *pAtoms, selection, nFormat ); + pFlavors->DataType = cppu::UnoType>::get(); + sal_Int32 nIndex = 0; + if( o3tl::getToken(pFlavors->MimeType, 0, ';', nIndex ) == u"text/plain" ) + { + std::u16string_view aToken(o3tl::getToken(pFlavors->MimeType, 0, ';', nIndex )); + // omit text/plain;charset=unicode since it is not well defined + if( aToken == u"charset=unicode" ) + { + pAtoms++; + continue; + } + bHaveText = true; + if( aToken == u"charset=utf-16" ) + { + bHaveUTF16 = true; + pFlavors->DataType = cppu::UnoType::get(); + } + else if( aToken == u"charset=utf-8" ) + { + aUTF8Type = *pAtoms; + } + } + pFlavors++; + aNativeTypes[ nNativeTypesIndex ] = *pAtoms; + nNativeTypesIndex++; + } + pAtoms++; + } + if( (pFlavors - rTypes.getArray()) < rTypes.getLength() ) + rTypes.realloc(pFlavors - rTypes.getArray()); + bSuccess = rTypes.hasElements(); + if( bHaveText && ! bHaveUTF16 ) + { + int i = 0; + + int nNewFlavors = rTypes.getLength()+1; + Sequence< DataFlavor > aTemp( nNewFlavors ); + for( i = 0; i < nNewFlavors-1; i++ ) + aTemp.getArray()[i+1] = rTypes.getConstArray()[i]; + aTemp.getArray()[0].MimeType = "text/plain;charset=utf-16"; + aTemp.getArray()[0].DataType = cppu::UnoType::get(); + rTypes = aTemp; + + std::vector< Atom > aNativeTemp( nNewFlavors ); + for( i = 0; i < nNewFlavors-1; i++ ) + aNativeTemp[ i + 1 ] = aNativeTypes[ i ]; + aNativeTemp[0] = None; + aNativeTypes = aNativeTemp; + } + } + + { + osl::MutexGuard aGuard(m_aMutex); + + it = m_aSelections.find( selection ); + if( it != m_aSelections.end() ) + { + if( bSuccess ) + { + it->second->m_aTypes = rTypes; + it->second->m_aNativeTypes = aNativeTypes; + it->second->m_nLastTimestamp = time( nullptr ); + it->second->m_bHaveUTF16 = bHaveUTF16; + it->second->m_aUTF8Type = aUTF8Type; + it->second->m_bHaveCompound = bHaveCompound; + } + else + { + it->second->m_aTypes = Sequence< DataFlavor >(); + it->second->m_aNativeTypes = std::vector< Atom >(); + it->second->m_nLastTimestamp = 0; + it->second->m_bHaveUTF16 = false; + it->second->m_aUTF8Type = None; + it->second->m_bHaveCompound = false; + } + } + } + +#if OSL_DEBUG_LEVEL > 1 + { + SAL_INFO("vcl.unx.dtrans", "SelectionManager::getPasteDataTypes( " + << getString( selection ) + << " ) = " + << (bSuccess ? "true" : "false")); + for( int i = 0; i < rTypes.getLength(); i++ ) + SAL_INFO("vcl.unx.dtrans", "type: " << rTypes.getConstArray()[i].MimeType); + } +#endif + + return bSuccess; +} + +PixmapHolder* SelectionManager::getPixmapHolder( Atom selection ) +{ + std::unordered_map< Atom, Selection* >::const_iterator it = m_aSelections.find( selection ); + if( it == m_aSelections.end() ) + return nullptr; + if( ! it->second->m_pPixmap ) + it->second->m_pPixmap = new PixmapHolder( m_pDisplay ); + return it->second->m_pPixmap; +} + +static std::size_t GetTrueFormatSize(int nFormat) +{ + // http://mail.gnome.org/archives/wm-spec-list/2003-March/msg00067.html + return nFormat == 32 ? sizeof(long) : nFormat/8; +} + +bool SelectionManager::sendData( SelectionAdaptor* pAdaptor, + ::Window requestor, + Atom target, + Atom property, + Atom selection ) +{ + osl::ResettableMutexGuard aGuard( m_aMutex ); + + // handle targets related to image/bmp + if( target == XA_COLORMAP || target == XA_PIXMAP || target == XA_BITMAP || target == XA_VISUALID ) + { + PixmapHolder* pPixmap = getPixmapHolder( selection ); + if( ! pPixmap ) return false; + XID nValue = None; + + // handle colormap request + if( target == XA_COLORMAP ) + nValue = static_cast(pPixmap->getColormap()); + else if( target == XA_VISUALID ) + nValue = static_cast(pPixmap->getVisualID()); + else if( target == XA_PIXMAP || target == XA_BITMAP ) + { + nValue = static_cast(pPixmap->getPixmap()); + if( nValue == None ) + { + // first conversion + Sequence< sal_Int8 > aData; + int nFormat; + aGuard.clear(); + bool bConverted = convertData( pAdaptor->getTransferable(), target, selection, nFormat, aData ); + aGuard.reset(); + if( bConverted ) + { + // get pixmap again since clearing the guard could have invalidated + // the pixmap in another thread + pPixmap = getPixmapHolder( selection ); + // conversion succeeded, so aData contains image/bmp now + if( pPixmap->needsConversion( reinterpret_cast(aData.getConstArray()) ) ) + { + SAL_INFO( "vcl.unx.dtrans", "trying bitmap conversion" ); + int depth = pPixmap->getDepth(); + aGuard.clear(); + aData = convertBitmapDepth(aData, depth); + aGuard.reset(); + } + // get pixmap again since clearing the guard could have invalidated + // the pixmap in another thread + pPixmap = getPixmapHolder( selection ); + nValue = static_cast(pPixmap->setBitmapData( reinterpret_cast(aData.getConstArray()) )); + } + if( nValue == None ) + return false; + } + if( target == XA_BITMAP ) + nValue = static_cast(pPixmap->getBitmap()); + } + + XChangeProperty( m_pDisplay, + requestor, + property, + target, + 32, + PropModeReplace, + reinterpret_cast(&nValue), + 1); + return true; + } + + /* + * special target TEXT allows us to transfer + * the data in an encoding of our choice + * COMPOUND_TEXT will work with most applications + */ + if( target == m_nTEXTAtom ) + target = m_nCOMPOUNDAtom; + + Sequence< sal_Int8 > aData; + int nFormat; + aGuard.clear(); + bool bConverted = convertData( pAdaptor->getTransferable(), target, selection, nFormat, aData ); + aGuard.reset(); + if( bConverted ) + { + // conversion succeeded + if( aData.getLength() > m_nIncrementalThreshold ) + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "using INCR protocol."); + std::unordered_map< ::Window, std::unordered_map< Atom, IncrementalTransfer > >::const_iterator win_it = m_aIncrementals.find( requestor ); + if( win_it != m_aIncrementals.end() ) + { + std::unordered_map< Atom, IncrementalTransfer >::const_iterator inc_it = win_it->second.find( property ); + if( inc_it != win_it->second.end() ) + { + const IncrementalTransfer& rInc = inc_it->second; + SAL_INFO("vcl.unx.dtrans", "premature end and new start for INCR transfer for window " + << std::showbase << std::hex + << rInc.m_aRequestor + << ", property " + << getString( rInc.m_aProperty ) + << ", type " + << getString( rInc.m_aTarget )); + } + } +#endif + + // insert IncrementalTransfer + IncrementalTransfer& rInc = m_aIncrementals[ requestor ][ property ]; + rInc.m_aData = aData; + rInc.m_nBufferPos = 0; + rInc.m_aRequestor = requestor; + rInc.m_aProperty = property; + rInc.m_aTarget = target; + rInc.m_nFormat = nFormat; + rInc.m_nTransferStartTime = time( nullptr ); + + // use incr protocol, signal start to requestor + tools::Long nMinSize = m_nIncrementalThreshold; + XSelectInput( m_pDisplay, requestor, PropertyChangeMask ); + XChangeProperty( m_pDisplay, requestor, property, + m_nINCRAtom, 32, PropModeReplace, reinterpret_cast(&nMinSize), 1 ); + XFlush( m_pDisplay ); + } + else + { + std::size_t nUnitSize = GetTrueFormatSize(nFormat); + XChangeProperty( m_pDisplay, + requestor, + property, + target, + nFormat, + PropModeReplace, + reinterpret_cast(aData.getConstArray()), + aData.getLength()/nUnitSize ); + } + } +#if OSL_DEBUG_LEVEL > 1 + else + SAL_WARN("vcl.unx.dtrans", "convertData failed for type: " + << convertTypeFromNative( target, selection, nFormat )); +#endif + return bConverted; +} + +bool SelectionManager::handleSelectionRequest( XSelectionRequestEvent& rRequest ) +{ + osl::ResettableMutexGuard aGuard( m_aMutex ); + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "handleSelectionRequest for selection " + << getString( rRequest.selection ) + << " and target " + << getString( rRequest.target )); +#endif + + XEvent aNotify; + + aNotify.type = SelectionNotify; + aNotify.xselection.display = rRequest.display; + aNotify.xselection.send_event = True; + aNotify.xselection.requestor = rRequest.requestor; + aNotify.xselection.selection = rRequest.selection; + aNotify.xselection.time = rRequest.time; + aNotify.xselection.target = rRequest.target; + aNotify.xselection.property = None; + + SelectionAdaptor* pAdaptor = getAdaptor( rRequest.selection ); + // ensure that we still own that selection + if( pAdaptor && + XGetSelectionOwner( m_pDisplay, rRequest.selection ) == m_aWindow ) + { + css::uno::Reference< XTransferable > xTrans( pAdaptor->getTransferable() ); + if( rRequest.target == m_nTARGETSAtom ) + { + // someone requests our types + if( xTrans.is() ) + { + aGuard.clear(); + Sequence< DataFlavor > aFlavors = xTrans->getTransferDataFlavors(); + aGuard.reset(); + + ::std::list< Atom > aConversions; + getNativeTypeList( aFlavors, aConversions, rRequest.selection ); + + int i, nTypes = aConversions.size(); + Atom* pTypes = static_cast(alloca( nTypes * sizeof( Atom ) )); + std::list< Atom >::const_iterator it; + for( i = 0, it = aConversions.begin(); i < nTypes; i++, ++it ) + pTypes[i] = *it; + XChangeProperty( m_pDisplay, rRequest.requestor, rRequest.property, + XA_ATOM, 32, PropModeReplace, reinterpret_cast(pTypes), nTypes ); + aNotify.xselection.property = rRequest.property; +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "sending type list:"); + for( int k = 0; k < nTypes; k++ ) + SAL_INFO("vcl.unx.dtrans", " " + << (pTypes[k] ? XGetAtomName( m_pDisplay, pTypes[k] ) : + "")); +#endif + } + } + else if( rRequest.target == m_nTIMESTAMPAtom ) + { + tools::Long nTimeStamp = static_cast(m_aSelections[rRequest.selection]->m_nOrigTimestamp); + XChangeProperty( m_pDisplay, rRequest.requestor, rRequest.property, + XA_INTEGER, 32, PropModeReplace, reinterpret_cast(&nTimeStamp), 1 ); + aNotify.xselection.property = rRequest.property; +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "sending timestamp: " << (int)nTimeStamp); +#endif + } + else + { + bool bEventSuccess = false; + if( rRequest.target == m_nMULTIPLEAtom ) + { + // get all targets + Atom nType = None; + int nFormat = 0; + unsigned long nItems = 0, nBytes = 0; + unsigned char* pData = nullptr; + + // get number of atoms + XGetWindowProperty( m_pDisplay, + rRequest.requestor, + rRequest.property, + 0, 0, + False, + AnyPropertyType, + &nType, &nFormat, + &nItems, &nBytes, + &pData ); + if( nFormat == 32 && nBytes/4 ) + { + if( pData ) // ?? should not happen + { + XFree( pData ); + pData = nullptr; + } + XGetWindowProperty( m_pDisplay, + rRequest.requestor, + rRequest.property, + 0, nBytes/4, + False, + nType, + &nType, &nFormat, + &nItems, &nBytes, + &pData ); + if( pData && nItems ) + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "found " + << nItems + << " atoms in MULTIPLE request."); +#endif + bEventSuccess = true; + bool bResetAtoms = false; + Atom* pAtoms = reinterpret_cast(pData); + aGuard.clear(); + for( unsigned long i = 0; i < nItems; i += 2 ) + { +#if OSL_DEBUG_LEVEL > 1 + std::ostringstream oss; + oss << " " + << getString( pAtoms[i] ) + << " => " + << getString( pAtoms[i+1] ) + << ": "; +#endif + + bool bSuccess = sendData( pAdaptor, rRequest.requestor, pAtoms[i], pAtoms[i+1], rRequest.selection ); +#if OSL_DEBUG_LEVEL > 1 + oss << (bSuccess ? "succeeded" : "failed"); + SAL_INFO("vcl.unx.dtrans", oss.str()); +#endif + if( ! bSuccess ) + { + pAtoms[i] = None; + bResetAtoms = true; + } + } + aGuard.reset(); + if( bResetAtoms ) + XChangeProperty( m_pDisplay, + rRequest.requestor, + rRequest.property, + XA_ATOM, + 32, + PropModeReplace, + pData, + nBytes/4 ); + } + if( pData ) + XFree( pData ); + } +#if OSL_DEBUG_LEVEL > 1 + else + { + std::ostringstream oss; + oss << "could not get type list from \"" + << getString( rRequest.property ) + << "\" of type \"" + << getString( nType ) + << "\" on requestor " + << std::showbase << std::hex + << rRequest.requestor + << ", requestor has properties:"; + + int nProps = 0; + Atom* pProps = XListProperties( m_pDisplay, rRequest.requestor, &nProps ); + if( pProps ) + { + for( int i = 0; i < nProps; i++ ) + oss << " \"" << getString( pProps[i]) << "\""; + XFree( pProps ); + } + SAL_INFO("vcl.unx.dtrans", oss.str()); + } +#endif + } + else + { + aGuard.clear(); + bEventSuccess = sendData( pAdaptor, rRequest.requestor, rRequest.target, rRequest.property, rRequest.selection ); + aGuard.reset(); + } + if( bEventSuccess ) + { + aNotify.xselection.target = rRequest.target; + aNotify.xselection.property = rRequest.property; + } + } + aGuard.clear(); + xTrans.clear(); + aGuard.reset(); + } + XSendEvent( m_pDisplay, rRequest.requestor, False, 0, &aNotify ); + + if( rRequest.selection == XA_PRIMARY && + m_bWaitingForPrimaryConversion && + m_xDragSourceListener.is() ) + { + DragSourceDropEvent dsde; + dsde.Source = getXWeak(); + dsde.DragSourceContext = new DragSourceContext( m_aDropWindow, *this ); + dsde.DragSource = static_cast< XDragSource* >(this); + if( aNotify.xselection.property != None ) + { + dsde.DropAction = DNDConstants::ACTION_COPY; + dsde.DropSuccess = true; + } + else + { + dsde.DropAction = DNDConstants::ACTION_NONE; + dsde.DropSuccess = false; + } + css::uno::Reference< XDragSourceListener > xListener( m_xDragSourceListener ); + m_xDragSourceListener.clear(); + aGuard.clear(); + if( xListener.is() ) + xListener->dragDropEnd( dsde ); + } + + // we handled the event in any case by answering + return true; +} + +bool SelectionManager::handleReceivePropertyNotify( XPropertyEvent const & rNotify ) +{ + osl::MutexGuard aGuard( m_aMutex ); + // data we requested arrived +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "handleReceivePropertyNotify for property " + << getString( rNotify.atom )); +#endif + bool bHandled = false; + + std::unordered_map< Atom, Selection* >::iterator it = + m_aSelections.find( rNotify.atom ); + if( it != m_aSelections.end() && + rNotify.state == PropertyNewValue && + ( it->second->m_eState == Selection::WaitingForResponse || + it->second->m_eState == Selection::WaitingForData || + it->second->m_eState == Selection::IncrementalTransfer + ) + ) + { + // MULTIPLE requests are only complete after selection notify + if( it->second->m_aRequestedType == m_nMULTIPLEAtom && + ( it->second->m_eState == Selection::WaitingForResponse || + it->second->m_eState == Selection::WaitingForData ) ) + return false; + + bHandled = true; + + Atom nType = None; + int nFormat = 0; + unsigned long nItems = 0, nBytes = 0; + unsigned char* pData = nullptr; + + // get type and length + XGetWindowProperty( m_pDisplay, + rNotify.window, + rNotify.atom, + 0, 0, + False, + AnyPropertyType, + &nType, &nFormat, + &nItems, &nBytes, + &pData ); +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "found " + << nBytes + << " bytes data of type " + << getString( nType ) + << " and format " + << nFormat + << ", items = " + << nItems); +#endif + if( pData ) + { + XFree( pData ); + pData = nullptr; + } + + if( nType == m_nINCRAtom ) + { + // start data transfer + XDeleteProperty( m_pDisplay, rNotify.window, rNotify.atom ); + it->second->m_eState = Selection::IncrementalTransfer; + } + else if( nType != None ) + { + XGetWindowProperty( m_pDisplay, + rNotify.window, + rNotify.atom, + 0, nBytes/4 +1, + True, + nType, + &nType, &nFormat, + &nItems, &nBytes, + &pData ); + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "read " + << nItems + << " items data of type " + << getString( nType ) + << " and format " + << nFormat + << ", " + << nBytes + << " bytes left in property."); +#endif + + std::size_t nUnitSize = GetTrueFormatSize(nFormat); + + if( it->second->m_eState == Selection::WaitingForData || + it->second->m_eState == Selection::WaitingForResponse ) + { + // copy data + it->second->m_aData = Sequence< sal_Int8 >( reinterpret_cast(pData), nItems*nUnitSize ); + it->second->m_eState = Selection::Inactive; + it->second->m_aDataArrived.set(); + } + else if( it->second->m_eState == Selection::IncrementalTransfer ) + { + if( nItems ) + { + // append data + Sequence< sal_Int8 > aData( it->second->m_aData.getLength() + nItems*nUnitSize ); + memcpy( aData.getArray(), it->second->m_aData.getArray(), it->second->m_aData.getLength() ); + memcpy( aData.getArray() + it->second->m_aData.getLength(), pData, nItems*nUnitSize ); + it->second->m_aData = aData; + } + else + { + it->second->m_eState = Selection::Inactive; + it->second->m_aDataArrived.set(); + } + } + if( pData ) + XFree( pData ); + } + else if( it->second->m_eState == Selection::IncrementalTransfer ) + { + it->second->m_eState = Selection::Inactive; + it->second->m_aDataArrived.set(); + } + } + return bHandled; +} + +bool SelectionManager::handleSendPropertyNotify( XPropertyEvent const & rNotify ) +{ + osl::MutexGuard aGuard( m_aMutex ); + + // ready for next part of an IncrementalTransfer +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "handleSendPropertyNotify for property " + << getString( rNotify.atom ) + << " (" + << (rNotify.state == PropertyNewValue ? + "new value" : + (rNotify.state == PropertyDelete ? + "deleted" : + "unknown")) + << ")."); +#endif + + bool bHandled = false; + // feed incrementals + if( rNotify.state == PropertyDelete ) + { + auto it = m_aIncrementals.find( rNotify.window ); + if( it != m_aIncrementals.end() ) + { + bHandled = true; + time_t nCurrentTime = time( nullptr ); + // throw out aborted transfers + std::vector< Atom > aTimeouts; + for (auto const& incrementalTransfer : it->second) + { + if( (nCurrentTime - incrementalTransfer.second.m_nTransferStartTime) > (getSelectionTimeout()+2) ) + { + aTimeouts.push_back( incrementalTransfer.first ); +#if OSL_DEBUG_LEVEL > 1 + const IncrementalTransfer& rInc = incrementalTransfer.second; + SAL_INFO("vcl.unx.dtrans", + "timeout on INCR transfer for window " + << std::showbase << std::hex + << rInc.m_aRequestor + << ", property " + << getString( rInc.m_aProperty ) + << ", type " + << getString( rInc.m_aTarget )); +#endif + } + } + + for (auto const& timeout : aTimeouts) + { + // transfer broken, might even be a new client with the + // same window id + it->second.erase( timeout ); + } + aTimeouts.clear(); + + auto inc_it = it->second.find( rNotify.atom ); + if( inc_it != it->second.end() ) + { + IncrementalTransfer& rInc = inc_it->second; + + int nBytes = rInc.m_aData.getLength() - rInc.m_nBufferPos; + nBytes = std::min(nBytes, m_nIncrementalThreshold); + if( nBytes < 0 ) // sanity check + nBytes = 0; +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "pushing " + << nBytes + << " bytes: \"" + << std::setw(std::min(nBytes, 32)) + << ((const unsigned char*) + rInc.m_aData.getConstArray()+rInc.m_nBufferPos) + << "\"..."); +#endif + std::size_t nUnitSize = GetTrueFormatSize(rInc.m_nFormat); + + XChangeProperty( m_pDisplay, + rInc.m_aRequestor, + rInc.m_aProperty, + rInc.m_aTarget, + rInc.m_nFormat, + PropModeReplace, + reinterpret_cast(rInc.m_aData.getConstArray())+rInc.m_nBufferPos, + nBytes/nUnitSize ); + rInc.m_nBufferPos += nBytes; + rInc.m_nTransferStartTime = nCurrentTime; + + if( nBytes == 0 ) // transfer finished + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "finished INCR transfer for " + << "window " + << std::showbase << std::hex + << rInc.m_aRequestor + << ", property " + << getString( rInc.m_aProperty ) + << ", type " + << getString( rInc.m_aTarget )); +#endif + it->second.erase( inc_it ); + } + + } + // eventually clean up the hash map + if( it->second.empty() ) + m_aIncrementals.erase( it ); + } + } + return bHandled; +} + +bool SelectionManager::handleSelectionNotify( XSelectionEvent const & rNotify ) +{ + osl::MutexGuard aGuard( m_aMutex ); + + bool bHandled = false; + + // notification about success/failure of one of our conversion requests +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "handleSelectionNotify for selection " + << getString( rNotify.selection ) + << " and property " + << (rNotify.property ? getString( rNotify.property ) : "None") + << " (" + << std::showbase << std::hex + << rNotify.property + << ")."); + SAL_WARN_IF(rNotify.requestor != m_aWindow && + rNotify.requestor != m_aCurrentDropWindow, + "vcl.unx.dtrans", "selection notify for unknown window " + << std::showbase << std::hex + << rNotify.requestor); +#endif + std::unordered_map< Atom, Selection* >::iterator it = + m_aSelections.find( rNotify.selection ); + if ( + (rNotify.requestor == m_aWindow || rNotify.requestor == m_aCurrentDropWindow) && + it != m_aSelections.end() && + ( + (it->second->m_eState == Selection::WaitingForResponse) || + (it->second->m_eState == Selection::WaitingForData) + ) + ) + { + bHandled = true; + if( it->second->m_aRequestedType == m_nMULTIPLEAtom ) + { + Atom nType = None; + int nFormat = 0; + unsigned long nItems = 0, nBytes = 0; + unsigned char* pData = nullptr; + + // get type and length + XGetWindowProperty( m_pDisplay, + rNotify.requestor, + rNotify.property, + 0, 256, + False, + AnyPropertyType, + &nType, &nFormat, + &nItems, &nBytes, + &pData ); + if( nBytes ) // HUGE request !!! + { + if( pData ) + XFree( pData ); + XGetWindowProperty( m_pDisplay, + rNotify.requestor, + rNotify.property, + 0, 256+(nBytes+3)/4, + False, + AnyPropertyType, + &nType, &nFormat, + &nItems, &nBytes, + &pData ); + } + it->second->m_eState = Selection::Inactive; + std::size_t nUnitSize = GetTrueFormatSize(nFormat); + it->second->m_aData = Sequence< sal_Int8 >(reinterpret_cast(pData), nItems * nUnitSize); + it->second->m_aDataArrived.set(); + if( pData ) + XFree( pData ); + } + // WaitingForData can actually happen; some + // applications (e.g. cmdtool on Solaris) first send + // a success and then cancel it. Weird ! + else if( rNotify.property == None ) + { + // conversion failed, stop transfer + it->second->m_eState = Selection::Inactive; + it->second->m_aData = Sequence< sal_Int8 >(); + it->second->m_aDataArrived.set(); + } + // get the bytes, by INCR if necessary + else + it->second->m_eState = Selection::WaitingForData; + } +#if OSL_DEBUG_LEVEL > 1 + else if( it != m_aSelections.end() ) + SAL_WARN("vcl.unx.dtrans", "selection in state " << it->second->m_eState); +#endif + return bHandled; +} + +bool SelectionManager::handleDropEvent( XClientMessageEvent const & rMessage ) +{ + osl::ResettableMutexGuard aGuard(m_aMutex); + + // handle drop related events + ::Window aSource = rMessage.data.l[0]; + ::Window aTarget = rMessage.window; + + bool bHandled = false; + + std::unordered_map< ::Window, DropTargetEntry >::iterator it = + m_aDropTargets.find( aTarget ); + +#if OSL_DEBUG_LEVEL > 1 + if( rMessage.message_type == m_nXdndEnter || + rMessage.message_type == m_nXdndLeave || + rMessage.message_type == m_nXdndDrop || + rMessage.message_type == m_nXdndPosition ) + { + std::ostringstream oss; + oss << "got drop event " + << getString( rMessage.message_type ) + << ", "; + + if( it == m_aDropTargets.end() ) + oss << "but no target found."; + else if( ! it->second.m_pTarget->m_bActive ) + oss << "but target is inactive."; + else if( m_aDropEnterEvent.data.l[0] != None && (::Window)m_aDropEnterEvent.data.l[0] != aSource ) + oss << "but source " + << std::showbase << std::hex + << aSource + << " is unknown (expected " + << m_aDropEnterEvent.data.l[0] + << " or 0)."; + else + oss << "processing."; + SAL_INFO("vcl.unx.dtrans", oss.str()); + } +#endif + + if( it != m_aDropTargets.end() && it->second.m_pTarget->m_bActive && + m_bDropWaitingForCompletion && m_aDropEnterEvent.data.l[0] ) + { + bHandled = true; + OSL_FAIL( "someone forgot to call dropComplete ?" ); + // some listener forgot to call dropComplete in the last operation + // let us end it now and accept the new enter event + aGuard.clear(); + dropComplete( false, m_aCurrentDropWindow ); + aGuard.reset(); + } + + if( it != m_aDropTargets.end() && + it->second.m_pTarget->m_bActive && + ( m_aDropEnterEvent.data.l[0] == None || ::Window(m_aDropEnterEvent.data.l[0]) == aSource ) + ) + { + if( rMessage.message_type == m_nXdndEnter ) + { + bHandled = true; + m_aDropEnterEvent = rMessage; + m_bDropEnterSent = false; + m_aCurrentDropWindow = aTarget; + m_nCurrentProtocolVersion = m_aDropEnterEvent.data.l[1] >> 24; +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "received XdndEnter on " + << std::showbase << std::hex + << aTarget); +#endif + } + else if( + rMessage.message_type == m_nXdndPosition && + aSource == ::Window(m_aDropEnterEvent.data.l[0]) + ) + { + bHandled = true; + m_nDropTime = m_nCurrentProtocolVersion > 0 ? rMessage.data.l[3] : CurrentTime; + + ::Window aChild; + XTranslateCoordinates( m_pDisplay, + it->second.m_aRootWindow, + it->first, + rMessage.data.l[2] >> 16, + rMessage.data.l[2] & 0xffff, + &m_nLastX, &m_nLastY, + &aChild ); +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "received XdndPosition on " + << std::showbase << std::hex + << aTarget + << " (" + << std::dec + << m_nLastX + << ", " + << m_nLastY + << ")."); +#endif + DropTargetDragEnterEvent aEvent; + aEvent.Source = static_cast< XDropTarget* >(it->second.m_pTarget); + aEvent.Context = new DropTargetDragContext( m_aCurrentDropWindow, *this ); + aEvent.LocationX = m_nLastX; + aEvent.LocationY = m_nLastY; + aEvent.SourceActions = m_nSourceActions; + if( m_nCurrentProtocolVersion < 2 ) + aEvent.DropAction = DNDConstants::ACTION_COPY; + else if( Atom(rMessage.data.l[4]) == m_nXdndActionCopy ) + aEvent.DropAction = DNDConstants::ACTION_COPY; + else if( Atom(rMessage.data.l[4]) == m_nXdndActionMove ) + aEvent.DropAction = DNDConstants::ACTION_MOVE; + else if( Atom(rMessage.data.l[4]) == m_nXdndActionLink ) + aEvent.DropAction = DNDConstants::ACTION_LINK; + else if( Atom(rMessage.data.l[4]) == m_nXdndActionAsk ) + // currently no interface to implement ask + aEvent.DropAction = ~0; + else + aEvent.DropAction = DNDConstants::ACTION_NONE; + + m_nLastDropAction = aEvent.DropAction; + if( ! m_bDropEnterSent ) + { + m_bDropEnterSent = true; + aEvent.SupportedDataFlavors = m_xDropTransferable->getTransferDataFlavors(); + aGuard.clear(); + it->second->dragEnter( aEvent ); + } + else + { + aGuard.clear(); + it->second->dragOver( aEvent ); + } + } + else if( + rMessage.message_type == m_nXdndLeave && + aSource == ::Window(m_aDropEnterEvent.data.l[0]) + ) + { + bHandled = true; +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "received XdndLeave on " + << std::showbase << std::hex + << aTarget); +#endif + DropTargetEvent aEvent; + aEvent.Source = static_cast< XDropTarget* >(it->second.m_pTarget); + m_aDropEnterEvent.data.l[0] = None; + if( m_aCurrentDropWindow == aTarget ) + m_aCurrentDropWindow = None; + m_nCurrentProtocolVersion = nXdndProtocolRevision; + aGuard.clear(); + it->second->dragExit( aEvent ); + } + else if( + rMessage.message_type == m_nXdndDrop && + aSource == ::Window(m_aDropEnterEvent.data.l[0]) + ) + { + bHandled = true; + m_nDropTime = m_nCurrentProtocolVersion > 0 ? rMessage.data.l[2] : CurrentTime; +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "received XdndDrop on " + << std::showbase << std::hex + << aTarget + << " (" + << m_nLastX + << ", " + << m_nLastY + << ")."); +#endif + if( m_bLastDropAccepted ) + { + DropTargetDropEvent aEvent; + aEvent.Source = static_cast< XDropTarget* >(it->second.m_pTarget); + aEvent.Context = new DropTargetDropContext( m_aCurrentDropWindow, *this ); + aEvent.LocationX = m_nLastX; + aEvent.LocationY = m_nLastY; + aEvent.DropAction = m_nLastDropAction; + // there is nothing corresponding to source supported actions + // every source can do link, copy and move + aEvent.SourceActions= m_nLastDropAction; + aEvent.Transferable = m_xDropTransferable; + + m_bDropWaitingForCompletion = true; + aGuard.clear(); + it->second->drop( aEvent ); + } + else + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "XdndDrop canceled due to " + << "m_bLastDropAccepted = false." ); +#endif + DropTargetEvent aEvent; + aEvent.Source = static_cast< XDropTarget* >(it->second.m_pTarget); + aGuard.clear(); + it->second->dragExit( aEvent ); + // reset the drop status, notify source + dropComplete( false, m_aCurrentDropWindow ); + } + } + } + return bHandled; +} + +/* + * methods for XDropTargetDropContext + */ + +void SelectionManager::dropComplete( bool bSuccess, ::Window aDropWindow ) +{ + osl::ClearableMutexGuard aGuard(m_aMutex); + + if( aDropWindow == m_aCurrentDropWindow ) + { + if( m_xDragSourceListener.is() ) + { + DragSourceDropEvent dsde; + dsde.Source = getXWeak(); + dsde.DragSourceContext = new DragSourceContext( m_aDropWindow, *this ); + dsde.DragSource = static_cast< XDragSource* >(this); + dsde.DropAction = getUserDragAction(); + dsde.DropSuccess = bSuccess; + css::uno::Reference< XDragSourceListener > xListener = m_xDragSourceListener; + m_xDragSourceListener.clear(); + + aGuard.clear(); + xListener->dragDropEnd( dsde ); + } + else if( m_aDropEnterEvent.data.l[0] && m_aCurrentDropWindow ) + { + XEvent aEvent; + aEvent.xclient.type = ClientMessage; + aEvent.xclient.display = m_pDisplay; + aEvent.xclient.window = m_aDropEnterEvent.data.l[0]; + aEvent.xclient.message_type = m_nXdndFinished; + aEvent.xclient.format = 32; + aEvent.xclient.data.l[0] = m_aCurrentDropWindow; + aEvent.xclient.data.l[1] = bSuccess ? 1 : 0; + aEvent.xclient.data.l[2] = 0; + aEvent.xclient.data.l[3] = 0; + aEvent.xclient.data.l[4] = 0; + if( bSuccess ) + { + if( m_nLastDropAction & DNDConstants::ACTION_MOVE ) + aEvent.xclient.data.l[2] = m_nXdndActionMove; + else if( m_nLastDropAction & DNDConstants::ACTION_COPY ) + aEvent.xclient.data.l[2] = m_nXdndActionCopy; + else if( m_nLastDropAction & DNDConstants::ACTION_LINK ) + aEvent.xclient.data.l[2] = m_nXdndActionLink; + } +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "Sending XdndFinished to " + << std::showbase << std::hex + << m_aDropEnterEvent.data.l[0]); +#endif + XSendEvent( m_pDisplay, m_aDropEnterEvent.data.l[0], + False, NoEventMask, & aEvent ); + + m_aDropEnterEvent.data.l[0] = None; + m_aCurrentDropWindow = None; + m_nCurrentProtocolVersion = nXdndProtocolRevision; + } + m_bDropWaitingForCompletion = false; + } + else + OSL_FAIL( "dropComplete from invalid DropTargetDropContext" ); +} + +/* + * methods for XDropTargetDragContext + */ + +void SelectionManager::sendDragStatus( Atom nDropAction ) +{ + osl::ClearableMutexGuard aGuard(m_aMutex); + + if( m_xDragSourceListener.is() ) + { + sal_Int8 nNewDragAction; + if( nDropAction == m_nXdndActionMove ) + nNewDragAction = DNDConstants::ACTION_MOVE; + else if( nDropAction == m_nXdndActionCopy ) + nNewDragAction = DNDConstants::ACTION_COPY; + else if( nDropAction == m_nXdndActionLink ) + nNewDragAction = DNDConstants::ACTION_LINK; + else + nNewDragAction = DNDConstants::ACTION_NONE; + nNewDragAction &= m_nSourceActions; + + if( nNewDragAction != m_nTargetAcceptAction ) + { + setCursor( getDefaultCursor( nNewDragAction ), m_aDropWindow ); + m_nTargetAcceptAction = nNewDragAction; + } + + DragSourceDragEvent dsde; + dsde.Source = getXWeak(); + dsde.DragSourceContext = new DragSourceContext( m_aDropWindow, *this ); + dsde.DragSource = static_cast< XDragSource* >(this); + dsde.DropAction = m_nSourceActions; + dsde.UserAction = getUserDragAction(); + + css::uno::Reference< XDragSourceListener > xListener( m_xDragSourceListener ); + // caution: do not change anything after this + aGuard.clear(); + if( xListener.is() ) + xListener->dragOver( dsde ); + } + else if( m_aDropEnterEvent.data.l[0] && m_aCurrentDropWindow ) + { + XEvent aEvent; + aEvent.xclient.type = ClientMessage; + aEvent.xclient.display = m_pDisplay; + aEvent.xclient.window = m_aDropEnterEvent.data.l[0]; + aEvent.xclient.message_type = m_nXdndStatus; + aEvent.xclient.format = 32; + aEvent.xclient.data.l[0] = m_aCurrentDropWindow; + aEvent.xclient.data.l[1] = 2; + if( nDropAction == m_nXdndActionMove || + nDropAction == m_nXdndActionLink || + nDropAction == m_nXdndActionCopy ) + aEvent.xclient.data.l[1] |= 1; + aEvent.xclient.data.l[2] = 0; + aEvent.xclient.data.l[3] = 0; + aEvent.xclient.data.l[4] = m_nCurrentProtocolVersion > 1 ? nDropAction : 0; + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "Sending XdndStatus to " + << std::showbase << std::hex + << m_aDropEnterEvent.data.l[0] + << " with action " + << getString( nDropAction )); +#endif + + XSendEvent( m_pDisplay, m_aDropEnterEvent.data.l[0], + False, NoEventMask, & aEvent ); + XFlush( m_pDisplay ); + } +} + +sal_Int8 SelectionManager::getUserDragAction() const +{ + return (m_nTargetAcceptAction != DNDConstants::ACTION_DEFAULT) ? m_nTargetAcceptAction : m_nUserDragAction; +} + +bool SelectionManager::updateDragAction( int modifierState ) +{ + bool bRet = false; + + sal_Int8 nNewDropAction = DNDConstants::ACTION_MOVE; + if( ( modifierState & ShiftMask ) && ! ( modifierState & ControlMask ) ) + nNewDropAction = DNDConstants::ACTION_MOVE; + else if( ( modifierState & ControlMask ) && ! ( modifierState & ShiftMask ) ) + nNewDropAction = DNDConstants::ACTION_COPY; + else if( ( modifierState & ShiftMask ) && ( modifierState & ControlMask ) ) + nNewDropAction = DNDConstants::ACTION_LINK; + if( m_nCurrentProtocolVersion < 0 && m_aDropWindow != None ) + nNewDropAction = DNDConstants::ACTION_COPY; + nNewDropAction &= m_nSourceActions; + + if( ! ( modifierState & ( ControlMask | ShiftMask ) ) ) + { + if( ! nNewDropAction ) + { + // default to an action so the user does not have to press + // keys explicitly + if( m_nSourceActions & DNDConstants::ACTION_MOVE ) + nNewDropAction = DNDConstants::ACTION_MOVE; + else if( m_nSourceActions & DNDConstants::ACTION_COPY ) + nNewDropAction = DNDConstants::ACTION_COPY; + else if( m_nSourceActions & DNDConstants::ACTION_LINK ) + nNewDropAction = DNDConstants::ACTION_LINK; + } + nNewDropAction |= DNDConstants::ACTION_DEFAULT; + } + + if( nNewDropAction != m_nUserDragAction || m_nTargetAcceptAction != DNDConstants::ACTION_DEFAULT ) + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "updateDragAction: " + << std::hex + << (int)m_nUserDragAction + << " -> " + << (int)nNewDropAction); +#endif + bRet = true; + m_nUserDragAction = nNewDropAction; + + DragSourceDragEvent dsde; + dsde.Source = getXWeak(); + dsde.DragSourceContext = new DragSourceContext( m_aDropWindow, *this ); + dsde.DragSource = static_cast< XDragSource* >(this); + dsde.DropAction = m_nUserDragAction; + dsde.UserAction = m_nUserDragAction; + m_nTargetAcceptAction = DNDConstants::ACTION_DEFAULT; // invalidate last accept + m_xDragSourceListener->dropActionChanged( dsde ); + } + return bRet; +} + +void SelectionManager::sendDropPosition( bool bForce, Time eventTime ) +{ + osl::ClearableMutexGuard aGuard(m_aMutex); + + if( m_bDropSent ) + return; + + std::unordered_map< ::Window, DropTargetEntry >::const_iterator it = + m_aDropTargets.find( m_aDropWindow ); + if( it != m_aDropTargets.end() ) + { + if( it->second.m_pTarget->m_bActive ) + { + int x, y; + ::Window aChild; + XTranslateCoordinates( m_pDisplay, it->second.m_aRootWindow, m_aDropWindow, m_nLastDragX, m_nLastDragY, &x, &y, &aChild ); + DropTargetDragEvent dtde; + dtde.Source = it->second.m_pTarget->getXWeak(); + dtde.Context = new DropTargetDragContext( m_aCurrentDropWindow, *this ); + dtde.LocationX = x; + dtde.LocationY = y; + dtde.DropAction = getUserDragAction(); + dtde.SourceActions = m_nSourceActions; + aGuard.clear(); + it->second->dragOver( dtde ); + } + } + else if( bForce || + + m_nLastDragX < m_nNoPosX || m_nLastDragX >= m_nNoPosX+m_nNoPosWidth || + m_nLastDragY < m_nNoPosY || m_nLastDragY >= m_nNoPosY+m_nNoPosHeight + ) + { + // send XdndPosition + XEvent aEvent; + aEvent.type = ClientMessage; + aEvent.xclient.display = m_pDisplay; + aEvent.xclient.format = 32; + aEvent.xclient.message_type = m_nXdndPosition; + aEvent.xclient.window = m_aDropWindow; + aEvent.xclient.data.l[0] = m_aWindow; + aEvent.xclient.data.l[1] = 0; + aEvent.xclient.data.l[2] = m_nLastDragX << 16 | (m_nLastDragY&0xffff); + aEvent.xclient.data.l[3] = eventTime; + + if( m_nUserDragAction & DNDConstants::ACTION_COPY ) + aEvent.xclient.data.l[4]=m_nXdndActionCopy; + else if( m_nUserDragAction & DNDConstants::ACTION_MOVE ) + aEvent.xclient.data.l[4]=m_nXdndActionMove; + else if( m_nUserDragAction & DNDConstants::ACTION_LINK ) + aEvent.xclient.data.l[4]=m_nXdndActionLink; + else + aEvent.xclient.data.l[4]=m_nXdndActionCopy; + XSendEvent( m_pDisplay, m_aDropProxy, False, NoEventMask, &aEvent ); + m_nNoPosX = m_nNoPosY = m_nNoPosWidth = m_nNoPosHeight = 0; + } +} + +bool SelectionManager::handleDragEvent( XEvent const & rMessage ) +{ + if( ! m_xDragSourceListener.is() ) + return false; + + osl::ResettableMutexGuard aGuard(m_aMutex); + + bool bHandled = false; + + // for shortcut + std::unordered_map< ::Window, DropTargetEntry >::const_iterator it = + m_aDropTargets.find( m_aDropWindow ); + +#if OSL_DEBUG_LEVEL > 1 + switch( rMessage.type ) + { + case ClientMessage: + SAL_INFO("vcl.unx.dtrans", "handleDragEvent: " + << getString( rMessage.xclient.message_type )); + break; + case MotionNotify: + break; + case EnterNotify: + SAL_INFO("vcl.unx.dtrans", "handleDragEvent: EnterNotify."); + break; + case LeaveNotify: + SAL_INFO("vcl.unx.dtrans", "handleDragEvent: LeaveNotify."); + break; + case ButtonPress: + SAL_INFO("vcl.unx.dtrans", "handleDragEvent: ButtonPress " + << rMessage.xbutton.button + << " (m_nDragButton = " + << m_nDragButton + << ")."); + break; + case ButtonRelease: + SAL_INFO("vcl.unx.dtrans", "handleDragEvent: ButtonRelease " + << rMessage.xbutton.button + << " (m_nDragButton = " + << m_nDragButton + << ")."); + break; + case KeyPress: + SAL_INFO("vcl.unx.dtrans", "handleDragEvent: KeyPress."); + break; + case KeyRelease: + SAL_INFO("vcl.unx.dtrans", "handleDragEvent: KeyRelease."); + break; + default: + SAL_INFO("vcl.unx.dtrans", "handleDragEvent: ."); + break; + } +#endif + + // handle drag related events + if( rMessage.type == ClientMessage ) + { + if( rMessage.xclient.message_type == m_nXdndStatus && Atom(rMessage.xclient.data.l[0]) == m_aDropWindow ) + { + bHandled = true; + DragSourceDragEvent dsde; + dsde.Source = getXWeak(); + dsde.DragSourceContext = new DragSourceContext( m_aDropWindow, *this ); + dsde.DragSource = static_cast< XDragSource* >( this ); + dsde.UserAction = getUserDragAction(); + dsde.DropAction = DNDConstants::ACTION_NONE; + m_bDropSuccess = (rMessage.xclient.data.l[1] & 1) != 0; +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "status drop action: accept = " + << (m_bDropSuccess ? "true" : "false") + << ", " + << getString( rMessage.xclient.data.l[4] )); +#endif + if( rMessage.xclient.data.l[1] & 1 ) + { + if( m_nCurrentProtocolVersion > 1 ) + { + if( Atom(rMessage.xclient.data.l[4]) == m_nXdndActionCopy ) + dsde.DropAction = DNDConstants::ACTION_COPY; + else if( Atom(rMessage.xclient.data.l[4]) == m_nXdndActionMove ) + dsde.DropAction = DNDConstants::ACTION_MOVE; + else if( Atom(rMessage.xclient.data.l[4]) == m_nXdndActionLink ) + dsde.DropAction = DNDConstants::ACTION_LINK; + } + else + dsde.DropAction = DNDConstants::ACTION_COPY; + } + m_nTargetAcceptAction = dsde.DropAction; + + if( ! ( rMessage.xclient.data.l[1] & 2 ) ) + { + m_nNoPosX = rMessage.xclient.data.l[2] >> 16; + m_nNoPosY = rMessage.xclient.data.l[2] & 0xffff; + m_nNoPosWidth = rMessage.xclient.data.l[3] >> 16; + m_nNoPosHeight = rMessage.xclient.data.l[3] & 0xffff; + } + else + m_nNoPosX = m_nNoPosY = m_nNoPosWidth = m_nNoPosHeight = 0; + + setCursor( getDefaultCursor( dsde.DropAction ), m_aDropWindow ); + aGuard.clear(); + m_xDragSourceListener->dragOver( dsde ); + } + else if( rMessage.xclient.message_type == m_nXdndFinished && m_aDropWindow == Atom(rMessage.xclient.data.l[0]) ) + { + bHandled = true; + // notify the listener + DragSourceDropEvent dsde; + dsde.Source = getXWeak(); + dsde.DragSourceContext = new DragSourceContext( m_aDropWindow, *this ); + dsde.DragSource = static_cast< XDragSource* >(this); + dsde.DropAction = m_nTargetAcceptAction; + dsde.DropSuccess = m_bDropSuccess; + css::uno::Reference< XDragSourceListener > xListener( m_xDragSourceListener ); + m_xDragSourceListener.clear(); + aGuard.clear(); + xListener->dragDropEnd( dsde ); + } + } + else if( rMessage.type == MotionNotify || + rMessage.type == EnterNotify || rMessage.type == LeaveNotify + ) + { + bHandled = true; + bool bForce = false; + int root_x = rMessage.type == MotionNotify ? rMessage.xmotion.x_root : rMessage.xcrossing.x_root; + int root_y = rMessage.type == MotionNotify ? rMessage.xmotion.y_root : rMessage.xcrossing.y_root; + ::Window root = rMessage.type == MotionNotify ? rMessage.xmotion.root : rMessage.xcrossing.root; + + aGuard.clear(); + if( rMessage.type == MotionNotify ) + { + bForce = updateDragAction( rMessage.xmotion.state ); + } + updateDragWindow( root_x, root_y, root ); + aGuard.reset(); + + if( m_nCurrentProtocolVersion >= 0 && m_aDropProxy != None ) + { + aGuard.clear(); + sendDropPosition( bForce, rMessage.type == MotionNotify ? rMessage.xmotion.time : rMessage.xcrossing.time ); + } + } + else if( rMessage.type == KeyPress || rMessage.type == KeyRelease ) + { + bHandled = true; + KeySym aKey = XkbKeycodeToKeysym( m_pDisplay, rMessage.xkey.keycode, 0, 0 ); + if( aKey == XK_Escape ) + { + // abort drag + if( it != m_aDropTargets.end() ) + { + DropTargetEvent dte; + dte.Source = it->second.m_pTarget->getXWeak(); + aGuard.clear(); + it->second.m_pTarget->dragExit( dte ); + aGuard.reset(); + } + else if( m_aDropProxy != None && m_nCurrentProtocolVersion >= 0 ) + { + // send XdndLeave + XEvent aEvent; + aEvent.type = ClientMessage; + aEvent.xclient.display = m_pDisplay; + aEvent.xclient.format = 32; + aEvent.xclient.message_type = m_nXdndLeave; + aEvent.xclient.window = m_aDropWindow; + aEvent.xclient.data.l[0] = m_aWindow; + memset( aEvent.xclient.data.l+1, 0, sizeof(long)*4); + m_aDropWindow = m_aDropProxy = None; + XSendEvent( m_pDisplay, m_aDropProxy, False, NoEventMask, &aEvent ); + } + // notify the listener + DragSourceDropEvent dsde; + dsde.Source = getXWeak(); + dsde.DragSourceContext = new DragSourceContext( m_aDropWindow, *this ); + dsde.DragSource = static_cast< XDragSource* >(this); + dsde.DropAction = DNDConstants::ACTION_NONE; + dsde.DropSuccess = false; + css::uno::Reference< XDragSourceListener > xListener( m_xDragSourceListener ); + m_xDragSourceListener.clear(); + aGuard.clear(); + xListener->dragDropEnd( dsde ); + } + else + { + /* + * man page says: state is state immediate PRIOR to the + * event. It would seem that this is a somewhat arguable + * design decision. + */ + int nState = rMessage.xkey.state; + int nNewState = 0; + switch( aKey ) + { + case XK_Shift_R: + case XK_Shift_L: nNewState = ShiftMask;break; + case XK_Control_R: + case XK_Control_L: nNewState = ControlMask;break; + // just interested in shift and ctrl for dnd + } + if( rMessage.type == KeyPress ) + nState |= nNewState; + else + nState &= ~nNewState; + aGuard.clear(); + if( updateDragAction( nState ) ) + sendDropPosition( true, rMessage.xkey.time ); + } + } + else if( + ( rMessage.type == ButtonPress || rMessage.type == ButtonRelease ) && + rMessage.xbutton.button == m_nDragButton ) + { + bool bCancel = true; + if( m_aDropWindow != None ) + { + if( it != m_aDropTargets.end() ) + { + if( it->second.m_pTarget->m_bActive && m_nUserDragAction != DNDConstants::ACTION_NONE && m_bLastDropAccepted ) + { + bHandled = true; + int x, y; + ::Window aChild; + XTranslateCoordinates( m_pDisplay, rMessage.xbutton.root, m_aDropWindow, rMessage.xbutton.x_root, rMessage.xbutton.y_root, &x, &y, &aChild ); + DropTargetDropEvent dtde; + dtde.Source = it->second.m_pTarget->getXWeak(); + dtde.Context = new DropTargetDropContext( m_aCurrentDropWindow, *this ); + dtde.LocationX = x; + dtde.LocationY = y; + dtde.DropAction = m_nUserDragAction; + dtde.SourceActions = m_nSourceActions; + dtde.Transferable = m_xDragSourceTransferable; + m_bDropSent = true; + m_nDropTimeout = time( nullptr ); + m_bDropWaitingForCompletion = true; + aGuard.clear(); + it->second->drop( dtde ); + bCancel = false; + } + else bCancel = true; + } + else if( m_nCurrentProtocolVersion >= 0 ) + { + bHandled = true; + + XEvent aEvent; + aEvent.type = ClientMessage; + aEvent.xclient.display = m_pDisplay; + aEvent.xclient.format = 32; + aEvent.xclient.message_type = m_nXdndDrop; + aEvent.xclient.window = m_aDropWindow; + aEvent.xclient.data.l[0] = m_aWindow; + aEvent.xclient.data.l[1] = 0; + aEvent.xclient.data.l[2] = rMessage.xbutton.time; + aEvent.xclient.data.l[3] = 0; + aEvent.xclient.data.l[4] = 0; + + m_bDropSent = true; + m_nDropTimeout = time( nullptr ); + XSendEvent( m_pDisplay, m_aDropProxy, False, NoEventMask, &aEvent ); + bCancel = false; + } + else + { + // dropping on non XdndWindows: acquire ownership of + // PRIMARY and send a middle mouse button click down/up to + // target window + SelectionAdaptor* pAdaptor = getAdaptor( XA_PRIMARY ); + if( pAdaptor ) + { + bHandled = true; + + ::Window aDummy; + XEvent aEvent; + aEvent.type = ButtonPress; + aEvent.xbutton.display = m_pDisplay; + aEvent.xbutton.window = m_aDropWindow; + aEvent.xbutton.root = rMessage.xbutton.root; + aEvent.xbutton.subwindow = m_aDropWindow; + aEvent.xbutton.time = rMessage.xbutton.time+1; + aEvent.xbutton.x_root = rMessage.xbutton.x_root; + aEvent.xbutton.y_root = rMessage.xbutton.y_root; + aEvent.xbutton.state = rMessage.xbutton.state; + aEvent.xbutton.button = Button2; + aEvent.xbutton.same_screen = True; + XTranslateCoordinates( m_pDisplay, + rMessage.xbutton.root, m_aDropWindow, + rMessage.xbutton.x_root, rMessage.xbutton.y_root, + &aEvent.xbutton.x, &aEvent.xbutton.y, + &aDummy ); + XSendEvent( m_pDisplay, m_aDropWindow, False, ButtonPressMask, &aEvent ); + aEvent.xbutton.type = ButtonRelease; + aEvent.xbutton.time++; + aEvent.xbutton.state |= Button2Mask; + XSendEvent( m_pDisplay, m_aDropWindow, False, ButtonReleaseMask, &aEvent ); + + m_bDropSent = true; + m_nDropTimeout = time( nullptr ); + XSendEvent( m_pDisplay, m_aDropProxy, False, NoEventMask, &aEvent ); + m_bWaitingForPrimaryConversion = true; + m_bDropSent = true; + m_nDropTimeout = time( nullptr ); + // HACK :-) + aGuard.clear(); + static_cast< X11Clipboard* >( pAdaptor )->setContents( m_xDragSourceTransferable, css::uno::Reference< css::datatransfer::clipboard::XClipboardOwner >() ); + aGuard.reset(); + bCancel = false; + } + } + } + if( bCancel ) + { + // cancel drag + DragSourceDropEvent dsde; + dsde.Source = getXWeak(); + dsde.DragSourceContext = new DragSourceContext( m_aDropWindow, *this ); + dsde.DragSource = static_cast< XDragSource* >(this); + dsde.DropAction = DNDConstants::ACTION_NONE; + dsde.DropSuccess = false; + css::uno::Reference< XDragSourceListener > xListener( m_xDragSourceListener ); + m_xDragSourceListener.clear(); + aGuard.clear(); + xListener->dragDropEnd( dsde ); + bHandled = true; + } + } + return bHandled; +} + +void SelectionManager::accept( sal_Int8 dragOperation, ::Window aDropWindow ) +{ + if( aDropWindow != m_aCurrentDropWindow ) + return; + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "accept: " << std::hex << dragOperation); +#endif + Atom nAction = None; + dragOperation &= (DNDConstants::ACTION_MOVE | DNDConstants::ACTION_COPY | DNDConstants::ACTION_LINK); + if( dragOperation & DNDConstants::ACTION_MOVE ) + nAction = m_nXdndActionMove; + else if( dragOperation & DNDConstants::ACTION_COPY ) + nAction = m_nXdndActionCopy; + else if( dragOperation & DNDConstants::ACTION_LINK ) + nAction = m_nXdndActionLink; + m_bLastDropAccepted = true; + sendDragStatus( nAction ); +} + +void SelectionManager::reject( ::Window aDropWindow ) +{ + if( aDropWindow != m_aCurrentDropWindow ) + return; + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "reject."); +#endif + m_bLastDropAccepted = false; + sendDragStatus( None ); + if( m_bDropSent && m_xDragSourceListener.is() ) + { + DragSourceDropEvent dsde; + dsde.Source = getXWeak(); + dsde.DragSourceContext = new DragSourceContext( m_aDropWindow, *this ); + dsde.DragSource = static_cast< XDragSource* >(this); + dsde.DropAction = DNDConstants::ACTION_NONE; + dsde.DropSuccess = false; + m_xDragSourceListener->dragDropEnd( dsde ); + m_xDragSourceListener.clear(); + } +} + +/* + * XDragSource + */ + +sal_Bool SelectionManager::isDragImageSupported() +{ + return false; +} + +sal_Int32 SelectionManager::getDefaultCursor( sal_Int8 dragAction ) +{ + Cursor aCursor = m_aNoneCursor; + if( dragAction & DNDConstants::ACTION_MOVE ) + aCursor = m_aMoveCursor; + else if( dragAction & DNDConstants::ACTION_COPY ) + aCursor = m_aCopyCursor; + else if( dragAction & DNDConstants::ACTION_LINK ) + aCursor = m_aLinkCursor; + return aCursor; +} + +int SelectionManager::getXdndVersion( ::Window aWindow, ::Window& rProxy ) +{ + Atom* pProperties = nullptr; + int nProperties = 0; + Atom nType; + int nFormat; + unsigned long nItems, nBytes; + unsigned char* pBytes = nullptr; + + int nVersion = -1; + rProxy = None; + + /* + * XListProperties is used here to avoid unnecessary XGetWindowProperty calls + * and therefore reducing latency penalty + */ + pProperties = XListProperties( m_pDisplay, aWindow, &nProperties ); + // first look for proxy + int i; + for( i = 0; i < nProperties; i++ ) + { + if( pProperties[i] == m_nXdndProxy ) + { + XGetWindowProperty( m_pDisplay, aWindow, m_nXdndProxy, 0, 1, False, XA_WINDOW, + &nType, &nFormat, &nItems, &nBytes, &pBytes ); + if( pBytes ) + { + if( nType == XA_WINDOW ) + rProxy = *reinterpret_cast< ::Window* >(pBytes); + XFree( pBytes ); + pBytes = nullptr; + if( rProxy != None ) + { + // now check proxy whether it points to itself + XGetWindowProperty( m_pDisplay, rProxy, m_nXdndProxy, 0, 1, False, XA_WINDOW, + &nType, &nFormat, &nItems, &nBytes, &pBytes ); + if( pBytes ) + { + if( nType == XA_WINDOW && *reinterpret_cast< ::Window* >(pBytes) != rProxy ) + rProxy = None; + XFree( pBytes ); + pBytes = nullptr; + } + else + rProxy = None; + } + } + break; + } + } + if ( pProperties ) + XFree (pProperties); + + ::Window aAwareWindow = rProxy != None ? rProxy : aWindow; + + XGetWindowProperty( m_pDisplay, aAwareWindow, m_nXdndAware, 0, 1, False, XA_ATOM, + &nType, &nFormat, &nItems, &nBytes, &pBytes ); + if( pBytes ) + { + if( nType == XA_ATOM ) + nVersion = *reinterpret_cast(pBytes); + XFree( pBytes ); + } + + nVersion = std::min(nVersion, nXdndProtocolRevision); + + return nVersion; +} + +void SelectionManager::updateDragWindow( int nX, int nY, ::Window aRoot ) +{ + osl::ResettableMutexGuard aGuard( m_aMutex ); + + css::uno::Reference< XDragSourceListener > xListener( m_xDragSourceListener ); + + m_nLastDragX = nX; + m_nLastDragY = nY; + + ::Window aParent = aRoot; + ::Window aChild; + ::Window aNewProxy = None, aNewCurrentWindow = None; + int nNewProtocolVersion = -1; + int nWinX, nWinY; + + // find the first XdndAware window or check if root window is + // XdndAware or has XdndProxy + do + { + XTranslateCoordinates( m_pDisplay, aRoot, aParent, nX, nY, &nWinX, &nWinY, &aChild ); + if( aChild != None ) + { + if( aChild == m_aCurrentDropWindow && aChild != aRoot && m_nCurrentProtocolVersion >= 0 ) + { + aParent = aChild; + break; + } + nNewProtocolVersion = getXdndVersion( aChild, aNewProxy ); + aParent = aChild; + } + } while( aChild != None && nNewProtocolVersion < 0 ); + + aNewCurrentWindow = aParent; + if( aNewCurrentWindow == aRoot ) + { + // no children, try root drop + nNewProtocolVersion = getXdndVersion( aNewCurrentWindow, aNewProxy ); + if( nNewProtocolVersion < 3 ) + { + aNewCurrentWindow = aNewProxy = None; + nNewProtocolVersion = nXdndProtocolRevision; + } + } + + DragSourceDragEvent dsde; + dsde.Source = getXWeak(); + dsde.DragSourceContext = new DragSourceContext( m_aDropWindow, *this ); + dsde.DragSource = static_cast< XDragSource* >(this); + dsde.DropAction = nNewProtocolVersion >= 0 ? m_nUserDragAction : DNDConstants::ACTION_COPY; + dsde.UserAction = nNewProtocolVersion >= 0 ? m_nUserDragAction : DNDConstants::ACTION_COPY; + + std::unordered_map< ::Window, DropTargetEntry >::const_iterator it; + if( aNewCurrentWindow != m_aDropWindow ) + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "drag left window " + << std::showbase << std::hex + << m_aDropWindow + << std::dec + << " (rev. " + << m_nCurrentProtocolVersion + << "), entered window " + << std::showbase << std::hex + << aNewCurrentWindow + << " (rev " + << std::dec + << nNewProtocolVersion + << ")."); +#endif + if( m_aDropWindow != None ) + { + it = m_aDropTargets.find( m_aDropWindow ); + if( it != m_aDropTargets.end() ) + // shortcut for own drop targets + { + DropTargetEvent dte; + dte.Source = it->second.m_pTarget->getXWeak(); + aGuard.clear(); + it->second.m_pTarget->dragExit( dte ); + aGuard.reset(); + } + else + { + // send old drop target a XdndLeave + XEvent aEvent; + aEvent.type = ClientMessage; + aEvent.xclient.display = m_pDisplay; + aEvent.xclient.format = 32; + aEvent.xclient.message_type = m_nXdndLeave; + aEvent.xclient.window = m_aDropWindow; + aEvent.xclient.data.l[0] = m_aWindow; + aEvent.xclient.data.l[1] = 0; + XSendEvent( m_pDisplay, m_aDropProxy, False, NoEventMask, &aEvent ); + } + if( xListener.is() ) + { + aGuard.clear(); + xListener->dragExit( dsde ); + aGuard.reset(); + } + } + + m_nCurrentProtocolVersion = nNewProtocolVersion; + m_aDropWindow = aNewCurrentWindow; + m_aDropProxy = aNewProxy != None ? aNewProxy : m_aDropWindow; + + it = m_aDropTargets.find( m_aDropWindow ); + if( it != m_aDropTargets.end() && ! it->second.m_pTarget->m_bActive ) + m_aDropProxy = None; + + if( m_aDropProxy != None && xListener.is() ) + { + aGuard.clear(); + xListener->dragEnter( dsde ); + aGuard.reset(); + } + // send XdndEnter + if( m_aDropProxy != None && m_nCurrentProtocolVersion >= 0 ) + { + it = m_aDropTargets.find( m_aDropWindow ); + if( it != m_aDropTargets.end() ) + { + XTranslateCoordinates( m_pDisplay, aRoot, m_aDropWindow, nX, nY, &nWinX, &nWinY, &aChild ); + DropTargetDragEnterEvent dtde; + dtde.Source = it->second.m_pTarget->getXWeak(); + dtde.Context = new DropTargetDragContext( m_aCurrentDropWindow, *this ); + dtde.LocationX = nWinX; + dtde.LocationY = nWinY; + dtde.DropAction = m_nUserDragAction; + dtde.SourceActions = m_nSourceActions; + dtde.SupportedDataFlavors = m_xDragSourceTransferable->getTransferDataFlavors(); + aGuard.clear(); + it->second.m_pTarget->dragEnter( dtde ); + aGuard.reset(); + } + else + { + XEvent aEvent; + aEvent.type = ClientMessage; + aEvent.xclient.display = m_pDisplay; + aEvent.xclient.format = 32; + aEvent.xclient.message_type = m_nXdndEnter; + aEvent.xclient.window = m_aDropWindow; + aEvent.xclient.data.l[0] = m_aWindow; + aEvent.xclient.data.l[1] = m_nCurrentProtocolVersion << 24; + memset( aEvent.xclient.data.l + 2, 0, sizeof( long )*3 ); + // fill in data types + ::std::list< Atom > aConversions; + getNativeTypeList( m_aDragFlavors, aConversions, m_nXdndSelection ); + if( aConversions.size() > 3 ) + aEvent.xclient.data.l[1] |= 1; + ::std::list< Atom >::const_iterator type_it = aConversions.begin(); + for( int i = 0; type_it != aConversions.end() && i < 3; i++, ++type_it ) + aEvent.xclient.data.l[i+2] = *type_it; + XSendEvent( m_pDisplay, m_aDropProxy, False, NoEventMask, &aEvent ); + } + } + m_nNoPosX = m_nNoPosY = m_nNoPosWidth = m_nNoPosHeight = 0; + } + else if( m_aDropProxy != None && xListener.is() ) + { + aGuard.clear(); + // drag over for XdndAware windows comes when receiving XdndStatus + xListener->dragOver( dsde ); + } +} + +void SelectionManager::startDrag( + const DragGestureEvent& trigger, + sal_Int8 sourceActions, + sal_Int32, + sal_Int32, + const css::uno::Reference< XTransferable >& transferable, + const css::uno::Reference< XDragSourceListener >& listener + ) +{ +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "startDrag( sourceActions = " + << std::hex + << (int)sourceActions + << " )."); +#endif + DragSourceDropEvent aDragFailedEvent; + aDragFailedEvent.Source = getXWeak(); + aDragFailedEvent.DragSource = static_cast< XDragSource* >(this); + aDragFailedEvent.DragSourceContext = new DragSourceContext( None, *this ); + aDragFailedEvent.DropAction = DNDConstants::ACTION_NONE; + aDragFailedEvent.DropSuccess = false; + + if( m_aDragRunning.check() ) + { + if( listener.is() ) + listener->dragDropEnd( aDragFailedEvent ); + +#if OSL_DEBUG_LEVEL > 1 + SAL_WARN("vcl.unx.dtrans", + "*** ERROR *** second drag and drop started."); + if( m_xDragSourceListener.is() ) + SAL_WARN("vcl.unx.dtrans", + "*** ERROR *** drag source listener already set."); + else + SAL_WARN("vcl.unx.dtrans", + "*** ERROR *** drag thread already running."); +#endif + return; + } + + SalFrame* pCaptureFrame = nullptr; + + { + osl::ClearableMutexGuard aGuard(m_aMutex); + + // first get the current pointer position and the window that + // the pointer is located in. since said window should be one + // of our DropTargets at the time of executeDrag we can use + // them for a start + ::Window aRoot, aParent, aChild; + int root_x(0), root_y(0), win_x(0), win_y(0); + unsigned int mask(0); + + bool bPointerFound = false; + for (auto const& dropTarget : m_aDropTargets) + { + if( XQueryPointer( m_pDisplay, dropTarget.second.m_aRootWindow, + &aRoot, &aParent, + &root_x, &root_y, + &win_x, &win_y, + &mask ) ) + { + aParent = dropTarget.second.m_aRootWindow; + aRoot = aParent; + bPointerFound = true; + break; + } + } + + // don't start DnD if there is none of our windows on the same screen as + // the pointer or if no mouse button is pressed + if( !bPointerFound || (mask & (Button1Mask|Button2Mask|Button3Mask)) == 0 ) + { + aGuard.clear(); + if( listener.is() ) + listener->dragDropEnd( aDragFailedEvent ); + return; + } + + // try to find which of our drop targets is the drag source + // if that drop target is deregistered we should stop executing + // the drag (actually this is a poor substitute for an "endDrag" + // method ). + m_aDragSourceWindow = None; + do + { + XTranslateCoordinates( m_pDisplay, aRoot, aParent, root_x, root_y, &win_x, &win_y, &aChild ); + if( aChild != None && m_aDropTargets.find( aChild ) != m_aDropTargets.end() ) + { + m_aDragSourceWindow = aChild; +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "found drag source window " + << std::showbase << std::hex + << m_aDragSourceWindow); +#endif + break; + } + aParent = aChild; + } while( aChild != None ); + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "try to grab pointer ..."); +#endif + int nPointerGrabSuccess = + XGrabPointer( m_pDisplay, aRoot, True, + DRAG_EVENT_MASK, + GrabModeAsync, GrabModeAsync, + None, + None, + CurrentTime ); + /* if we could not grab the pointer here, there is a chance + that the pointer is grabbed by the other vcl display (the main loop) + so let's break that grab and reset it later + + remark: this whole code should really be molten into normal vcl so only + one display is used... + */ + if( nPointerGrabSuccess != GrabSuccess ) + { + comphelper::SolarMutex& rSolarMutex( Application::GetSolarMutex() ); + if( rSolarMutex.tryToAcquire() ) + { + pCaptureFrame = vcl_sal::getSalDisplay(GetGenericUnixSalData())->GetCaptureFrame(); + if( pCaptureFrame ) + { + vcl_sal::getSalDisplay(GetGenericUnixSalData())->CaptureMouse( nullptr ); + nPointerGrabSuccess = + XGrabPointer( m_pDisplay, aRoot, True, + DRAG_EVENT_MASK, + GrabModeAsync, GrabModeAsync, + None, + None, + CurrentTime ); + } + } + } +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "... grabbed pointer: " + << nPointerGrabSuccess); + SAL_INFO("vcl.unx.dtrans", "try to grab keyboard ..."); +#endif + int nKeyboardGrabSuccess = + XGrabKeyboard( m_pDisplay, aRoot, True, + GrabModeAsync, GrabModeAsync, CurrentTime ); +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "... grabbed keyboard: " + << nKeyboardGrabSuccess); +#endif + if( nPointerGrabSuccess != GrabSuccess || nKeyboardGrabSuccess != GrabSuccess ) + { + if( nPointerGrabSuccess == GrabSuccess ) + XUngrabPointer( m_pDisplay, CurrentTime ); + if( nKeyboardGrabSuccess == GrabSuccess ) + XUngrabKeyboard( m_pDisplay, CurrentTime ); + XFlush( m_pDisplay ); + aGuard.clear(); + if( listener.is() ) + listener->dragDropEnd( aDragFailedEvent ); + if( pCaptureFrame ) + { + comphelper::SolarMutex& rSolarMutex( Application::GetSolarMutex() ); + if( rSolarMutex.tryToAcquire() ) + vcl_sal::getSalDisplay(GetGenericUnixSalData())->CaptureMouse( pCaptureFrame ); +#if OSL_DEBUG_LEVEL > 0 + else + OSL_FAIL( "failed to acquire SolarMutex to reset capture frame" ); +#endif + } + return; + } + + m_xDragSourceTransferable = transferable; + m_xDragSourceListener = listener; + m_aDragFlavors = transferable->getTransferDataFlavors(); + m_aCurrentCursor = None; + + requestOwnership( m_nXdndSelection ); + + ::std::list< Atom > aConversions; + getNativeTypeList( m_aDragFlavors, aConversions, m_nXdndSelection ); + + Atom* pTypes = static_cast(alloca( sizeof(Atom)*aConversions.size() )); + int nTypes = 0; + for (auto const& conversion : aConversions) + pTypes[nTypes++] = conversion; + + XChangeProperty( m_pDisplay, m_aWindow, m_nXdndTypeList, XA_ATOM, 32, PropModeReplace, reinterpret_cast(pTypes), nTypes ); + + m_nSourceActions = sourceActions | DNDConstants::ACTION_DEFAULT; + m_nUserDragAction = DNDConstants::ACTION_MOVE & m_nSourceActions; + if( ! m_nUserDragAction ) + m_nUserDragAction = DNDConstants::ACTION_COPY & m_nSourceActions; + if( ! m_nUserDragAction ) + m_nUserDragAction = DNDConstants::ACTION_LINK & m_nSourceActions; + m_nTargetAcceptAction = DNDConstants::ACTION_DEFAULT; + m_bDropSent = false; + m_bDropSuccess = false; + m_bWaitingForPrimaryConversion = false; + m_nDragButton = Button1; // default to left button + css::awt::MouseEvent aEvent; + if( trigger.Event >>= aEvent ) + { + if( aEvent.Buttons & MouseButton::LEFT ) + m_nDragButton = Button1; + else if( aEvent.Buttons & MouseButton::RIGHT ) + m_nDragButton = Button3; + else if( aEvent.Buttons & MouseButton::MIDDLE ) + m_nDragButton = Button2; + } +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "m_nUserDragAction = " + << std::hex + << (int)m_nUserDragAction); +#endif + updateDragWindow( root_x, root_y, aRoot ); + m_nUserDragAction = ~0; + updateDragAction( mask ); + } + + m_aDragRunning.set(); + m_aDragExecuteThread = osl_createSuspendedThread( call_SelectionManager_runDragExecute, this ); + if( m_aDragExecuteThread ) + osl_resumeThread( m_aDragExecuteThread ); + else + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "osl_createSuspendedThread failed " + << "for drag execute."); +#endif + m_xDragSourceListener.clear(); + m_xDragSourceTransferable.clear(); + + m_bDropSent = false; + m_bDropSuccess = false; + m_bWaitingForPrimaryConversion = false; + m_aDropWindow = None; + m_aDropProxy = None; + m_nCurrentProtocolVersion = nXdndProtocolRevision; + m_nNoPosX = 0; + m_nNoPosY = 0; + m_nNoPosWidth = 0; + m_nNoPosHeight = 0; + m_aCurrentCursor = None; + + XUngrabPointer( m_pDisplay, CurrentTime ); + XUngrabKeyboard( m_pDisplay, CurrentTime ); + XFlush( m_pDisplay ); + + if( pCaptureFrame ) + { + comphelper::SolarMutex& rSolarMutex( Application::GetSolarMutex() ); + if( rSolarMutex.tryToAcquire() ) + vcl_sal::getSalDisplay(GetGenericUnixSalData())->CaptureMouse( pCaptureFrame ); +#if OSL_DEBUG_LEVEL > 0 + else + OSL_FAIL( "failed to acquire SolarMutex to reset capture frame" ); +#endif + } + + m_aDragRunning.reset(); + + if( listener.is() ) + listener->dragDropEnd( aDragFailedEvent ); + } +} + +void SelectionManager::runDragExecute( void* pThis ) +{ + SelectionManager* This = static_cast(pThis); + This->dragDoDispatch(); +} + +void SelectionManager::dragDoDispatch() +{ + + // do drag + // m_xDragSourceListener will be cleared on finished drop +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "begin executeDrag dispatching."); +#endif + oslThread aThread = m_aDragExecuteThread; + while( m_xDragSourceListener.is() && ( ! m_bDropSent || time(nullptr)-m_nDropTimeout < 5 ) && osl_scheduleThread( aThread ) ) + { + // let the thread in the run method do the dispatching + // just look occasionally here whether drop timed out or is completed + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + } +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "end executeDrag dispatching."); +#endif + { + osl::ClearableMutexGuard aGuard(m_aMutex); + + css::uno::Reference< XDragSourceListener > xListener( m_xDragSourceListener ); + css::uno::Reference< XTransferable > xTransferable( m_xDragSourceTransferable ); + m_xDragSourceListener.clear(); + m_xDragSourceTransferable.clear(); + + DragSourceDropEvent dsde; + dsde.Source = getXWeak(); + dsde.DragSourceContext = new DragSourceContext( m_aDropWindow, *this ); + dsde.DragSource = static_cast< XDragSource* >(this); + dsde.DropAction = DNDConstants::ACTION_NONE; + dsde.DropSuccess = false; + + // cleanup after drag + if( m_bWaitingForPrimaryConversion ) + { + SelectionAdaptor* pAdaptor = getAdaptor( XA_PRIMARY ); + if (pAdaptor) + pAdaptor->clearTransferable(); + } + + m_bDropSent = false; + m_bDropSuccess = false; + m_bWaitingForPrimaryConversion = false; + m_aDropWindow = None; + m_aDropProxy = None; + m_nCurrentProtocolVersion = nXdndProtocolRevision; + m_nNoPosX = 0; + m_nNoPosY = 0; + m_nNoPosWidth = 0; + m_nNoPosHeight = 0; + m_aCurrentCursor = None; + + XUngrabPointer( m_pDisplay, CurrentTime ); + XUngrabKeyboard( m_pDisplay, CurrentTime ); + XFlush( m_pDisplay ); + + m_aDragExecuteThread = nullptr; + m_aDragRunning.reset(); + + aGuard.clear(); + if( xListener.is() ) + { + xTransferable.clear(); + xListener->dragDropEnd( dsde ); + } + } + osl_destroyThread( aThread ); +} + +/* + * XDragSourceContext + */ + + +void SelectionManager::setCursor( sal_Int32 cursor, ::Window aDropWindow ) +{ + osl::MutexGuard aGuard( m_aMutex ); + if( aDropWindow == m_aDropWindow && Cursor(cursor) != m_aCurrentCursor ) + { + if( m_xDragSourceListener.is() && ! m_bDropSent ) + { + m_aCurrentCursor = cursor; + XChangeActivePointerGrab( m_pDisplay, DRAG_EVENT_MASK, cursor, CurrentTime ); + XFlush( m_pDisplay ); + } + } +} + +void SelectionManager::transferablesFlavorsChanged() +{ + osl::MutexGuard aGuard(m_aMutex); + + m_aDragFlavors = m_xDragSourceTransferable->getTransferDataFlavors(); + + std::list< Atom > aConversions; + + getNativeTypeList( m_aDragFlavors, aConversions, m_nXdndSelection ); + + Atom* pTypes = static_cast(alloca( sizeof(Atom)*aConversions.size() )); + int nTypes = 0; + for (auto const& conversion : aConversions) + pTypes[nTypes++] = conversion; + XChangeProperty( m_pDisplay, m_aWindow, m_nXdndTypeList, XA_ATOM, 32, PropModeReplace, reinterpret_cast(pTypes), nTypes ); + + if( m_aCurrentDropWindow == None || m_nCurrentProtocolVersion < 0 ) + return; + + // send synthetic leave and enter events + + XEvent aEvent; + + aEvent.type = ClientMessage; + aEvent.xclient.display = m_pDisplay; + aEvent.xclient.format = 32; + aEvent.xclient.window = m_aDropWindow; + aEvent.xclient.data.l[0] = m_aWindow; + + aEvent.xclient.message_type = m_nXdndLeave; + aEvent.xclient.data.l[1] = 0; + XSendEvent( m_pDisplay, m_aDropProxy, False, NoEventMask, &aEvent ); + + aEvent.xclient.message_type = m_nXdndEnter; + aEvent.xclient.data.l[1] = m_nCurrentProtocolVersion << 24; + memset( aEvent.xclient.data.l + 2, 0, sizeof( long )*3 ); + // fill in data types + if( nTypes > 3 ) + aEvent.xclient.data.l[1] |= 1; + for( int j = 0; j < nTypes && j < 3; j++ ) + aEvent.xclient.data.l[j+2] = pTypes[j]; + + XSendEvent( m_pDisplay, m_aDropProxy, False, NoEventMask, &aEvent ); + +} + +/* + * dispatch loop + */ + +bool SelectionManager::handleXEvent( XEvent& rEvent ) +{ + /* + * since we are XConnectionListener to a second X display + * to get client messages it is essential not to dispatch + * events twice that we get on both connections + * + * between dispatching ButtonPress and startDrag + * the user can already have released the mouse. The ButtonRelease + * will then be dispatched in VCLs queue and never turn up here. + * Which is not so good, since startDrag will XGrabPointer and + * XGrabKeyboard -> solid lock. + */ + if( rEvent.xany.display != m_pDisplay + && rEvent.type != ClientMessage + && rEvent.type != ButtonPress + && rEvent.type != ButtonRelease + ) + return false; + + bool bHandled = false; + switch (rEvent.type) + { + case SelectionClear: + { + osl::ClearableMutexGuard aGuard(m_aMutex); +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "SelectionClear for selection " + << getString( rEvent.xselectionclear.selection )); +#endif + SelectionAdaptor* pAdaptor = getAdaptor( rEvent.xselectionclear.selection ); + std::unordered_map< Atom, Selection* >::iterator it( m_aSelections.find( rEvent.xselectionclear.selection ) ); + if( it != m_aSelections.end() ) + it->second->m_bOwner = false; + aGuard.clear(); + if ( pAdaptor ) + pAdaptor->clearTransferable(); + } + break; + + case SelectionRequest: + bHandled = handleSelectionRequest( rEvent.xselectionrequest ); + break; + case PropertyNotify: + if( rEvent.xproperty.window == m_aWindow || + rEvent.xproperty.window == m_aCurrentDropWindow + ) + bHandled = handleReceivePropertyNotify( rEvent.xproperty ); + else + bHandled = handleSendPropertyNotify( rEvent.xproperty ); + break; + case SelectionNotify: + bHandled = handleSelectionNotify( rEvent.xselection ); + break; + case ClientMessage: + // messages from drag target + if( rEvent.xclient.message_type == m_nXdndStatus || + rEvent.xclient.message_type == m_nXdndFinished ) + bHandled = handleDragEvent( rEvent ); + // messages from drag source + else if( + rEvent.xclient.message_type == m_nXdndEnter || + rEvent.xclient.message_type == m_nXdndLeave || + rEvent.xclient.message_type == m_nXdndPosition || + rEvent.xclient.message_type == m_nXdndDrop + ) + bHandled = handleDropEvent( rEvent.xclient ); + break; + case EnterNotify: + case LeaveNotify: + case MotionNotify: + case ButtonPress: + case ButtonRelease: + case KeyPress: + case KeyRelease: + bHandled = handleDragEvent( rEvent ); + break; + default: + ; + } + return bHandled; +} + +void SelectionManager::dispatchEvent( int millisec ) +{ + // acquire the mutex to prevent other threads + // from using the same X connection + osl::ResettableMutexGuard aGuard(m_aMutex); + + if( !XPending( m_pDisplay )) + { + int nfds = 1; + // wait for any events if none are already queued + pollfd aPollFD[2]; + aPollFD[0].fd = XConnectionNumber( m_pDisplay ); + aPollFD[0].events = POLLIN; + aPollFD[0].revents = 0; + + // on infinite timeout we need endthreadpipe monitoring too + if (millisec < 0) + { + aPollFD[1].fd = m_EndThreadPipe[0]; + aPollFD[1].events = POLLIN | POLLERR; + aPollFD[1].revents = 0; + nfds = 2; + } + + // release mutex for the time of waiting for possible data + aGuard.clear(); + if( poll( aPollFD, nfds, millisec ) <= 0 ) + return; + aGuard.reset(); + } + while( XPending( m_pDisplay )) + { + XEvent event; + XNextEvent( m_pDisplay, &event ); + aGuard.clear(); + handleXEvent( event ); + aGuard.reset(); + } +} + +void SelectionManager::run( void* pThis ) +{ +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "SelectionManager::run."); +#endif + osl::Thread::setName("SelectionManager"); + // dispatch until the cows come home + + SelectionManager* This = static_cast(pThis); + + timeval aLast; + gettimeofday( &aLast, nullptr ); + + css::uno::Reference< XComponentContext > xContext( ::comphelper::getProcessComponentContext() ); + This->m_xDesktop.set( Desktop::create(xContext) ); + This->m_xDesktop->addTerminateListener(This); + + // if end thread pipe properly initialized, allow infinite wait in poll + // otherwise, fallback on 1 sec timeout + const int timeout = (This->m_EndThreadPipe[0] != This->m_EndThreadPipe[1]) ? -1 : 1000; + + while( osl_scheduleThread(This->m_aThread) ) + { + This->dispatchEvent( timeout ); + + timeval aNow; + gettimeofday( &aNow, nullptr ); + + if( (aNow.tv_sec - aLast.tv_sec) > 0 ) + { + osl::ClearableMutexGuard aGuard(This->m_aMutex); + std::vector< std::pair< SelectionAdaptor*, css::uno::Reference< XInterface > > > aChangeVector; + + for (auto const& selection : This->m_aSelections) + { + if( selection.first != This->m_nXdndSelection && ! selection.second->m_bOwner ) + { + ::Window aOwner = XGetSelectionOwner( This->m_pDisplay, selection.first ); + if( aOwner != selection.second->m_aLastOwner ) + { + selection.second->m_aLastOwner = aOwner; + std::pair< SelectionAdaptor*, css::uno::Reference< XInterface > > + aKeep( selection.second->m_pAdaptor, selection.second->m_pAdaptor->getReference() ); + aChangeVector.push_back( aKeep ); + } + } + } + aGuard.clear(); + for (auto const& change : aChangeVector) + { + change.first->fireContentsChanged(); + } + aLast = aNow; + } + } + // close write end on exit so write() fails and other thread does not block + // forever + close(This->m_EndThreadPipe[1]); + close(This->m_EndThreadPipe[0]); +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "SelectionManager::run end."); +#endif +} + +void SelectionManager::shutdown() noexcept +{ +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "SelectionManager got app termination event."); +#endif + + osl::ResettableMutexGuard aGuard(m_aMutex); + + if( m_bShutDown ) + return; + m_bShutDown = true; + + if ( m_xDesktop.is() ) + m_xDesktop->removeTerminateListener(this); + + if( m_xDisplayConnection.is() ) + m_xDisplayConnection->removeEventHandler(Any(), this); + + // stop dispatching + if( m_aThread ) + { + osl_terminateThread( m_aThread ); + /* + * Allow thread to finish before app exits to avoid pulling the carpet + * out from under it if pasting is occurring during shutdown + * + * a) allow it to have the Mutex and + * b) reschedule to allow it to complete callbacks to any + * Application::GetSolarMutex protected regions, etc. e.g. + * TransferableHelper::getTransferDataFlavors (via + * SelectionManager::handleSelectionRequest) which it might + * currently be trying to enter. + * + * Otherwise the thread may be left still waiting on a GlobalMutex + * when that gets destroyed, letting the thread blow up and die + * when enters the section in a now dead OOo instance. + */ + aGuard.clear(); + while (osl_isThreadRunning(m_aThread)) + { + { // drop mutex before write - otherwise may deadlock + SolarMutexGuard guard2; + Application::Reschedule(); + } + // trigger poll()'s wait end by writing a dummy value + char dummy=0; + dummy = write(m_EndThreadPipe[1], &dummy, 1); + } + osl_joinWithThread( m_aThread ); + osl_destroyThread( m_aThread ); + m_aThread = nullptr; + aGuard.reset(); + } + m_xDesktop.clear(); + m_xDisplayConnection.clear(); + m_xDropTransferable.clear(); +} + +sal_Bool SelectionManager::handleEvent(const Any& event) +{ + Sequence< sal_Int8 > aSeq; + if( event >>= aSeq ) + { + XEvent* pEvent = reinterpret_cast(aSeq.getArray()); + Time nTimestamp = CurrentTime; + if( pEvent->type == ButtonPress || pEvent->type == ButtonRelease ) + nTimestamp = pEvent->xbutton.time; + else if( pEvent->type == KeyPress || pEvent->type == KeyRelease ) + nTimestamp = pEvent->xkey.time; + else if( pEvent->type == MotionNotify ) + nTimestamp = pEvent->xmotion.time; + else if( pEvent->type == PropertyNotify ) + nTimestamp = pEvent->xproperty.time; + + if( nTimestamp != CurrentTime ) + { + osl::MutexGuard aGuard(m_aMutex); + + m_nSelectionTimestamp = nTimestamp; + } + + return handleXEvent( *pEvent ); + } + else + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "SelectionManager got downing event."); +#endif + shutdown(); + } + return true; +} + +void SAL_CALL SelectionManager::disposing( const css::lang::EventObject& rEvt ) +{ + if (rEvt.Source == m_xDesktop || rEvt.Source == m_xDisplayConnection) + shutdown(); +} + +void SAL_CALL SelectionManager::queryTermination( const css::lang::EventObject& ) +{ +} + +/* + * To be safe, shutdown needs to be called before the ~SfxApplication is called, waiting until + * the downing event can be too late if paste are requested during shutdown and ~SfxApplication + * has been called before vcl is shutdown + */ +void SAL_CALL SelectionManager::notifyTermination( const css::lang::EventObject& rEvent ) +{ + disposing(rEvent); +} + +void SelectionManager::registerHandler( Atom selection, SelectionAdaptor& rAdaptor ) +{ + osl::MutexGuard aGuard(m_aMutex); + + Selection* pNewSelection = new Selection(); + pNewSelection->m_pAdaptor = &rAdaptor; + m_aSelections[ selection ] = pNewSelection; +} + +void SelectionManager::deregisterHandler( Atom selection ) +{ + osl::MutexGuard aGuard(m_aMutex); + + std::unordered_map< Atom, Selection* >::iterator it = + m_aSelections.find( selection ); + if( it != m_aSelections.end() ) + { + delete it->second->m_pPixmap; + delete it->second; + m_aSelections.erase( it ); + } +} + +static bool bWasError = false; + +extern "C" +{ + static int local_xerror_handler(Display* , XErrorEvent*) + { + bWasError = true; + return 0; + } + typedef int(*xerror_hdl_t)(Display*,XErrorEvent*); +} + +void SelectionManager::registerDropTarget( ::Window aWindow, DropTarget* pTarget ) +{ + osl::MutexGuard aGuard(m_aMutex); + + // sanity check + std::unordered_map< ::Window, DropTargetEntry >::const_iterator it = + m_aDropTargets.find( aWindow ); + if( it != m_aDropTargets.end() ) + OSL_FAIL( "attempt to register window as drop target twice" ); + else if( aWindow && m_pDisplay ) + { + DropTargetEntry aEntry( pTarget ); + bWasError=false; + /* #i100000# ugly workaround: gtk sets its own XErrorHandler which is not suitable for us + unfortunately XErrorHandler is not per display, so this is just and ugly hack + Need to remove separate display and integrate clipboard/dnd into vcl's unx code ASAP + */ + xerror_hdl_t pOldHandler = XSetErrorHandler( local_xerror_handler ); + XSelectInput( m_pDisplay, aWindow, PropertyChangeMask ); + if( ! bWasError ) + { + // set XdndAware + XChangeProperty( m_pDisplay, aWindow, m_nXdndAware, XA_ATOM, 32, PropModeReplace, reinterpret_cast(&nXdndProtocolRevision), 1 ); + if( ! bWasError ) + { + // get root window of window (in 99.999% of all cases this will be + // DefaultRootWindow( m_pDisplay ) + int x, y; + unsigned int w, h, bw, d; + XGetGeometry( m_pDisplay, aWindow, &aEntry.m_aRootWindow, + &x, &y, &w, &h, &bw, &d ); + } + } + XSetErrorHandler( pOldHandler ); + if(bWasError) + return; + m_aDropTargets[ aWindow ] = aEntry; + } + else + OSL_FAIL( "attempt to register None as drop target" ); +} + +void SelectionManager::deregisterDropTarget( ::Window aWindow ) +{ + osl::ResettableGuard aGuard(m_aMutex); + + m_aDropTargets.erase( aWindow ); + if( aWindow != m_aDragSourceWindow || !m_aDragRunning.check() ) + return; + + // abort drag + std::unordered_map< ::Window, DropTargetEntry >::const_iterator it = + m_aDropTargets.find( m_aDropWindow ); + if( it != m_aDropTargets.end() ) + { + DropTargetEvent dte; + dte.Source = it->second.m_pTarget->getXWeak(); + aGuard.clear(); + it->second.m_pTarget->dragExit( dte ); + aGuard.reset(); + } + else if( m_aDropProxy != None && m_nCurrentProtocolVersion >= 0 ) + { + // send XdndLeave + XEvent aEvent; + aEvent.type = ClientMessage; + aEvent.xclient.display = m_pDisplay; + aEvent.xclient.format = 32; + aEvent.xclient.message_type = m_nXdndLeave; + aEvent.xclient.window = m_aDropWindow; + aEvent.xclient.data.l[0] = m_aWindow; + memset( aEvent.xclient.data.l+1, 0, sizeof(long)*4); + m_aDropWindow = m_aDropProxy = None; + XSendEvent( m_pDisplay, m_aDropProxy, False, NoEventMask, &aEvent ); + } + // notify the listener + DragSourceDropEvent dsde; + dsde.Source = getXWeak(); + dsde.DragSourceContext = new DragSourceContext( m_aDropWindow, *this ); + dsde.DragSource = static_cast< XDragSource* >(this); + dsde.DropAction = DNDConstants::ACTION_NONE; + dsde.DropSuccess = false; + css::uno::Reference< XDragSourceListener > xListener( m_xDragSourceListener ); + m_xDragSourceListener.clear(); + aGuard.clear(); + xListener->dragDropEnd( dsde ); +} + +/* + * SelectionAdaptor + */ + +css::uno::Reference< XTransferable > SelectionManager::getTransferable() noexcept +{ + return m_xDragSourceTransferable; +} + +void SelectionManager::clearTransferable() noexcept +{ + m_xDragSourceTransferable.clear(); +} + +void SelectionManager::fireContentsChanged() noexcept +{ +} + +css::uno::Reference< XInterface > SelectionManager::getReference() noexcept +{ + return getXWeak(); +} + +/* + * SelectionManagerHolder + */ + +SelectionManagerHolder::SelectionManagerHolder() : + ::cppu::WeakComponentImplHelper< + XDragSource, + XInitialization, + XServiceInfo > (m_aMutex) +{ +} + +SelectionManagerHolder::~SelectionManagerHolder() +{ +} + +void SelectionManagerHolder::initialize( const Sequence< Any >& arguments ) +{ + OUString aDisplayName; + + if( arguments.hasElements() ) + { + css::uno::Reference< XDisplayConnection > xConn; + arguments.getConstArray()[0] >>= xConn; + if( xConn.is() ) + { + Any aIdentifier; + aIdentifier >>= aDisplayName; + } + } + + SelectionManager& rManager = SelectionManager::get( aDisplayName ); + rManager.initialize( arguments ); + m_xRealDragSource = static_cast< XDragSource* >(&rManager); +} + +/* + * XDragSource + */ + +sal_Bool SelectionManagerHolder::isDragImageSupported() +{ + return m_xRealDragSource.is() && m_xRealDragSource->isDragImageSupported(); +} + +sal_Int32 SelectionManagerHolder::getDefaultCursor( sal_Int8 dragAction ) +{ + return m_xRealDragSource.is() ? m_xRealDragSource->getDefaultCursor( dragAction ) : 0; +} + +void SelectionManagerHolder::startDrag( + const css::datatransfer::dnd::DragGestureEvent& trigger, + sal_Int8 sourceActions, sal_Int32 cursor, sal_Int32 image, + const css::uno::Reference< css::datatransfer::XTransferable >& transferable, + const css::uno::Reference< css::datatransfer::dnd::XDragSourceListener >& listener + ) +{ + if( m_xRealDragSource.is() ) + m_xRealDragSource->startDrag( trigger, sourceActions, cursor, image, transferable, listener ); +} + +/* + * XServiceInfo + */ + +OUString SelectionManagerHolder::getImplementationName() +{ + return "com.sun.star.datatransfer.dnd.XdndSupport"; +} + +sal_Bool SelectionManagerHolder::supportsService( const OUString& ServiceName ) +{ + return cppu::supportsService(this, ServiceName); +} + +Sequence< OUString > SelectionManagerHolder::getSupportedServiceNames() +{ + return Xdnd_getSupportedServiceNames(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/X11_selection.hxx b/vcl/unx/generic/dtrans/X11_selection.hxx new file mode 100644 index 0000000000..bbfe07e5f6 --- /dev/null +++ b/vcl/unx/generic/dtrans/X11_selection.hxx @@ -0,0 +1,496 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + + +namespace x11 { + + class PixmapHolder; // in bmp.hxx + class SelectionManager; + + rtl_TextEncoding getTextPlainEncoding( const OUString& rMimeType ); + + class SelectionAdaptor + { + public: + virtual css::uno::Reference< css::datatransfer::XTransferable > getTransferable() = 0; + virtual void clearTransferable() = 0; + virtual void fireContentsChanged() = 0; + virtual css::uno::Reference< css::uno::XInterface > getReference() = 0; + // returns a reference that will keep the SelectionAdaptor alive until the + // reference is released + + protected: + ~SelectionAdaptor() {} + }; + + class DropTarget : + public ::cppu::WeakComponentImplHelper< + css::datatransfer::dnd::XDropTarget, + css::lang::XInitialization, + css::lang::XServiceInfo + > + { + public: + ::osl::Mutex m_aMutex; + bool m_bActive; + sal_Int8 m_nDefaultActions; + ::Window m_aTargetWindow; + rtl::Reference + m_xSelectionManager; + ::std::vector< css::uno::Reference< css::datatransfer::dnd::XDropTargetListener > > + m_aListeners; + + DropTarget(); + virtual ~DropTarget() override; + + // convenience functions that loop over listeners + void dragEnter( const css::datatransfer::dnd::DropTargetDragEnterEvent& dtde ) noexcept; + void dragExit( const css::datatransfer::dnd::DropTargetEvent& dte ) noexcept; + void dragOver( const css::datatransfer::dnd::DropTargetDragEvent& dtde ) noexcept; + void drop( const css::datatransfer::dnd::DropTargetDropEvent& dtde ) noexcept; + + // XInitialization + virtual void SAL_CALL initialize( const css::uno::Sequence< css::uno::Any >& args ) override; + + // XDropTarget + virtual void SAL_CALL addDropTargetListener( const css::uno::Reference< css::datatransfer::dnd::XDropTargetListener >& ) override; + virtual void SAL_CALL removeDropTargetListener( const css::uno::Reference< css::datatransfer::dnd::XDropTargetListener >& ) override; + virtual sal_Bool SAL_CALL isActive() override; + virtual void SAL_CALL setActive( sal_Bool active ) override; + virtual sal_Int8 SAL_CALL getDefaultActions() override; + virtual void SAL_CALL setDefaultActions( sal_Int8 actions ) override; + + // XServiceInfo + virtual OUString SAL_CALL getImplementationName() override; + virtual sal_Bool SAL_CALL supportsService( const OUString& ServiceName ) override; + virtual css::uno::Sequence< OUString > + SAL_CALL getSupportedServiceNames() override; + }; + + class SelectionManagerHolder : + public ::cppu::WeakComponentImplHelper< + css::datatransfer::dnd::XDragSource, + css::lang::XInitialization, + css::lang::XServiceInfo + > + { + ::osl::Mutex m_aMutex; + css::uno::Reference< css::datatransfer::dnd::XDragSource > + m_xRealDragSource; + public: + SelectionManagerHolder(); + virtual ~SelectionManagerHolder() override; + + // XServiceInfo + virtual OUString SAL_CALL getImplementationName() override; + virtual sal_Bool SAL_CALL supportsService( const OUString& ServiceName ) override; + virtual css::uno::Sequence< OUString > + SAL_CALL getSupportedServiceNames() override; + + // XInitialization + virtual void SAL_CALL initialize( const css::uno::Sequence< css::uno::Any >& arguments ) override; + + // XDragSource + virtual sal_Bool SAL_CALL isDragImageSupported() override; + virtual sal_Int32 SAL_CALL getDefaultCursor( sal_Int8 dragAction ) override; + virtual void SAL_CALL startDrag( + const css::datatransfer::dnd::DragGestureEvent& trigger, + sal_Int8 sourceActions, sal_Int32 cursor, sal_Int32 image, + const css::uno::Reference< css::datatransfer::XTransferable >& transferable, + const css::uno::Reference< css::datatransfer::dnd::XDragSourceListener >& listener + ) override; + + }; + + class SelectionManager : + public ::cppu::WeakImplHelper< + css::datatransfer::dnd::XDragSource, + css::lang::XInitialization, + css::awt::XEventHandler, + css::frame::XTerminateListener + >, + public SelectionAdaptor + { + static std::unordered_map< OUString, SelectionManager* >& getInstances(); + + // for INCR type selection transfer + // INCR protocol is used if the data cannot + // be transported at once but in parts + // IncrementalTransfer holds the bytes to be transmitted + // as well as the current position + // INCR triggers the delivery of the next part by deleting the + // property used to transfer the data + struct IncrementalTransfer + { + css::uno::Sequence< sal_Int8 > m_aData; + int m_nBufferPos; + ::Window m_aRequestor; + Atom m_aProperty; + Atom m_aTarget; + int m_nFormat; + time_t m_nTransferStartTime; + }; + int m_nIncrementalThreshold; + + // a struct to hold the data associated with a selection + struct Selection + { + enum State + { + Inactive, WaitingForResponse, WaitingForData, IncrementalTransfer + }; + + State m_eState; + SelectionAdaptor* m_pAdaptor; + ::osl::Condition m_aDataArrived; + css::uno::Sequence< sal_Int8 > m_aData; + css::uno::Sequence< css::datatransfer::DataFlavor > + m_aTypes; + std::vector< Atom > m_aNativeTypes; + // this is used for caching + // m_aTypes is invalid after 2 seconds + // m_aNativeTypes contains the corresponding original atom + Atom m_aRequestedType; + // m_aRequestedType is only valid while WaitingForResponse and WaitingFotData + time_t m_nLastTimestamp; + bool m_bHaveUTF16; + Atom m_aUTF8Type; + bool m_bHaveCompound; + bool m_bOwner; + ::Window m_aLastOwner; + PixmapHolder* m_pPixmap; + // m_nOrigTimestamp contains the Timestamp at which the selection + // was acquired; needed for TimeSTAMP target + Time m_nOrigTimestamp; + + Selection() : m_eState( Inactive ), + m_pAdaptor( nullptr ), + m_aRequestedType( None ), + m_nLastTimestamp( 0 ), + m_bHaveUTF16( false ), + m_aUTF8Type( None ), + m_bHaveCompound( false ), + m_bOwner( false ), + m_aLastOwner( None ), + m_pPixmap( nullptr ), + m_nOrigTimestamp( CurrentTime ) + {} + }; + + // a struct to hold data associated with a XDropTarget + struct DropTargetEntry + { + DropTarget* m_pTarget; + ::Window m_aRootWindow; + + DropTargetEntry() : m_pTarget( nullptr ), m_aRootWindow( None ) {} + explicit DropTargetEntry( DropTarget* pTarget ) : + m_pTarget( pTarget ), + m_aRootWindow( None ) + {} + DropTargetEntry( const DropTargetEntry& rEntry ) : + m_pTarget( rEntry.m_pTarget ), + m_aRootWindow( rEntry.m_aRootWindow ) + {} + + DropTarget* operator->() const { return m_pTarget; } + DropTargetEntry& operator=(const DropTargetEntry& rEntry) + { m_pTarget = rEntry.m_pTarget; m_aRootWindow = rEntry.m_aRootWindow; return *this; } + }; + + // internal data + Display* m_pDisplay; + oslThread m_aThread; + int m_EndThreadPipe[2]; + oslThread m_aDragExecuteThread; + ::osl::Condition m_aDragRunning; + ::Window m_aWindow; + css::uno::Reference< css::frame::XDesktop2 > m_xDesktop; + css::uno::Reference< css::awt::XDisplayConnection > + m_xDisplayConnection; + sal_Int32 m_nSelectionTimeout; + Time m_nSelectionTimestamp; + + // members used for Xdnd + + // drop only + + // contains the XdndEnterEvent of a drop action running + // with one of our targets. The data.l[0] member + // (containing the drag source ::Window) is set + // to None while that is not the case + XClientMessageEvent m_aDropEnterEvent; + // set to false on XdndEnter + // set to true on first XdndPosition or XdndLeave + bool m_bDropEnterSent; + ::Window m_aCurrentDropWindow; + // Time code of XdndDrop + Time m_nDropTime; + sal_Int8 m_nLastDropAction; + // XTransferable for Xdnd with foreign drag source + css::uno::Reference< css::datatransfer::XTransferable > + m_xDropTransferable; + int m_nLastX, m_nLastY; + // set to true when calling drop() + // if another XdndEnter is received this shows that + // someone forgot to call dropComplete - we should reset + // and react to the new drop + bool m_bDropWaitingForCompletion; + + // drag only + + // None if no Dnd action is running with us as source + ::Window m_aDropWindow; + // either m_aDropWindow or its XdndProxy + ::Window m_aDropProxy; + ::Window m_aDragSourceWindow; + // XTransferable for Xdnd when we are drag source + css::uno::Reference< css::datatransfer::XTransferable > + m_xDragSourceTransferable; + css::uno::Reference< css::datatransfer::dnd::XDragSourceListener > + m_xDragSourceListener; + // root coordinates + int m_nLastDragX, m_nLastDragY; + css::uno::Sequence< css::datatransfer::DataFlavor > + m_aDragFlavors; + // the rectangle the pointer must leave until a new XdndPosition should + // be sent. empty unless the drop target told to fill + int m_nNoPosX, m_nNoPosY, m_nNoPosWidth, m_nNoPosHeight; + unsigned int m_nDragButton; + sal_Int8 m_nUserDragAction; + sal_Int8 m_nTargetAcceptAction; + sal_Int8 m_nSourceActions; + bool m_bLastDropAccepted; + bool m_bDropSuccess; + bool m_bDropSent; + time_t m_nDropTimeout; + bool m_bWaitingForPrimaryConversion; + + // drag cursors + Cursor m_aMoveCursor; + Cursor m_aCopyCursor; + Cursor m_aLinkCursor; + Cursor m_aNoneCursor; + Cursor m_aCurrentCursor; + + // drag and drop + + int m_nCurrentProtocolVersion; + std::unordered_map< ::Window, DropTargetEntry > + m_aDropTargets; + + // some special atoms that are needed often + Atom m_nTARGETSAtom; + Atom m_nTIMESTAMPAtom; + Atom m_nTEXTAtom; + Atom m_nINCRAtom; + Atom m_nCOMPOUNDAtom; + Atom m_nMULTIPLEAtom; + Atom m_nImageBmpAtom; + Atom m_nXdndAware; + Atom m_nXdndEnter; + Atom m_nXdndLeave; + Atom m_nXdndPosition; + Atom m_nXdndStatus; + Atom m_nXdndDrop; + Atom m_nXdndFinished; + Atom m_nXdndSelection; + Atom m_nXdndTypeList; + Atom m_nXdndProxy; + Atom m_nXdndActionCopy; + Atom m_nXdndActionMove; + Atom m_nXdndActionLink; + Atom m_nXdndActionAsk; + + // caching for atoms + std::unordered_map< Atom, OUString > + m_aAtomToString; + std::unordered_map< OUString, Atom > + m_aStringToAtom; + + // the registered selections + std::unordered_map< Atom, Selection* > + m_aSelections; + // IncrementalTransfers in progress + std::unordered_map< ::Window, std::unordered_map< Atom, IncrementalTransfer > > + m_aIncrementals; + + // do not use X11 multithreading capabilities + // since this leads to deadlocks in different Xlib implementations + // (XFree as well as Xsun) use an own mutex instead + ::osl::Mutex m_aMutex; + bool m_bShutDown; + + SelectionManager(); + virtual ~SelectionManager() override; + + SelectionAdaptor* getAdaptor( Atom selection ); + PixmapHolder* getPixmapHolder( Atom selection ); + + // handle various events + bool handleSelectionRequest( XSelectionRequestEvent& rRequest ); + bool handleSendPropertyNotify( XPropertyEvent const & rNotify ); + bool handleReceivePropertyNotify( XPropertyEvent const & rNotify ); + bool handleSelectionNotify( XSelectionEvent const & rNotify ); + bool handleDragEvent( XEvent const & rMessage ); + bool handleDropEvent( XClientMessageEvent const & rMessage ); + + // dnd helpers + void sendDragStatus( Atom nDropAction ); + void sendDropPosition( bool bForce, Time eventTime ); + bool updateDragAction( int modifierState ); + int getXdndVersion( ::Window aXLIB_Window, ::Window& rProxy ); + Cursor createCursor( const unsigned char* pPointerData, const unsigned char* pMaskData, int width, int height, int hotX, int hotY ); + // coordinates on root ::Window + void updateDragWindow( int nX, int nY, ::Window aRoot ); + + bool getPasteData( Atom selection, Atom type, css::uno::Sequence< sal_Int8 >& rData ); + // returns true if conversion was successful + bool convertData( const css::uno::Reference< css::datatransfer::XTransferable >& xTransferable, + Atom nType, + Atom nSelection, + int & rFormat, + css::uno::Sequence< sal_Int8 >& rData ); + bool sendData( SelectionAdaptor* pAdaptor, ::Window requestor, Atom target, Atom property, Atom selection ); + + // thread dispatch loop + public: + // public for extern "C" stub + static void run( void* ); + private: + void dispatchEvent( int millisec ); + // drag thread dispatch + public: + // public for extern "C" stub + static void runDragExecute( void* ); + private: + void dragDoDispatch(); + bool handleXEvent( XEvent& rEvent ); + + // compound text conversion + OString convertToCompound( const OUString& rText ); + OUString convertFromCompound( const char* pText, int nLen ); + + sal_Int8 getUserDragAction() const; + sal_Int32 getSelectionTimeout(); + public: + static SelectionManager& get( const OUString& rDisplayName = OUString() ); + + Display * getDisplay() { return m_pDisplay; }; + + void registerHandler( Atom selection, SelectionAdaptor& rAdaptor ); + void deregisterHandler( Atom selection ); + bool requestOwnership( Atom selection ); + + // allow for synchronization over one mutex for XClipboard + osl::Mutex& getMutex() { return m_aMutex; } + + Atom getAtom( const OUString& rString ); + OUString getString( Atom nAtom ); + + // type conversion + // note: convertTypeToNative does NOT clear the list, so you can append + // multiple types to the same list + void convertTypeToNative( const OUString& rType, Atom selection, int& rFormat, ::std::list< Atom >& rConversions, bool bPushFront = false ); + OUString convertTypeFromNative( Atom nType, Atom selection, int& rFormat ); + void getNativeTypeList( const css::uno::Sequence< css::datatransfer::DataFlavor >& rTypes, std::list< Atom >& rOutTypeList, Atom targetselection ); + + // methods for transferable + bool getPasteDataTypes( Atom selection, css::uno::Sequence< css::datatransfer::DataFlavor >& rTypes ); + bool getPasteData( Atom selection, const OUString& rType, css::uno::Sequence< sal_Int8 >& rData ); + + // for XDropTarget to register/deregister itself + void registerDropTarget( ::Window aXLIB_Window, DropTarget* pTarget ); + void deregisterDropTarget( ::Window aXLIB_Window ); + + // for XDropTarget{Drag|Drop}Context + void accept( sal_Int8 dragOperation, ::Window aDropXLIB_Window ); + void reject( ::Window aDropXLIB_Window ); + void dropComplete( bool success, ::Window aDropXLIB_Window ); + + // for XDragSourceContext + sal_Int32 getCurrentCursor() const { return m_aCurrentCursor;} + void setCursor( sal_Int32 cursor, ::Window aDropXLIB_Window ); + void transferablesFlavorsChanged(); + + void shutdown() noexcept; + + // XInitialization + virtual void SAL_CALL initialize( const css::uno::Sequence< css::uno::Any >& arguments ) override; + + // XEventHandler + virtual sal_Bool SAL_CALL handleEvent(const css::uno::Any& event) override; + + // XDragSource + virtual sal_Bool SAL_CALL isDragImageSupported() override; + virtual sal_Int32 SAL_CALL getDefaultCursor( sal_Int8 dragAction ) override; + virtual void SAL_CALL startDrag( + const css::datatransfer::dnd::DragGestureEvent& trigger, + sal_Int8 sourceActions, sal_Int32 cursor, sal_Int32 image, + const css::uno::Reference< css::datatransfer::XTransferable >& transferable, + const css::uno::Reference< css::datatransfer::dnd::XDragSourceListener >& listener + ) override; + + // SelectionAdaptor for XdndSelection Drag (we are drag source) + virtual css::uno::Reference< css::datatransfer::XTransferable > getTransferable() noexcept override; + virtual void clearTransferable() noexcept override; + virtual void fireContentsChanged() noexcept override; + virtual css::uno::Reference< css::uno::XInterface > getReference() noexcept override; + + // XEventListener + virtual void SAL_CALL disposing( const css::lang::EventObject& Source ) override; + + // XTerminateListener + virtual void SAL_CALL queryTermination( const css::lang::EventObject& aEvent ) override; + virtual void SAL_CALL notifyTermination( const css::lang::EventObject& aEvent ) override; + }; + + css::uno::Sequence< OUString > Xdnd_getSupportedServiceNames(); + css::uno::Reference< css::uno::XInterface > SAL_CALL Xdnd_createInstance( + const css::uno::Reference< css::lang::XMultiServiceFactory > & xMultiServiceFactory); + + css::uno::Sequence< OUString > Xdnd_dropTarget_getSupportedServiceNames(); + css::uno::Reference< css::uno::XInterface > SAL_CALL Xdnd_dropTarget_createInstance( + const css::uno::Reference< css::lang::XMultiServiceFactory > & xMultiServiceFactory); + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/X11_service.cxx b/vcl/unx/generic/dtrans/X11_service.cxx new file mode 100644 index 0000000000..e633020b6d --- /dev/null +++ b/vcl/unx/generic/dtrans/X11_service.cxx @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include + +#include "X11_clipboard.hxx" +#include + +using namespace cppu; +using namespace com::sun::star::uno; +using namespace com::sun::star::lang; +using namespace com::sun::star::datatransfer::clipboard; +using namespace com::sun::star::awt; +using namespace x11; + +Sequence< OUString > x11::X11Clipboard_getSupportedServiceNames() +{ + return { "com.sun.star.datatransfer.clipboard.SystemClipboard" }; +} + +Sequence< OUString > x11::Xdnd_getSupportedServiceNames() +{ + return { "com.sun.star.datatransfer.dnd.X11DragSource" }; +} + +Sequence< OUString > x11::Xdnd_dropTarget_getSupportedServiceNames() +{ + return { "com.sun.star.datatransfer.dnd.X11DropTarget" }; +} + +css::uno::Reference< XInterface > X11SalInstance::CreateClipboard( const Sequence< Any >& arguments ) +{ + if ( IsRunningUnitTest() ) + return SalInstance::CreateClipboard( arguments ); + + SelectionManager& rManager = SelectionManager::get(); + css::uno::Sequence mgrArgs{ css::uno::Any(Application::GetDisplayConnection()) }; + rManager.initialize(mgrArgs); + + OUString sel; + if (!arguments.hasElements()) { + sel = "CLIPBOARD"; + } else if (arguments.getLength() != 1 || !(arguments[0] >>= sel)) { + throw css::lang::IllegalArgumentException( + "bad X11SalInstance::CreateClipboard arguments", + css::uno::Reference(), -1); + } + Atom nSelection = rManager.getAtom(sel); + + std::unordered_map< Atom, css::uno::Reference< XClipboard > >::iterator it = m_aInstances.find( nSelection ); + if( it != m_aInstances.end() ) + return it->second; + + css::uno::Reference pClipboard = X11Clipboard::create( rManager, nSelection ); + m_aInstances[ nSelection ] = pClipboard; + + return pClipboard; +} + +css::uno::Reference X11SalInstance::ImplCreateDragSource(const SystemEnvData* pSysEnv) +{ + return vcl::X11DnDHelper(new SelectionManagerHolder(), pSysEnv->aShellWindow); +} + +css::uno::Reference X11SalInstance::ImplCreateDropTarget(const SystemEnvData* pSysEnv) +{ + return vcl::X11DnDHelper(new DropTarget(), pSysEnv->aShellWindow); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/X11_transferable.cxx b/vcl/unx/generic/dtrans/X11_transferable.cxx new file mode 100644 index 0000000000..a6ad1b4a15 --- /dev/null +++ b/vcl/unx/generic/dtrans/X11_transferable.cxx @@ -0,0 +1,101 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "X11_transferable.hxx" +#include +#include +#include +#include + +using namespace com::sun::star::datatransfer; +using namespace com::sun::star::lang; +using namespace com::sun::star::io; +using namespace com::sun::star::uno; +using namespace cppu; +using namespace osl; + +using namespace x11; + +X11Transferable::X11Transferable( + SelectionManager& rManager, + Atom selection + ) : + m_rManager( rManager ), + m_aSelection( selection ) +{ +} + +X11Transferable::~X11Transferable() +{ +} + +Any SAL_CALL X11Transferable::getTransferData( const DataFlavor& rFlavor ) +{ + Any aRet; + Sequence< sal_Int8 > aData; + bool bSuccess = m_rManager.getPasteData( m_aSelection ? m_aSelection : XA_PRIMARY, rFlavor.MimeType, aData ); + if( ! bSuccess && m_aSelection == 0 ) + bSuccess = m_rManager.getPasteData( m_rManager.getAtom( "CLIPBOARD" ), rFlavor.MimeType, aData ); + + if( ! bSuccess ) + { + throw UnsupportedFlavorException( rFlavor.MimeType, static_cast < XTransferable * > ( this ) ); + } + if( rFlavor.MimeType.equalsIgnoreAsciiCase( "text/plain;charset=utf-16" ) ) + { + int nLen = aData.getLength()/2; + if( reinterpret_cast(aData.getConstArray())[nLen-1] == 0 ) + nLen--; + OUString aString( reinterpret_cast(aData.getConstArray()), nLen ); + SAL_INFO( "vcl.unx.dtrans", "X11Transferable::getTransferData( \"" << rFlavor.MimeType << "\" )\n -> \"" << aString << "\""); + aRet <<= aString.replaceAll("\r\n", "\n"); + } + else + aRet <<= aData; + return aRet; +} + +Sequence< DataFlavor > SAL_CALL X11Transferable::getTransferDataFlavors() +{ + Sequence< DataFlavor > aFlavorList; + bool bSuccess = m_rManager.getPasteDataTypes( m_aSelection ? m_aSelection : XA_PRIMARY, aFlavorList ); + if( ! bSuccess && m_aSelection == 0 ) + m_rManager.getPasteDataTypes( m_rManager.getAtom( "CLIPBOARD" ), aFlavorList ); + + return aFlavorList; +} + +sal_Bool SAL_CALL X11Transferable::isDataFlavorSupported( const DataFlavor& aFlavor ) +{ + if( aFlavor.DataType != cppu::UnoType>::get() ) + { + if( ! aFlavor.MimeType.equalsIgnoreAsciiCase( "text/plain;charset=utf-16" ) && + aFlavor.DataType == cppu::UnoType::get() ) + return false; + } + + const Sequence< DataFlavor > aFlavors( getTransferDataFlavors() ); + return std::any_of(aFlavors.begin(), aFlavors.end(), + [&aFlavor](const DataFlavor& rFlavor) { + return aFlavor.MimeType.equalsIgnoreAsciiCase( rFlavor.MimeType ) + && aFlavor.DataType == rFlavor.DataType; + }); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/X11_transferable.hxx b/vcl/unx/generic/dtrans/X11_transferable.hxx new file mode 100644 index 0000000000..23cb9dd373 --- /dev/null +++ b/vcl/unx/generic/dtrans/X11_transferable.hxx @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include "X11_selection.hxx" +#include + +#include + +namespace x11 { + + class X11Transferable : public ::cppu::WeakImplHelper< css::datatransfer::XTransferable > + { + SelectionManager& m_rManager; + Atom m_aSelection; + public: + X11Transferable( SelectionManager& rManager, Atom selection ); + virtual ~X11Transferable() override; + + /* + * XTransferable + */ + + virtual css::uno::Any SAL_CALL getTransferData( const css::datatransfer::DataFlavor& aFlavor ) override; + + virtual css::uno::Sequence< css::datatransfer::DataFlavor > SAL_CALL getTransferDataFlavors( ) override; + + virtual sal_Bool SAL_CALL isDataFlavorSupported( const css::datatransfer::DataFlavor& aFlavor ) override; + }; + +} // namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/bmp.cxx b/vcl/unx/generic/dtrans/bmp.cxx new file mode 100644 index 0000000000..ac8e50cc2e --- /dev/null +++ b/vcl/unx/generic/dtrans/bmp.cxx @@ -0,0 +1,786 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include "bmp.hxx" + +using namespace x11; + +/* + * helper functions + */ + +static void writeLE( sal_uInt16 nNumber, sal_uInt8* pBuffer ) +{ + pBuffer[ 0 ] = (nNumber & 0xff); + pBuffer[ 1 ] = ((nNumber>>8)&0xff); +} + +static void writeLE( sal_uInt32 nNumber, sal_uInt8* pBuffer ) +{ + pBuffer[ 0 ] = (nNumber & 0xff); + pBuffer[ 1 ] = ((nNumber>>8)&0xff); + pBuffer[ 2 ] = ((nNumber>>16)&0xff); + pBuffer[ 3 ] = ((nNumber>>24)&0xff); +} + +static sal_uInt16 readLE16( const sal_uInt8* pBuffer ) +{ + //This is untainted data which comes from a controlled source + //so, using a byte-swapping pattern which coverity doesn't + //detect as such + //http://security.coverity.com/blog/2014/Apr/on-detecting-heartbleed-with-static-analysis.html + sal_uInt16 v = pBuffer[1]; v <<= 8; + v |= pBuffer[0]; + return v; +} + +static sal_uInt32 readLE32( const sal_uInt8* pBuffer ) +{ + //This is untainted data which comes from a controlled source + //so, using a byte-swapping pattern which coverity doesn't + //detect as such + //http://security.coverity.com/blog/2014/Apr/on-detecting-heartbleed-with-static-analysis.html + sal_uInt32 v = pBuffer[3]; v <<= 8; + v |= pBuffer[2]; v <<= 8; + v |= pBuffer[1]; v <<= 8; + v |= pBuffer[0]; + return v; +} + +/* + * scanline helpers + */ + +static void X11_writeScanlinePixel( unsigned long nColor, sal_uInt8* pScanline, int depth, int x ) +{ + switch( depth ) + { + case 1: + pScanline[ x/8 ] &= ~(1 << (x&7)); + pScanline[ x/8 ] |= ((nColor & 1) << (x&7)); + break; + case 4: + pScanline[ x/2 ] &= ((x&1) ? 0x0f : 0xf0); + pScanline[ x/2 ] |= ((x&1) ? (nColor & 0x0f) : ((nColor & 0x0f) << 4)); + break; + default: + case 8: + pScanline[ x ] = (nColor & 0xff); + break; + } +} + +static sal_uInt8* X11_getPaletteBmpFromImage( + Display* pDisplay, + XImage* pImage, + Colormap aColormap, + sal_Int32& rOutSize + ) +{ + sal_uInt32 nColors = 0; + + rOutSize = 0; + + sal_uInt8* pBuffer = nullptr; + sal_uInt32 nHeaderSize, nScanlineSize; + sal_uInt16 nBitCount; + // determine header and scanline size + switch( pImage->depth ) + { + case 1: + nHeaderSize = 64; + nScanlineSize = (pImage->width+31)/32; + nBitCount = 1; + break; + case 4: + nHeaderSize = 72; + nScanlineSize = (pImage->width+1)/2; + nBitCount = 4; + break; + default: + case 8: + nHeaderSize = 1084; + nScanlineSize = pImage->width; + nBitCount = 8; + break; + } + // adjust scan lines to begin on %4 boundaries + if( nScanlineSize & 3 ) + { + nScanlineSize &= 0xfffffffc; + nScanlineSize += 4; + } + + // allocate buffer to hold header and scanlines, initialize to zero + rOutSize = nHeaderSize + nScanlineSize*pImage->height; + pBuffer = static_cast(rtl_allocateZeroMemory( rOutSize )); + for( int y = 0; y < pImage->height; y++ ) + { + sal_uInt8* pScanline = pBuffer + nHeaderSize + (pImage->height-1-y)*nScanlineSize; + for( int x = 0; x < pImage->width; x++ ) + { + unsigned long nPixel = XGetPixel( pImage, x, y ); + if( nPixel >= nColors ) + nColors = nPixel+1; + X11_writeScanlinePixel( nPixel, pScanline, pImage->depth, x ); + } + } + + // fill in header fields + pBuffer[ 0 ] = 'B'; + pBuffer[ 1 ] = 'M'; + + writeLE( nHeaderSize, pBuffer+10 ); + writeLE( sal_uInt32(40), pBuffer+14 ); + writeLE( static_cast(pImage->width), pBuffer+18 ); + writeLE( static_cast(pImage->height), pBuffer+22 ); + writeLE( sal_uInt16(1), pBuffer+26 ); + writeLE( nBitCount, pBuffer+28 ); + writeLE( static_cast(DisplayWidth(pDisplay,DefaultScreen(pDisplay))*1000/DisplayWidthMM(pDisplay,DefaultScreen(pDisplay))), pBuffer+38); + writeLE( static_cast(DisplayHeight(pDisplay,DefaultScreen(pDisplay))*1000/DisplayHeightMM(pDisplay,DefaultScreen(pDisplay))), pBuffer+42); + writeLE( nColors, pBuffer+46 ); + writeLE( nColors, pBuffer+50 ); + + XColor aColors[256]; + if( nColors > (1U << nBitCount) ) // paranoia + nColors = (1U << nBitCount); + for( unsigned long nPixel = 0; nPixel < nColors; nPixel++ ) + { + aColors[nPixel].flags = DoRed | DoGreen | DoBlue; + aColors[nPixel].pixel = nPixel; + } + XQueryColors( pDisplay, aColormap, aColors, nColors ); + for( sal_uInt32 i = 0; i < nColors; i++ ) + { + pBuffer[ 54 + i*4 ] = static_cast(aColors[i].blue >> 8); + pBuffer[ 55 + i*4 ] = static_cast(aColors[i].green >> 8); + pBuffer[ 56 + i*4 ] = static_cast(aColors[i].red >> 8); + } + + // done + + return pBuffer; +} + +static unsigned long doRightShift( unsigned long nValue, int nShift ) +{ + return (nShift > 0) ? (nValue >> nShift) : (nValue << (-nShift)); +} + +static unsigned long doLeftShift( unsigned long nValue, int nShift ) +{ + return (nShift > 0) ? (nValue << nShift) : (nValue >> (-nShift)); +} + +static void getShift( unsigned long nMask, int& rShift, int& rSigBits, int& rShift2 ) +{ + unsigned long nUseMask = nMask; + rShift = 0; + while( nMask & 0xffffff00 ) + { + rShift++; + nMask >>= 1; + } + if( rShift == 0 ) + while( ! (nMask & 0x00000080) ) + { + rShift--; + nMask <<= 1; + } + + int nRotate = sizeof(unsigned long)*8 - rShift; + rSigBits = 0; + nMask = doRightShift( nUseMask, rShift) ; + while( nRotate-- ) + { + if( nMask & 1 ) + rSigBits++; + nMask >>= 1; + } + + rShift2 = 0; + if( rSigBits < 8 ) + rShift2 = 8-rSigBits; +} + +static sal_uInt8* X11_getTCBmpFromImage( + Display* pDisplay, + XImage* pImage, + sal_Int32& rOutSize, + int nScreenNo + ) +{ + // get masks from visual info (guesswork) + XVisualInfo aVInfo; + if( ! XMatchVisualInfo( pDisplay, nScreenNo, pImage->depth, TrueColor, &aVInfo ) ) + return nullptr; + + rOutSize = 0; + + sal_uInt8* pBuffer = nullptr; + sal_uInt32 nHeaderSize = 60; + sal_uInt32 nScanlineSize = pImage->width*3; + + // adjust scan lines to begin on %4 boundaries + if( nScanlineSize & 3 ) + { + nScanlineSize &= 0xfffffffc; + nScanlineSize += 4; + } + int nRedShift, nRedSig, nRedShift2 = 0; + getShift( aVInfo.red_mask, nRedShift, nRedSig, nRedShift2 ); + int nGreenShift, nGreenSig, nGreenShift2 = 0; + getShift( aVInfo.green_mask, nGreenShift, nGreenSig, nGreenShift2 ); + int nBlueShift, nBlueSig, nBlueShift2 = 0; + getShift( aVInfo.blue_mask, nBlueShift, nBlueSig, nBlueShift2 ); + + // allocate buffer to hold header and scanlines, initialize to zero + rOutSize = nHeaderSize + nScanlineSize*pImage->height; + pBuffer = static_cast(rtl_allocateZeroMemory( rOutSize )); + for( int y = 0; y < pImage->height; y++ ) + { + sal_uInt8* pScanline = pBuffer + nHeaderSize + (pImage->height-1-y)*nScanlineSize; + for( int x = 0; x < pImage->width; x++ ) + { + unsigned long nPixel = XGetPixel( pImage, x, y ); + + sal_uInt8 nValue = static_cast(doRightShift( nPixel&aVInfo.blue_mask, nBlueShift)); + if( nBlueShift2 ) + nValue |= (nValue >> nBlueShift2 ); + *pScanline++ = nValue; + + nValue = static_cast(doRightShift( nPixel&aVInfo.green_mask, nGreenShift)); + if( nGreenShift2 ) + nValue |= (nValue >> nGreenShift2 ); + *pScanline++ = nValue; + + nValue = static_cast(doRightShift( nPixel&aVInfo.red_mask, nRedShift)); + if( nRedShift2 ) + nValue |= (nValue >> nRedShift2 ); + *pScanline++ = nValue; + } + } + + // fill in header fields + pBuffer[ 0 ] = 'B'; + pBuffer[ 1 ] = 'M'; + + writeLE( nHeaderSize, pBuffer+10 ); + writeLE( sal_uInt32(40), pBuffer+14 ); + writeLE( static_cast(pImage->width), pBuffer+18 ); + writeLE( static_cast(pImage->height), pBuffer+22 ); + writeLE( sal_uInt16(1), pBuffer+26 ); + writeLE( sal_uInt16(24), pBuffer+28 ); + writeLE( static_cast(DisplayWidth(pDisplay,DefaultScreen(pDisplay))*1000/DisplayWidthMM(pDisplay,DefaultScreen(pDisplay))), pBuffer+38); + writeLE( static_cast(DisplayHeight(pDisplay,DefaultScreen(pDisplay))*1000/DisplayHeightMM(pDisplay,DefaultScreen(pDisplay))), pBuffer+42); + + // done + + return pBuffer; +} + +sal_uInt8* x11::X11_getBmpFromPixmap( + Display* pDisplay, + Drawable aDrawable, + Colormap aColormap, + sal_Int32& rOutSize + ) +{ + // get geometry of drawable + ::Window aRoot; + int x,y; + unsigned int w, h, bw, d; + XGetGeometry( pDisplay, aDrawable, &aRoot, &x, &y, &w, &h, &bw, &d ); + + // find which screen we are on + int nScreenNo = ScreenCount( pDisplay ); + while( nScreenNo-- ) + { + if( RootWindow( pDisplay, nScreenNo ) == aRoot ) + break; + } + if( nScreenNo < 0 ) + return nullptr; + + if( aColormap == None ) + aColormap = DefaultColormap( pDisplay, nScreenNo ); + + // get the image + XImage* pImage = XGetImage( pDisplay, aDrawable, 0, 0, w, h, AllPlanes, ZPixmap ); + if( ! pImage ) + return nullptr; + + sal_uInt8* pBmp = d <= 8 ? + X11_getPaletteBmpFromImage( pDisplay, pImage, aColormap, rOutSize ) : + X11_getTCBmpFromImage( pDisplay, pImage, rOutSize, nScreenNo ); + XDestroyImage( pImage ); + + return pBmp; +} + +/* + * PixmapHolder + */ + +PixmapHolder::PixmapHolder( Display* pDisplay ) + : m_pDisplay(pDisplay) + , m_aColormap(None) + , m_aPixmap(None) + , m_aBitmap(None) + , m_nRedShift(0) + , m_nGreenShift(0) + , m_nBlueShift(0) + , m_nBlueShift2Mask(0) + , m_nRedShift2Mask(0) + , m_nGreenShift2Mask(0) +{ + /* try to get a 24 bit true color visual, if that fails, + * revert to default visual + */ + if( ! XMatchVisualInfo( m_pDisplay, DefaultScreen( m_pDisplay ), 24, TrueColor, &m_aInfo ) ) + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "PixmapHolder reverting to default visual."); +#endif + Visual* pVisual = DefaultVisual( m_pDisplay, DefaultScreen( m_pDisplay ) ); + m_aInfo.screen = DefaultScreen( m_pDisplay ); + m_aInfo.visual = pVisual; + m_aInfo.visualid = pVisual->visualid; + m_aInfo.c_class = pVisual->c_class; + m_aInfo.red_mask = pVisual->red_mask; + m_aInfo.green_mask = pVisual->green_mask; + m_aInfo.blue_mask = pVisual->blue_mask; + m_aInfo.depth = DefaultDepth( m_pDisplay, m_aInfo.screen ); + } + m_aColormap = DefaultColormap( m_pDisplay, m_aInfo.screen ); +#if OSL_DEBUG_LEVEL > 1 + static const char* pClasses[] = + { "StaticGray", "GrayScale", "StaticColor", "PseudoColor", "TrueColor", "DirectColor" }; + SAL_INFO("vcl.unx.dtrans", "PixmapHolder visual: id = " + << std::showbase << std::hex + << m_aInfo.visualid + << ", class = " + << ((m_aInfo.c_class >= 0 && + unsigned(m_aInfo.c_class) < + SAL_N_ELEMENTS(pClasses)) ? + pClasses[m_aInfo.c_class] : + "") + << " (" + << std::dec + << m_aInfo.c_class + << "), depth=" + << m_aInfo.depth + << "; color map = " + << std::showbase << std::hex + << m_aColormap); +#endif + if( m_aInfo.c_class != TrueColor ) + return; + + int nRedShift2(0); + int nGreenShift2(0); + int nBlueShift2(0); + int nRedSig, nGreenSig, nBlueSig; + getShift( m_aInfo.red_mask, m_nRedShift, nRedSig, nRedShift2 ); + getShift( m_aInfo.green_mask, m_nGreenShift, nGreenSig, nGreenShift2 ); + getShift( m_aInfo.blue_mask, m_nBlueShift, nBlueSig, nBlueShift2 ); + + m_nBlueShift2Mask = nBlueShift2 ? ~static_cast((1<((1<((1<(b); + nValue &= m_nBlueShift2Mask; + nPixel |= doLeftShift( nValue, m_nBlueShift ); + + nValue = static_cast(g); + nValue &= m_nGreenShift2Mask; + nPixel |= doLeftShift( nValue, m_nGreenShift ); + + nValue = static_cast(r); + nValue &= m_nRedShift2Mask; + nPixel |= doLeftShift( nValue, m_nRedShift ); + + return nPixel; +} + +void PixmapHolder::setBitmapDataPalette( const sal_uInt8* pData, XImage* pImage ) +{ + // setup palette + XColor aPalette[256]; + + sal_uInt32 nColors = readLE32( pData+32 ); + sal_uInt32 nWidth = readLE32( pData+4 ); + sal_uInt32 nHeight = readLE32( pData+8 ); + sal_uInt16 nDepth = readLE16( pData+14 ); + + for( sal_uInt32 i = 0 ; i < nColors; i++ ) + { + if( m_aInfo.c_class != TrueColor ) + { + //This is untainted data which comes from a controlled source + //so, using a byte-swapping pattern which coverity doesn't + //detect as such + //http://security.coverity.com/blog/2014/Apr/on-detecting-heartbleed-with-static-analysis.html + aPalette[i].red = static_cast(pData[42 + i*4]); + aPalette[i].red <<= 8; + aPalette[i].red |= static_cast(pData[42 + i*4]); + + aPalette[i].green = static_cast(pData[41 + i*4]); + aPalette[i].green <<= 8; + aPalette[i].green |= static_cast(pData[41 + i*4]); + + aPalette[i].blue = static_cast(pData[40 + i*4]); + aPalette[i].blue <<= 8; + aPalette[i].blue |= static_cast(pData[40 + i*4]); + XAllocColor( m_pDisplay, m_aColormap, aPalette+i ); + } + else + aPalette[i].pixel = getTCPixel( pData[42+i*4], pData[41+i*4], pData[40+i*4] ); + } + const sal_uInt8* pBMData = pData + readLE32( pData ) + 4*nColors; + + sal_uInt32 nScanlineSize = 0; + switch( nDepth ) + { + case 1: + nScanlineSize = (nWidth+31)/32; + break; + case 4: + nScanlineSize = (nWidth+1)/2; + break; + case 8: + nScanlineSize = nWidth; + break; + } + // adjust scan lines to begin on %4 boundaries + if( nScanlineSize & 3 ) + { + nScanlineSize &= 0xfffffffc; + nScanlineSize += 4; + } + + // allocate buffer to hold header and scanlines, initialize to zero + for( unsigned int y = 0; y < nHeight; y++ ) + { + const sal_uInt8* pScanline = pBMData + (nHeight-1-y)*nScanlineSize; + for( unsigned int x = 0; x < nWidth; x++ ) + { + int nCol = 0; + switch( nDepth ) + { + case 1: nCol = (pScanline[ x/8 ] & (0x80 >> (x&7))) != 0 ? 0 : 1; break; + case 4: + if( x & 1 ) + nCol = static_cast(pScanline[ x/2 ] >> 4); + else + nCol = static_cast(pScanline[ x/2 ] & 0x0f); + break; + case 8: nCol = static_cast(pScanline[x]); + } + XPutPixel( pImage, x, y, aPalette[nCol].pixel ); + } + } +} + +void PixmapHolder::setBitmapDataTCDither( const sal_uInt8* pData, XImage* pImage ) +{ + XColor aPalette[216]; + + int nNonAllocs = 0; + + for( int r = 0; r < 6; r++ ) + { + for( int g = 0; g < 6; g++ ) + { + for( int b = 0; b < 6; b++ ) + { + int i = r*36+g*6+b; + aPalette[i].red = r == 5 ? 0xffff : r*10922; + aPalette[i].green = g == 5 ? 0xffff : g*10922; + aPalette[i].blue = b == 5 ? 0xffff : b*10922; + aPalette[i].pixel = 0; + if( ! XAllocColor( m_pDisplay, m_aColormap, aPalette+i ) ) + nNonAllocs++; + } + } + } + + if( nNonAllocs ) + { + XColor aRealPalette[256]; + int nColors = 1 << m_aInfo.depth; + int i; + for( i = 0; i < nColors; i++ ) + aRealPalette[i].pixel = static_cast(i); + XQueryColors( m_pDisplay, m_aColormap, aRealPalette, nColors ); + for( i = 0; i < nColors; i++ ) + { + sal_uInt8 nIndex = + 36*static_cast(aRealPalette[i].red/10923) + + 6*static_cast(aRealPalette[i].green/10923) + + static_cast(aRealPalette[i].blue/10923); + if( aPalette[nIndex].pixel == 0 ) + aPalette[nIndex] = aRealPalette[i]; + } + } + + sal_uInt32 nWidth = readLE32( pData+4 ); + sal_uInt32 nHeight = readLE32( pData+8 ); + + const sal_uInt8* pBMData = pData + readLE32( pData ); + sal_uInt32 nScanlineSize = nWidth*3; + // adjust scan lines to begin on %4 boundaries + if( nScanlineSize & 3 ) + { + nScanlineSize &= 0xfffffffc; + nScanlineSize += 4; + } + + for( int y = 0; y < static_cast(nHeight); y++ ) + { + const sal_uInt8* pScanline = pBMData + (nHeight-1-static_cast(y))*nScanlineSize; + for( int x = 0; x < static_cast(nWidth); x++ ) + { + sal_uInt8 b = pScanline[3*x]; + sal_uInt8 g = pScanline[3*x+1]; + sal_uInt8 r = pScanline[3*x+2]; + sal_uInt8 i = 36*(r/43) + 6*(g/43) + (b/43); + + XPutPixel( pImage, x, y, aPalette[ i ].pixel ); + } + } +} + +void PixmapHolder::setBitmapDataTC( const sal_uInt8* pData, XImage* pImage ) +{ + sal_uInt32 nWidth = readLE32( pData+4 ); + sal_uInt32 nHeight = readLE32( pData+8 ); + + if (!nWidth || !nHeight) + return; + + const sal_uInt8* pBMData = pData + readLE32( pData ); + sal_uInt32 nScanlineSize = nWidth*3; + // adjust scan lines to begin on %4 boundaries + if( nScanlineSize & 3 ) + { + nScanlineSize &= 0xfffffffc; + nScanlineSize += 4; + } + + for( int y = 0; y < static_cast(nHeight); y++ ) + { + const sal_uInt8* pScanline = pBMData + (nHeight-1-static_cast(y))*nScanlineSize; + for( int x = 0; x < static_cast(nWidth); x++ ) + { + unsigned long nPixel = getTCPixel( pScanline[3*x+2], pScanline[3*x+1], pScanline[3*x] ); + XPutPixel( pImage, x, y, nPixel ); + } + } +} + +bool PixmapHolder::needsConversion( const sal_uInt8* pData ) const +{ + if( pData[0] != 'B' || pData[1] != 'M' ) + return true; + + pData = pData+14; + sal_uInt32 nDepth = readLE32( pData+14 ); + if( nDepth == 24 ) + { + if( m_aInfo.c_class != TrueColor ) + return true; + } + else if( nDepth != static_cast(m_aInfo.depth) ) + { + if( m_aInfo.c_class != TrueColor ) + return true; + } + + return false; +} + +Pixmap PixmapHolder::setBitmapData( const sal_uInt8* pData ) +{ + if( pData[0] != 'B' || pData[1] != 'M' ) + return None; + + pData = pData+14; + + // reject compressed data + if( readLE32( pData + 16 ) != 0 ) + return None; + + sal_uInt32 nWidth = readLE32( pData+4 ); + sal_uInt32 nHeight = readLE32( pData+8 ); + + if( m_aPixmap != None ) + { + XFreePixmap( m_pDisplay, m_aPixmap ); + m_aPixmap = None; + } + if( m_aBitmap != None ) + { + XFreePixmap( m_pDisplay, m_aBitmap ); + m_aBitmap = None; + } + + m_aPixmap = limitXCreatePixmap( m_pDisplay, + RootWindow( m_pDisplay, m_aInfo.screen ), + nWidth, nHeight, m_aInfo.depth ); + + if( m_aPixmap != None ) + { + XImage aImage; + aImage.width = static_cast(nWidth); + aImage.height = static_cast(nHeight); + aImage.xoffset = 0; + aImage.format = ZPixmap; + aImage.data = nullptr; + aImage.byte_order = ImageByteOrder( m_pDisplay ); + aImage.bitmap_unit = BitmapUnit( m_pDisplay ); + aImage.bitmap_bit_order = BitmapBitOrder( m_pDisplay ); + aImage.bitmap_pad = BitmapPad( m_pDisplay ); + aImage.depth = m_aInfo.depth; + aImage.red_mask = m_aInfo.red_mask; + aImage.green_mask = m_aInfo.green_mask; + aImage.blue_mask = m_aInfo.blue_mask; + aImage.bytes_per_line = 0; // filled in by XInitImage + if( m_aInfo.depth <= 8 ) + aImage.bits_per_pixel = m_aInfo.depth; + else + aImage.bits_per_pixel = 8*((m_aInfo.depth+7)/8); + aImage.obdata = nullptr; + + XInitImage( &aImage ); + aImage.data = static_cast(std::malloc( nHeight*aImage.bytes_per_line )); + + if( readLE32( pData+14 ) == 24 ) + { + if( m_aInfo.c_class == TrueColor ) + setBitmapDataTC( pData, &aImage ); + else + setBitmapDataTCDither( pData, &aImage ); + } + else + setBitmapDataPalette( pData, &aImage ); + + // put the image + XPutImage( m_pDisplay, + m_aPixmap, + DefaultGC( m_pDisplay, m_aInfo.screen ), + &aImage, + 0, 0, + 0, 0, + nWidth, nHeight ); + + // clean up + std::free( aImage.data ); + + // prepare bitmap (mask) + m_aBitmap = limitXCreatePixmap( m_pDisplay, + RootWindow( m_pDisplay, m_aInfo.screen ), + nWidth, nHeight, 1 ); + XGCValues aVal; + aVal.function = GXcopy; + aVal.foreground = 0xffffffff; + GC aGC = XCreateGC( m_pDisplay, m_aBitmap, GCFunction | GCForeground, &aVal ); + XFillRectangle( m_pDisplay, m_aBitmap, aGC, 0, 0, nWidth, nHeight ); + XFreeGC( m_pDisplay, aGC ); + } + + return m_aPixmap; +} + +css::uno::Sequence x11::convertBitmapDepth( + css::uno::Sequence const & data, int depth) +{ + if (depth < 4) { + depth = 1; + } else if (depth < 8) { + depth = 4; + } else if (depth > 8 && depth < 24) { + depth = 24; + } + SolarMutexGuard g; + SvMemoryStream in( + const_cast(data.getConstArray()), data.getLength(), + StreamMode::READ); + Bitmap bm; + ReadDIB(bm, in, true); + if (bm.getPixelFormat() == vcl::PixelFormat::N24_BPP && depth <= 8) { + bm.Dither(); + } + if (vcl::pixelFormatBitCount(bm.getPixelFormat()) != depth) { + switch (depth) { + case 1: + bm.Convert(BmpConversion::N1BitThreshold); + break; + case 4: + { + BitmapEx aBmpEx(bm); + BitmapFilter::Filter(aBmpEx, BitmapSimpleColorQuantizationFilter(1<<4)); + bm = aBmpEx.GetBitmap(); + } + break; + + case 8: + { + BitmapEx aBmpEx(bm); + BitmapFilter::Filter(aBmpEx, BitmapSimpleColorQuantizationFilter(1<<8)); + bm = aBmpEx.GetBitmap(); + } + break; + + case 24: + bm.Convert(BmpConversion::N24Bit); + break; + } + } + SvMemoryStream out; + WriteDIB(bm, out, false, true); + return css::uno::Sequence( + static_cast(out.GetData()), out.GetEndOfData()); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/bmp.hxx b/vcl/unx/generic/dtrans/bmp.hxx new file mode 100644 index 0000000000..3a37158db3 --- /dev/null +++ b/vcl/unx/generic/dtrans/bmp.hxx @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include +#include + +#include +#include + +namespace x11 { + +// helper methods +sal_uInt8* X11_getBmpFromPixmap( Display* pDisplay, + Drawable aDrawable, + Colormap aColormap, + sal_Int32& rOutSize ); + +class PixmapHolder +{ + Display* m_pDisplay; + Colormap m_aColormap; + Pixmap m_aPixmap; + Pixmap m_aBitmap; + XVisualInfo m_aInfo; + + int m_nRedShift; + int m_nGreenShift; + int m_nBlueShift; + tools::ULong m_nBlueShift2Mask, m_nRedShift2Mask, m_nGreenShift2Mask; + + // these expect data pointers to bitmapinfo header + void setBitmapDataTC( const sal_uInt8* pData, XImage* pImage ); + void setBitmapDataTCDither( const sal_uInt8* pData, XImage* pImage ); + void setBitmapDataPalette( const sal_uInt8* pData, XImage* pImage ); + + tools::ULong getTCPixel( sal_uInt8 r, sal_uInt8 g, sal_uInt8 b ) const; +public: + PixmapHolder( Display* pDisplay ); + ~PixmapHolder(); + + // accepts bitmap file (including bitmap file header) + Pixmap setBitmapData( const sal_uInt8* pData ); + bool needsConversion( const sal_uInt8* pData ) const; + + Colormap getColormap() const { return m_aColormap; } + Pixmap getPixmap() const { return m_aPixmap; } + Pixmap getBitmap() const { return m_aBitmap; } + VisualID getVisualID() const { return m_aInfo.visualid; } + int getDepth() const { return m_aInfo.depth; } +}; + +css::uno::Sequence convertBitmapDepth( + css::uno::Sequence const & data, int depth); + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/config.cxx b/vcl/unx/generic/dtrans/config.cxx new file mode 100644 index 0000000000..c7292628e3 --- /dev/null +++ b/vcl/unx/generic/dtrans/config.cxx @@ -0,0 +1,123 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include + +#include "X11_selection.hxx" + +constexpr OUStringLiteral SETTINGS_CONFIGNODE = u"VCL/Settings/Transfer"; +constexpr OUString SELECTION_PROPERTY = u"SelectionTimeout"_ustr; + +namespace x11 +{ + +namespace { + +class DtransX11ConfigItem : public ::utl::ConfigItem +{ + sal_Int32 m_nSelectionTimeout; + + virtual void Notify( const css::uno::Sequence< OUString >& rPropertyNames ) override; + virtual void ImplCommit() override; + +public: + DtransX11ConfigItem(); + + sal_Int32 getSelectionTimeout() const { return m_nSelectionTimeout; } +}; + +} + +} + +using namespace com::sun::star::lang; +using namespace com::sun::star::uno; +using namespace x11; + +sal_Int32 SelectionManager::getSelectionTimeout() +{ + if( m_nSelectionTimeout < 1 ) + { + DtransX11ConfigItem aCfg; + m_nSelectionTimeout = aCfg.getSelectionTimeout(); +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "initialized selection timeout to " + << m_nSelectionTimeout + << " seconds."); +#endif + } + return m_nSelectionTimeout; +} + +/* + * DtransX11ConfigItem constructor + */ + +DtransX11ConfigItem::DtransX11ConfigItem() : + ConfigItem( SETTINGS_CONFIGNODE, + ConfigItemMode::NONE ), + m_nSelectionTimeout( 3 ) +{ + Sequence aKeys { SELECTION_PROPERTY }; + const Sequence< Any > aValues = GetProperties( aKeys ); +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "found " + << aValues.getLength() + << " properties for " + << SELECTION_PROPERTY); +#endif + for( Any const & value : aValues ) + { + if( auto pLine = o3tl::tryAccess(value) ) + { + if( !pLine->isEmpty() ) + { + m_nSelectionTimeout = pLine->toInt32(); + if( m_nSelectionTimeout < 1 ) + m_nSelectionTimeout = 1; + } +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.dtrans", "found SelectionTimeout \"" << *pLine << "\"."); +#endif + } +#if OSL_DEBUG_LEVEL > 1 + else + SAL_INFO("vcl.unx.dtrans", "found SelectionTimeout of type \"" + << value.getValueType().getTypeName() << "\"."); +#endif + } +} + +void DtransX11ConfigItem::ImplCommit() +{ + // for the clipboard service this is readonly, so + // there is nothing to commit +} + +/* + * DtransX11ConfigItem::Notify + */ + +void DtransX11ConfigItem::Notify( const Sequence< OUString >& /*rPropertyNames*/ ) +{ +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/copydata_curs.h b/vcl/unx/generic/dtrans/copydata_curs.h new file mode 100644 index 0000000000..4cc36ebdeb --- /dev/null +++ b/vcl/unx/generic/dtrans/copydata_curs.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#define copydata_curs_width 32 +#define copydata_curs_height 32 +#define copydata_curs_x_hot 1 +#define copydata_curs_y_hot 1 +static unsigned char copydata_curs_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x0e, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, + 0x7e, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, + 0xfe, 0x03, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, + 0x66, 0x00, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x10, 0x53, 0x00, 0x00, + 0x28, 0xa3, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x10, 0xf0, 0x1f, 0x00, 0x08, 0xf0, 0x1f, 0x00, 0x10, 0xf0, 0x1e, 0x00, + 0xa8, 0xf2, 0x1e, 0x00, 0x50, 0x35, 0x18, 0x00, 0x00, 0xf0, 0x1e, 0x00, + 0x00, 0xf0, 0x1e, 0x00, 0x00, 0xf0, 0x1f, 0x00, 0x00, 0xf0, 0x1f, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/copydata_mask.h b/vcl/unx/generic/dtrans/copydata_mask.h new file mode 100644 index 0000000000..a3538c9522 --- /dev/null +++ b/vcl/unx/generic/dtrans/copydata_mask.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#define copydata_mask_width 32 +#define copydata_mask_height 32 +#define copydata_mask_x_hot 1 +#define copydata_mask_y_hot 1 +static unsigned char copydata_mask_bits[] = { + 0x07, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, + 0x3f, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, + 0xff, 0x01, 0x00, 0x00, 0xff, 0x03, 0x00, 0x00, 0xff, 0x07, 0x00, 0x00, + 0xff, 0x07, 0x00, 0x00, 0xff, 0x07, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, + 0xff, 0x01, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0xe7, 0x03, 0x00, 0x00, + 0xe0, 0x03, 0x00, 0x00, 0xf8, 0xff, 0x00, 0x00, 0xfc, 0xff, 0x01, 0x00, + 0xfc, 0xff, 0x01, 0x00, 0xfc, 0xff, 0x01, 0x00, 0x3c, 0xf8, 0x3f, 0x00, + 0x3c, 0xf8, 0x3f, 0x00, 0x3c, 0xf8, 0x3f, 0x00, 0xfc, 0xff, 0x3f, 0x00, + 0xfc, 0xff, 0x3f, 0x00, 0xfc, 0xff, 0x3f, 0x00, 0xf8, 0xff, 0x3f, 0x00, + 0x00, 0xf8, 0x3f, 0x00, 0x00, 0xf8, 0x3f, 0x00, 0x00, 0xf8, 0x3f, 0x00, + 0x00, 0xf8, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/linkdata_curs.h b/vcl/unx/generic/dtrans/linkdata_curs.h new file mode 100644 index 0000000000..8a4e6db387 --- /dev/null +++ b/vcl/unx/generic/dtrans/linkdata_curs.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#define linkdata_curs_width 32 +#define linkdata_curs_height 32 +#define linkdata_curs_x_hot 1 +#define linkdata_curs_y_hot 1 +static unsigned char linkdata_curs_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x0e, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, + 0x7e, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, + 0xfe, 0x03, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, + 0x66, 0x00, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x10, 0x53, 0x00, 0x00, + 0x28, 0xa3, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x10, 0xf0, 0x1f, 0x00, 0x08, 0x70, 0x18, 0x00, 0x10, 0xf0, 0x18, 0x00, + 0xa8, 0x72, 0x18, 0x00, 0x50, 0x35, 0x1a, 0x00, 0x00, 0x30, 0x1f, 0x00, + 0x00, 0xb0, 0x1f, 0x00, 0x00, 0x70, 0x1f, 0x00, 0x00, 0xf0, 0x1f, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/linkdata_mask.h b/vcl/unx/generic/dtrans/linkdata_mask.h new file mode 100644 index 0000000000..a1875a8e0a --- /dev/null +++ b/vcl/unx/generic/dtrans/linkdata_mask.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#define linkdata_mask_width 32 +#define linkdata_mask_height 32 +#define linkdata_mask_x_hot 1 +#define linkdata_mask_y_hot 1 +static unsigned char linkdata_mask_bits[] = { + 0x07, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, + 0x3f, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, + 0xff, 0x01, 0x00, 0x00, 0xff, 0x03, 0x00, 0x00, 0xff, 0x07, 0x00, 0x00, + 0xff, 0x07, 0x00, 0x00, 0xff, 0x07, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, + 0xff, 0x01, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0xe7, 0x03, 0x00, 0x00, + 0xe0, 0x03, 0x00, 0x00, 0xf8, 0xff, 0x00, 0x00, 0xfc, 0xff, 0x01, 0x00, + 0xfc, 0xff, 0x01, 0x00, 0xfc, 0xff, 0x01, 0x00, 0x3c, 0xf8, 0x3f, 0x00, + 0x3c, 0xf8, 0x3f, 0x00, 0x3c, 0xf8, 0x3f, 0x00, 0xfc, 0xff, 0x3f, 0x00, + 0xfc, 0xff, 0x3f, 0x00, 0xfc, 0xff, 0x3f, 0x00, 0xf8, 0xff, 0x3f, 0x00, + 0x00, 0xf8, 0x3f, 0x00, 0x00, 0xf8, 0x3f, 0x00, 0x00, 0xf8, 0x3f, 0x00, + 0x00, 0xf8, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/movedata_curs.h b/vcl/unx/generic/dtrans/movedata_curs.h new file mode 100644 index 0000000000..b253ce70ca --- /dev/null +++ b/vcl/unx/generic/dtrans/movedata_curs.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#define movedata_curs_width 32 +#define movedata_curs_height 32 +#define movedata_curs_x_hot 1 +#define movedata_curs_y_hot 1 +static unsigned char movedata_curs_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, + 0x0e, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, + 0x7e, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0xfe, 0x01, 0x00, 0x00, + 0xfe, 0x03, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x6e, 0x00, 0x00, 0x00, + 0x66, 0x00, 0x00, 0x00, 0xc2, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, + 0x80, 0x01, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x10, 0x53, 0x00, 0x00, + 0x28, 0xa3, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00, 0x08, 0x80, 0x00, 0x00, + 0x10, 0x40, 0x00, 0x00, 0x08, 0x80, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00, + 0xa8, 0xaa, 0x00, 0x00, 0x50, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/movedata_mask.h b/vcl/unx/generic/dtrans/movedata_mask.h new file mode 100644 index 0000000000..d317b1556e --- /dev/null +++ b/vcl/unx/generic/dtrans/movedata_mask.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#define movedata_mask_width 32 +#define movedata_mask_height 32 +#define movedata_mask_x_hot 1 +#define movedata_mask_y_hot 1 +static unsigned char movedata_mask_bits[] = { + 0x07, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, + 0x3f, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, + 0xff, 0x01, 0x00, 0x00, 0xff, 0x03, 0x00, 0x00, 0xff, 0x07, 0x00, 0x00, + 0xff, 0x07, 0x00, 0x00, 0xff, 0x07, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, + 0xff, 0x01, 0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0xe7, 0x03, 0x00, 0x00, + 0xe0, 0x03, 0x00, 0x00, 0xf8, 0xff, 0x00, 0x00, 0xfc, 0xff, 0x01, 0x00, + 0xfc, 0xff, 0x01, 0x00, 0xfc, 0xff, 0x01, 0x00, 0x3c, 0xe0, 0x01, 0x00, + 0x3c, 0xe0, 0x01, 0x00, 0x3c, 0xe0, 0x01, 0x00, 0xfc, 0xff, 0x01, 0x00, + 0xfc, 0xff, 0x01, 0x00, 0xfc, 0xff, 0x01, 0x00, 0xf8, 0xff, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/nodrop_curs.h b/vcl/unx/generic/dtrans/nodrop_curs.h new file mode 100644 index 0000000000..9582575180 --- /dev/null +++ b/vcl/unx/generic/dtrans/nodrop_curs.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#define nodrop_curs_width 32 +#define nodrop_curs_height 32 +#define nodrop_curs_x_hot 9 +#define nodrop_curs_y_hot 9 +static unsigned char nodrop_curs_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0xf0, 0x3f, 0x00, 0x00, + 0xf8, 0x7f, 0x00, 0x00, 0x7c, 0xf8, 0x00, 0x00, 0x1c, 0xfc, 0x00, 0x00, + 0x1e, 0xfe, 0x01, 0x00, 0x0e, 0xdf, 0x01, 0x00, 0x8e, 0xcf, 0x01, 0x00, + 0xce, 0xc7, 0x01, 0x00, 0xee, 0xc3, 0x01, 0x00, 0xfe, 0xe1, 0x01, 0x00, + 0xfc, 0xe0, 0x00, 0x00, 0x7c, 0xf8, 0x00, 0x00, 0xf8, 0x7f, 0x00, 0x00, + 0xf0, 0x3f, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/dtrans/nodrop_mask.h b/vcl/unx/generic/dtrans/nodrop_mask.h new file mode 100644 index 0000000000..662a300645 --- /dev/null +++ b/vcl/unx/generic/dtrans/nodrop_mask.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#define nodrop_mask_width 32 +#define nodrop_mask_height 32 +#define nodrop_mask_x_hot 9 +#define nodrop_mask_y_hot 9 +static unsigned char nodrop_mask_bits[] = { + 0xc0, 0x0f, 0x00, 0x00, 0xf0, 0x3f, 0x00, 0x00, 0xf8, 0x7f, 0x00, 0x00, + 0xfc, 0xff, 0x00, 0x00, 0xfe, 0xff, 0x01, 0x00, 0x7e, 0xfe, 0x01, 0x00, + 0x3f, 0xff, 0x03, 0x00, 0x9f, 0xff, 0x03, 0x00, 0xdf, 0xff, 0x03, 0x00, + 0xff, 0xef, 0x03, 0x00, 0xff, 0xe7, 0x03, 0x00, 0xff, 0xf3, 0x03, 0x00, + 0xfe, 0xf9, 0x01, 0x00, 0xfe, 0xff, 0x01, 0x00, 0xfc, 0xff, 0x00, 0x00, + 0xf8, 0x7f, 0x00, 0x00, 0xf0, 0x3f, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/fontmanager/fontconfig.cxx b/vcl/unx/generic/fontmanager/fontconfig.cxx new file mode 100644 index 0000000000..4eb186a269 --- /dev/null +++ b/vcl/unx/generic/fontmanager/fontconfig.cxx @@ -0,0 +1,1310 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include + +#include + +#include +#include +#include + +using namespace psp; +using namespace osl; + +namespace +{ + +struct FontOptionsKey +{ + OUString m_sFamilyName; + int m_nFontSize; + FontItalic m_eItalic; + FontWeight m_eWeight; + FontWidth m_eWidth; + FontPitch m_ePitch; + + bool operator==(const FontOptionsKey& rOther) const + { + return m_sFamilyName == rOther.m_sFamilyName && + m_nFontSize == rOther.m_nFontSize && + m_eItalic == rOther.m_eItalic && + m_eWeight == rOther.m_eWeight && + m_eWidth == rOther.m_eWidth && + m_ePitch == rOther.m_ePitch; + } +}; + +} + +namespace std +{ + +template <> struct hash +{ + std::size_t operator()(const FontOptionsKey& k) const noexcept + { + std::size_t seed = k.m_sFamilyName.hashCode(); + o3tl::hash_combine(seed, k.m_nFontSize); + o3tl::hash_combine(seed, k.m_eItalic); + o3tl::hash_combine(seed, k.m_eWeight); + o3tl::hash_combine(seed, k.m_eWidth); + o3tl::hash_combine(seed, k.m_ePitch); + return seed; + } +}; + +} // end std namespace + +namespace +{ + +struct FcPatternDeleter +{ + void operator()(FcPattern* pPattern) const + { + FcPatternDestroy(pPattern); + } +}; + +typedef std::unique_ptr FcPatternUniquePtr; + +class CachedFontConfigFontOptions +{ +private: + o3tl::lru_map lru_options_cache; + +public: + CachedFontConfigFontOptions() + : lru_options_cache(10) // arbitrary cache size of 10 + { + } + + std::unique_ptr lookup(const FontOptionsKey& rKey) + { + auto it = lru_options_cache.find(rKey); + if (it != lru_options_cache.end()) + return std::make_unique(FcPatternDuplicate(it->second.get())); + return nullptr; + } + + void cache(const FontOptionsKey& rKey, const FcPattern* pPattern) + { + lru_options_cache.insert(std::make_pair(rKey, FcPatternUniquePtr(FcPatternDuplicate(pPattern)))); + } + +}; + +typedef std::pair lang_and_element; + +class FontCfgWrapper +{ + FcFontSet* m_pFontSet; + + FontCfgWrapper(); + ~FontCfgWrapper(); + +public: + static FontCfgWrapper& get(); + static void release(); + + void addFontSet( FcSetName ); + + FcFontSet* getFontSet(); + void replaceFontSet(FcFontSet* pFilteredFontSet); + + void clear(); + +public: + FcResult LocalizedElementFromPattern(FcPattern const * pPattern, FcChar8 **family, + const char *elementtype, const char *elementlangtype); +//to-do, make private and add some cleaner accessor methods + std::unordered_map< OString, OString > m_aFontNameToLocalized; + std::unordered_map< OString, OString > m_aLocalizedToCanonical; + CachedFontConfigFontOptions m_aCachedFontOptions; +private: + void cacheLocalizedFontNames(const FcChar8 *origfontname, const FcChar8 *bestfontname, const std::vector< lang_and_element > &lang_and_elements); + + std::unique_ptr m_pLanguageTag; +}; + +} + +FontCfgWrapper::FontCfgWrapper() + : m_pFontSet( nullptr ) +{ + FcInit(); +} + +#ifndef FC_FONT_WRAPPER +#define FC_FONT_WRAPPER "fontwrapper" +#endif + +void FontCfgWrapper::addFontSet( FcSetName eSetName ) +{ + // Add only acceptable fonts to our config, for future fontconfig use. + FcFontSet* pOrig = FcConfigGetFonts( FcConfigGetCurrent(), eSetName ); + if( !pOrig ) + return; + + // filter the font sets to remove obsolete faces + for( int i = 0; i < pOrig->nfont; ++i ) + { + FcPattern* pPattern = pOrig->fonts[i]; + // #i115131# ignore non-scalable fonts + // Scalable fonts are usually outline fonts, but some bitmaps fonts + // (like Noto Color Emoji) are also scalable. + FcBool bScalable = FcFalse; + FcResult eScalableRes = FcPatternGetBool(pPattern, FC_SCALABLE, 0, &bScalable); + if ((eScalableRes != FcResultMatch) || (bScalable == FcFalse)) + continue; + + // Ignore Type 1 fonts, too. + FcChar8* pFormat = nullptr; + FcResult eFormatRes = FcPatternGetString(pPattern, FC_FONTFORMAT, 0, &pFormat); + if ((eFormatRes == FcResultMatch) && (strcmp(reinterpret_cast(pFormat), "Type 1") == 0)) + continue; + + // Ignore any other non-SFNT wrapper format, including WOFF and WOFF2, too. + FcChar8* pWrapper = nullptr; + FcResult eWrapperRes = FcPatternGetString(pPattern, FC_FONT_WRAPPER, 0, &pWrapper); + if ((eWrapperRes == FcResultMatch) && (strcmp(reinterpret_cast(pWrapper), "SFNT") != 0)) + continue; + + FcPatternReference( pPattern ); + FcFontSetAdd( m_pFontSet, pPattern ); + } + + // TODO?: FcFontSetDestroy( pOrig ); +} + +namespace +{ + int compareFontNames(const FcPattern *a, const FcPattern *b) + { + FcChar8 *pNameA=nullptr, *pNameB=nullptr; + + bool bHaveA = FcPatternGetString(a, FC_FAMILY, 0, &pNameA) == FcResultMatch; + bool bHaveB = FcPatternGetString(b, FC_FAMILY, 0, &pNameB) == FcResultMatch; + + if (bHaveA && bHaveB) + return strcmp(reinterpret_cast(pNameA), reinterpret_cast(pNameB)); + + return int(bHaveA) - int(bHaveB); + } + + //Sort fonts so that fonts with the same family name are side-by-side, with + //those with higher version numbers first + class SortFont + { + public: + bool operator()(const FcPattern *a, const FcPattern *b) + { + int comp = compareFontNames(a, b); + if (comp != 0) + return comp < 0; + + int nVersionA=0, nVersionB=0; + + bool bHaveA = FcPatternGetInteger(a, FC_FONTVERSION, 0, &nVersionA) == FcResultMatch; + bool bHaveB = FcPatternGetInteger(b, FC_FONTVERSION, 0, &nVersionB) == FcResultMatch; + + if (bHaveA && bHaveB) + return nVersionA > nVersionB; + + return bHaveA > bHaveB; + } + }; + + //See fdo#30729 for where an old opensymbol installed system-wide can + //clobber the new opensymbol installed locally + + //See if this font is a duplicate with equal attributes which has already been + //inserted, or if it an older version of an inserted fonts. Depends on FcFontSet + //on being sorted with SortFont + bool isPreviouslyDuplicateOrObsoleted(FcFontSet const *pFSet, int i) + { + const FcPattern *a = pFSet->fonts[i]; + + FcPattern* pTestPatternA = FcPatternDuplicate(a); + FcPatternDel(pTestPatternA, FC_FILE); + FcPatternDel(pTestPatternA, FC_CHARSET); + FcPatternDel(pTestPatternA, FC_CAPABILITY); + FcPatternDel(pTestPatternA, FC_FONTVERSION); + FcPatternDel(pTestPatternA, FC_LANG); + + bool bIsDup(false); + + // fdo#66715: loop for case of several font files for same font + for (int j = i - 1; 0 <= j && !bIsDup; --j) + { + const FcPattern *b = pFSet->fonts[j]; + + if (compareFontNames(a, b) != 0) + break; + + FcPattern* pTestPatternB = FcPatternDuplicate(b); + FcPatternDel(pTestPatternB, FC_FILE); + FcPatternDel(pTestPatternB, FC_CHARSET); + FcPatternDel(pTestPatternB, FC_CAPABILITY); + FcPatternDel(pTestPatternB, FC_FONTVERSION); + FcPatternDel(pTestPatternB, FC_LANG); + + bIsDup = FcPatternEqual(pTestPatternA, pTestPatternB); + + FcPatternDestroy(pTestPatternB); + } + + FcPatternDestroy(pTestPatternA); + + return bIsDup; + } +} + +FcFontSet* FontCfgWrapper::getFontSet() +{ + if( !m_pFontSet ) + { + m_pFontSet = FcFontSetCreate(); + bool bRestrictFontSetToApplicationFonts = false; +#if HAVE_MORE_FONTS + bRestrictFontSetToApplicationFonts = [] { + return getenv("SAL_NON_APPLICATION_FONT_USE") != nullptr; + }(); +#endif + // Add the application fonts before the system fonts. + // tdf#157939 We will remove duplicate fonts, where the duplicate is + // the one with a smaller version number. If the same version font is + // available system-wide or bundled with our application, then we + // prefer via stable-sort the first one we see. Load application fonts + // first to prefer the one we bundle in the application in that case. + addFontSet( FcSetApplication ); + if (!bRestrictFontSetToApplicationFonts) + addFontSet( FcSetSystem ); + + std::stable_sort(m_pFontSet->fonts,m_pFontSet->fonts+m_pFontSet->nfont,SortFont()); + } + + return m_pFontSet; +} + +void FontCfgWrapper::replaceFontSet(FcFontSet* pFilteredFontSet) +{ + if (m_pFontSet) + FcFontSetDestroy(m_pFontSet); + m_pFontSet = pFilteredFontSet; +} + +FontCfgWrapper::~FontCfgWrapper() +{ + clear(); + //To-Do: get gtk vclplug smoketest to pass + //FcFini(); +} + +static FontCfgWrapper* pOneInstance = nullptr; + +FontCfgWrapper& FontCfgWrapper::get() +{ + if( ! pOneInstance ) + pOneInstance = new FontCfgWrapper(); + return *pOneInstance; +} + +void FontCfgWrapper::release() +{ + if( pOneInstance ) + { + delete pOneInstance; + pOneInstance = nullptr; + } +} + +namespace +{ + FcChar8* bestname(const std::vector &elements, const LanguageTag & rLangTag); + + FcChar8* bestname(const std::vector &elements, const LanguageTag & rLangTag) + { + FcChar8* candidate = elements.begin()->second; + /* FIXME-BCP47: once fontconfig supports language tags this + * language-territory stuff needs to be changed! */ + SAL_INFO_IF( !rLangTag.isIsoLocale(), "vcl.fonts", "localizedsorter::bestname - not an ISO locale"); + OString sLangMatch(OUStringToOString(rLangTag.getLanguage().toAsciiLowerCase(), RTL_TEXTENCODING_UTF8)); + OString sFullMatch = sLangMatch + + "-" + + OUStringToOString(rLangTag.getCountry().toAsciiLowerCase(), RTL_TEXTENCODING_UTF8); + + bool alreadyclosematch = false; + bool found_fallback_englishname = false; + for (auto const& element : elements) + { + const char *pLang = reinterpret_cast(element.first); + if( sFullMatch == pLang) + { + // both language and country match + candidate = element.second; + break; + } + else if( alreadyclosematch ) + { + // current candidate matches lang of lang-TERRITORY + // override candidate only if there is a full match + continue; + } + else if( sLangMatch == pLang) + { + // just the language matches + candidate = element.second; + alreadyclosematch = true; + } + else if( found_fallback_englishname ) + { + // already found an english fallback, don't override candidate + // unless there is a better language match + continue; + } + else if( rtl_str_compare( pLang, "en") == 0) + { + // select a fallback candidate of the first english element + // name + candidate = element.second; + found_fallback_englishname = true; + } + } + return candidate; + } +} + +//Set up maps to quickly map between a fonts best UI name and all the rest of its names, and vice versa +void FontCfgWrapper::cacheLocalizedFontNames(const FcChar8 *origfontname, const FcChar8 *bestfontname, + const std::vector< lang_and_element > &lang_and_elements) +{ + for (auto const& element : lang_and_elements) + { + const char *candidate = reinterpret_cast(element.second); + if (rtl_str_compare(candidate, reinterpret_cast(bestfontname)) != 0) + m_aFontNameToLocalized[OString(candidate)] = OString(reinterpret_cast(bestfontname)); + } + if (rtl_str_compare(reinterpret_cast(origfontname), reinterpret_cast(bestfontname)) != 0) + m_aLocalizedToCanonical[OString(reinterpret_cast(bestfontname))] = OString(reinterpret_cast(origfontname)); +} + +FcResult FontCfgWrapper::LocalizedElementFromPattern(FcPattern const * pPattern, FcChar8 **element, + const char *elementtype, const char *elementlangtype) +{ /* e. g.: ^ FC_FAMILY ^ FC_FAMILYLANG */ + FcChar8 *origelement; + FcResult eElementRes = FcPatternGetString( pPattern, elementtype, 0, &origelement ); + *element = origelement; + + if( eElementRes == FcResultMatch) + { + FcChar8* elementlang = nullptr; + if (FcPatternGetString( pPattern, elementlangtype, 0, &elementlang ) == FcResultMatch) + { + std::vector< lang_and_element > lang_and_elements; + lang_and_elements.emplace_back(elementlang, *element); + int k = 1; + while (true) + { + if (FcPatternGetString( pPattern, elementlangtype, k, &elementlang ) != FcResultMatch) + break; + if (FcPatternGetString( pPattern, elementtype, k, element ) != FcResultMatch) + break; + lang_and_elements.emplace_back(elementlang, *element); + ++k; + } + + if (!m_pLanguageTag) + m_pLanguageTag.reset(new LanguageTag(SvtSysLocaleOptions().GetRealUILanguageTag())); + + // FontConfig orders Typographic Family/Subfamily before old + // R/B/I/BI-compatible ones, but we want the later, so reverse the + // names to match them first. + std::reverse(lang_and_elements.begin(), lang_and_elements.end()); + + *element = bestname(lang_and_elements, *m_pLanguageTag); + + //if this element is a fontname, map the other names to this best-name + if (rtl_str_compare(elementtype, FC_FAMILY) == 0) + cacheLocalizedFontNames(origelement, *element, lang_and_elements); + } + } + + return eElementRes; +} + +void FontCfgWrapper::clear() +{ + m_aFontNameToLocalized.clear(); + m_aLocalizedToCanonical.clear(); + if( m_pFontSet ) + { + FcFontSetDestroy( m_pFontSet ); + m_pFontSet = nullptr; + } + m_pLanguageTag.reset(); +} + +/* + * PrintFontManager::initFontconfig + */ +void PrintFontManager::initFontconfig() +{ + FontCfgWrapper& rWrapper = FontCfgWrapper::get(); + rWrapper.clear(); +} + +namespace +{ + FontWeight convertWeight(int weight) + { + // set weight + if( weight <= FC_WEIGHT_THIN ) + return WEIGHT_THIN; + else if( weight <= FC_WEIGHT_ULTRALIGHT ) + return WEIGHT_ULTRALIGHT; + else if( weight <= FC_WEIGHT_LIGHT ) + return WEIGHT_LIGHT; + else if( weight <= FC_WEIGHT_BOOK ) + return WEIGHT_SEMILIGHT; + else if( weight <= FC_WEIGHT_NORMAL ) + return WEIGHT_NORMAL; + else if( weight <= FC_WEIGHT_MEDIUM ) + return WEIGHT_MEDIUM; + else if( weight <= FC_WEIGHT_SEMIBOLD ) + return WEIGHT_SEMIBOLD; + else if( weight <= FC_WEIGHT_BOLD ) + return WEIGHT_BOLD; + else if( weight <= FC_WEIGHT_ULTRABOLD ) + return WEIGHT_ULTRABOLD; + return WEIGHT_BLACK; + } + + FontItalic convertSlant(int slant) + { + // set italic + if( slant == FC_SLANT_ITALIC ) + return ITALIC_NORMAL; + else if( slant == FC_SLANT_OBLIQUE ) + return ITALIC_OBLIQUE; + return ITALIC_NONE; + } + + FontPitch convertSpacing(int spacing) + { + // set pitch + if( spacing == FC_MONO || spacing == FC_CHARCELL ) + return PITCH_FIXED; + return PITCH_VARIABLE; + } + + // translation: fontconfig enum -> vcl enum + FontWidth convertWidth(int width) + { + if (width == FC_WIDTH_ULTRACONDENSED) + return WIDTH_ULTRA_CONDENSED; + else if (width == FC_WIDTH_EXTRACONDENSED) + return WIDTH_EXTRA_CONDENSED; + else if (width == FC_WIDTH_CONDENSED) + return WIDTH_CONDENSED; + else if (width == FC_WIDTH_SEMICONDENSED) + return WIDTH_SEMI_CONDENSED; + else if (width == FC_WIDTH_SEMIEXPANDED) + return WIDTH_SEMI_EXPANDED; + else if (width == FC_WIDTH_EXPANDED) + return WIDTH_EXPANDED; + else if (width == FC_WIDTH_EXTRAEXPANDED) + return WIDTH_EXTRA_EXPANDED; + else if (width == FC_WIDTH_ULTRAEXPANDED) + return WIDTH_ULTRA_EXPANDED; + return WIDTH_NORMAL; + } +} + +namespace +{ + // for variable fonts, FC_INDEX has been changed such that the lower half is now the + // index of the font within the collection, and the upper half has been repurposed + // as the index within the variations + unsigned int GetCollectionIndex(unsigned int nEntryId) + { + return nEntryId & 0xFFFF; + } + + unsigned int GetVariationIndex(unsigned int nEntryId) + { + return nEntryId >> 16; + } +} + +void PrintFontManager::countFontconfigFonts() +{ + int nFonts = 0; + FontCfgWrapper& rWrapper = FontCfgWrapper::get(); + + FcFontSet* pFSet = rWrapper.getFontSet(); + const bool bMinimalFontset = utl::ConfigManager::IsFuzzing(); + if( pFSet ) + { + SAL_INFO("vcl.fonts", "found " << pFSet->nfont << " entries in fontconfig fontset"); + + FcFontSet* pFilteredSet = FcFontSetCreate(); + + for( int i = 0; i < pFSet->nfont; i++ ) + { + FcChar8* file = nullptr; + FcChar8* family = nullptr; + FcChar8* style = nullptr; + FcChar8* format = nullptr; + int slant = 0; + int weight = 0; + int width = 0; + int spacing = 0; + int symbol = 0; + int nEntryId = -1; + FcBool scalable = false; + + FcResult eFileRes = FcPatternGetString(pFSet->fonts[i], FC_FILE, 0, &file); + FcResult eFamilyRes = rWrapper.LocalizedElementFromPattern( pFSet->fonts[i], &family, FC_FAMILY, FC_FAMILYLANG ); + if (bMinimalFontset && strncmp(reinterpret_cast(family), "Liberation", strlen("Liberation"))) + continue; + FcResult eStyleRes = rWrapper.LocalizedElementFromPattern( pFSet->fonts[i], &style, FC_STYLE, FC_STYLELANG ); + FcResult eSlantRes = FcPatternGetInteger(pFSet->fonts[i], FC_SLANT, 0, &slant); + FcResult eWeightRes = FcPatternGetInteger(pFSet->fonts[i], FC_WEIGHT, 0, &weight); + FcResult eWidthRes = FcPatternGetInteger(pFSet->fonts[i], FC_WIDTH, 0, &width); + FcResult eSpacRes = FcPatternGetInteger(pFSet->fonts[i], FC_SPACING, 0, &spacing); + FcResult eScalableRes = FcPatternGetBool(pFSet->fonts[i], FC_SCALABLE, 0, &scalable); + FcResult eSymbolRes = FcPatternGetBool(pFSet->fonts[i], FC_SYMBOL, 0, &symbol); + FcResult eIndexRes = FcPatternGetInteger(pFSet->fonts[i], FC_INDEX, 0, &nEntryId); + FcResult eFormatRes = FcPatternGetString(pFSet->fonts[i], FC_FONTFORMAT, 0, &format); + + if( eFileRes != FcResultMatch || eFamilyRes != FcResultMatch || eScalableRes != FcResultMatch || eStyleRes != FcResultMatch ) + continue; + + SAL_INFO( + "vcl.fonts.detail", + "found font \"" << family << "\" in file " << file << ", weight = " + << (eWeightRes == FcResultMatch ? weight : -1) << ", slant = " + << (eSpacRes == FcResultMatch ? slant : -1) << ", style = \"" + << (eStyleRes == FcResultMatch ? reinterpret_cast(style) : "") + << "\", width = " << (eWeightRes == FcResultMatch ? width : -1) << ", spacing = " + << (eSpacRes == FcResultMatch ? spacing : -1) << ", scalable = " + << (eScalableRes == FcResultMatch ? scalable : -1) << ", format " + << (eFormatRes == FcResultMatch + ? reinterpret_cast(format) : "") + << " symbol = " << (eSymbolRes == FcResultMatch ? symbol : -1)); + +// OSL_ASSERT(eScalableRes != FcResultMatch || scalable); + + // We support only scalable fonts + if( eScalableRes == FcResultMatch && ! scalable ) + continue; + + if (isPreviouslyDuplicateOrObsoleted(pFSet, i)) + { + SAL_INFO("vcl.fonts.detail", "Ditching " << file << " as duplicate/obsolete"); + continue; + } + + OString aDir, aBase, aOrgPath( reinterpret_cast(file) ); + splitPath( aOrgPath, aDir, aBase ); + int nDirID = getDirectoryAtom( aDir ); + + PrintFont aFont; + aFont.m_nDirectory = nDirID; + aFont.m_aFontFile = aBase; + if (eIndexRes == FcResultMatch) + { + aFont.m_nCollectionEntry = GetCollectionIndex(nEntryId); + aFont.m_nVariationEntry = GetVariationIndex(nEntryId); + } + + auto& rFA = aFont.m_aFontAttributes; + rFA.SetWeight(WEIGHT_NORMAL); + rFA.SetWidthType(WIDTH_NORMAL); + rFA.SetPitch(PITCH_VARIABLE); + rFA.SetQuality(512); + + rFA.SetFamilyName(OStringToOUString(std::string_view(reinterpret_cast(family)), RTL_TEXTENCODING_UTF8)); + if (eStyleRes == FcResultMatch) + rFA.SetStyleName(OStringToOUString(std::string_view(reinterpret_cast(style)), RTL_TEXTENCODING_UTF8)); + if (eWeightRes == FcResultMatch) + rFA.SetWeight(convertWeight(weight)); + if (eWidthRes == FcResultMatch) + rFA.SetWidthType(convertWidth(width)); + if (eSpacRes == FcResultMatch) + rFA.SetPitch(convertSpacing(spacing)); + if (eSlantRes == FcResultMatch) + rFA.SetItalic(convertSlant(slant)); + if (eSymbolRes == FcResultMatch) + rFA.SetMicrosoftSymbolEncoded(bool(symbol)); + + // sort into known fonts + fontID nFontID = m_nNextFontID++; + m_aFonts.emplace(nFontID, aFont); + m_aFontFileToFontID[aBase].insert(nFontID); + nFonts++; + + FcPattern* pPattern = pFSet->fonts[i]; + FcPatternReference(pPattern); + FcFontSetAdd(pFilteredSet, pPattern); + + SAL_INFO("vcl.fonts.detail", "inserted font " << family << " as fontID " << nFontID); + } + + // tdf#157939 if we drop fonts, drop them from the FcConfig set too so they are not + // candidates for suggestions by fontconfig + if (pFSet->nfont != pFilteredSet->nfont) + rWrapper.replaceFontSet(pFilteredSet); + else + FcFontSetDestroy(pFilteredSet); + + } + + // how does one get rid of the config ? + SAL_INFO("vcl.fonts", "inserted " << nFonts << " fonts from fontconfig"); +} + +void PrintFontManager::deinitFontconfig() +{ + FontCfgWrapper::release(); +} + +void PrintFontManager::addFontconfigDir( const OString& rDirName ) +{ + const char* pDirName = rDirName.getStr(); + bool bDirOk = (FcConfigAppFontAddDir(FcConfigGetCurrent(), reinterpret_cast(pDirName) ) == FcTrue); + + SAL_INFO("vcl.fonts", "FcConfigAppFontAddDir( \"" << pDirName << "\") => " << bDirOk); + + if( !bDirOk ) + return; + + // load dir-specific fc-config file too if available + const OString aConfFileName = rDirName + "/fc_local.conf"; + FILE* pCfgFile = fopen( aConfFileName.getStr(), "rb" ); + if( pCfgFile ) + { + fclose( pCfgFile); + bool bCfgOk = FcConfigParseAndLoad(FcConfigGetCurrent(), + reinterpret_cast(aConfFileName.getStr()), FcTrue); + + SAL_INFO_IF(!bCfgOk, + "vcl.fonts", "FcConfigParseAndLoad( \"" + << aConfFileName << "\") => " << bCfgOk); + } else { + SAL_INFO("vcl.fonts", "cannot open " << aConfFileName); + } +} + +void PrintFontManager::addFontconfigFile( const OString& rFileName ) +{ + const char* pFileName = rFileName.getStr(); + bool bFileOk = (FcConfigAppFontAddFile(FcConfigGetCurrent(), reinterpret_cast(pFileName) ) == FcTrue); + + SAL_INFO("vcl.fonts", "FcConfigAppFontAddFile(\"" << pFileName << "\") => " << std::boolalpha << bFileOk); + + if( !bFileOk ) + return; + + // FIXME: we want to add only the newly added font not re-add the whole + // application font set. + FontCfgWrapper& rWrapper = FontCfgWrapper::get(); + rWrapper.addFontSet( FcSetApplication ); +} + +static void addtopattern(FcPattern *pPattern, + FontItalic eItalic, FontWeight eWeight, FontWidth eWidth, FontPitch ePitch) +{ + if( eItalic != ITALIC_DONTKNOW ) + { + int nSlant = FC_SLANT_ROMAN; + switch( eItalic ) + { + case ITALIC_NORMAL: + nSlant = FC_SLANT_ITALIC; + break; + case ITALIC_OBLIQUE: + nSlant = FC_SLANT_OBLIQUE; + break; + default: + break; + } + FcPatternAddInteger(pPattern, FC_SLANT, nSlant); + } + if( eWeight != WEIGHT_DONTKNOW ) + { + int nWeight = FC_WEIGHT_NORMAL; + switch( eWeight ) + { + case WEIGHT_THIN: nWeight = FC_WEIGHT_THIN;break; + case WEIGHT_ULTRALIGHT: nWeight = FC_WEIGHT_ULTRALIGHT;break; + case WEIGHT_LIGHT: nWeight = FC_WEIGHT_LIGHT;break; + case WEIGHT_SEMILIGHT: nWeight = FC_WEIGHT_BOOK;break; + case WEIGHT_NORMAL: nWeight = FC_WEIGHT_NORMAL;break; + case WEIGHT_MEDIUM: nWeight = FC_WEIGHT_MEDIUM;break; + case WEIGHT_SEMIBOLD: nWeight = FC_WEIGHT_SEMIBOLD;break; + case WEIGHT_BOLD: nWeight = FC_WEIGHT_BOLD;break; + case WEIGHT_ULTRABOLD: nWeight = FC_WEIGHT_ULTRABOLD;break; + case WEIGHT_BLACK: nWeight = FC_WEIGHT_BLACK;break; + default: + break; + } + FcPatternAddInteger(pPattern, FC_WEIGHT, nWeight); + } + if( eWidth != WIDTH_DONTKNOW ) + { + int nWidth = FC_WIDTH_NORMAL; + switch( eWidth ) + { + case WIDTH_ULTRA_CONDENSED: nWidth = FC_WIDTH_ULTRACONDENSED;break; + case WIDTH_EXTRA_CONDENSED: nWidth = FC_WIDTH_EXTRACONDENSED;break; + case WIDTH_CONDENSED: nWidth = FC_WIDTH_CONDENSED;break; + case WIDTH_SEMI_CONDENSED: nWidth = FC_WIDTH_SEMICONDENSED;break; + case WIDTH_NORMAL: nWidth = FC_WIDTH_NORMAL;break; + case WIDTH_SEMI_EXPANDED: nWidth = FC_WIDTH_SEMIEXPANDED;break; + case WIDTH_EXPANDED: nWidth = FC_WIDTH_EXPANDED;break; + case WIDTH_EXTRA_EXPANDED: nWidth = FC_WIDTH_EXTRAEXPANDED;break; + case WIDTH_ULTRA_EXPANDED: nWidth = FC_WIDTH_ULTRAEXPANDED;break; + default: + break; + } + FcPatternAddInteger(pPattern, FC_WIDTH, nWidth); + } + if( ePitch == PITCH_DONTKNOW ) + return; + + int nSpacing = FC_PROPORTIONAL; + switch( ePitch ) + { + case PITCH_FIXED: nSpacing = FC_MONO;break; + case PITCH_VARIABLE: nSpacing = FC_PROPORTIONAL;break; + default: + break; + } + FcPatternAddInteger(pPattern, FC_SPACING, nSpacing); + if (nSpacing == FC_MONO) + FcPatternAddString(pPattern, FC_FAMILY, reinterpret_cast("monospace")); +} + +namespace +{ + //Someday fontconfig will hopefully use bcp47, see: + //https://gitlab.freedesktop.org/fontconfig/fontconfig/-/issues/50 + //In the meantime try something that will fit to workaround, see: + //https://gitlab.freedesktop.org/fontconfig/fontconfig/-/issues/30 + OString mapToFontConfigLangTag(const LanguageTag &rLangTag) + { + std::shared_ptr xLangSet(FcGetLangs(), FcStrSetDestroy); + OString sLangAttrib; + + sLangAttrib = OUStringToOString(rLangTag.getBcp47(), RTL_TEXTENCODING_UTF8).toAsciiLowerCase(); + if (FcStrSetMember(xLangSet.get(), reinterpret_cast(sLangAttrib.getStr()))) + { + return sLangAttrib; + } + + sLangAttrib = OUStringToOString(rLangTag.getLanguageAndScript(), RTL_TEXTENCODING_UTF8).toAsciiLowerCase(); + if (FcStrSetMember(xLangSet.get(), reinterpret_cast(sLangAttrib.getStr()))) + { + return sLangAttrib; + } + + OString sLang = OUStringToOString(rLangTag.getLanguage(), RTL_TEXTENCODING_UTF8).toAsciiLowerCase(); + OString sRegion = OUStringToOString(rLangTag.getCountry(), RTL_TEXTENCODING_UTF8).toAsciiLowerCase(); + + if (!sRegion.isEmpty()) + { + sLangAttrib = sLang + "-" + sRegion; + if (FcStrSetMember(xLangSet.get(), reinterpret_cast(sLangAttrib.getStr()))) + { + return sLangAttrib; + } + } + + if (FcStrSetMember(xLangSet.get(), reinterpret_cast(sLang.getStr()))) + { + return sLang; + } + + return OString(); + } + + bool isEmoji(sal_uInt32 nCurrentChar) + { + return u_hasBinaryProperty(nCurrentChar, UCHAR_EMOJI); + } + + //returns true if the given code-point couldn't possibly be in rLangTag. + bool isImpossibleCodePointForLang(const LanguageTag &rLangTag, sal_uInt32 currentChar) + { + //a non-default script is set, lets believe it + if (rLangTag.hasScript()) + return false; + + int32_t script = u_getIntPropertyValue(currentChar, UCHAR_SCRIPT); + UScriptCode eScript = static_cast(script); + bool bIsImpossible = false; + OUString sLang = rLangTag.getLanguage(); + switch (eScript) + { + //http://en.wiktionary.org/wiki/Category:Oriya_script_languages + case USCRIPT_ORIYA: + bIsImpossible = + sLang != "or" && + sLang != "kxv"; + break; + //http://en.wiktionary.org/wiki/Category:Telugu_script_languages + case USCRIPT_TELUGU: + bIsImpossible = + sLang != "te" && + sLang != "gon" && + sLang != "kfc"; + break; + //http://en.wiktionary.org/wiki/Category:Bengali_script_languages + case USCRIPT_BENGALI: + bIsImpossible = + sLang != "bn" && + sLang != "as" && + sLang != "bpy" && + sLang != "ctg" && + sLang != "sa"; + break; + default: + break; + } + SAL_WARN_IF(bIsImpossible, "vcl.fonts", "In glyph fallback throwing away the language property of " + << sLang << " because the detected script for '0x" + << OUString::number(currentChar, 16) + << "' is " << uscript_getName(eScript) + << " and that language doesn't make sense. Autodetecting instead."); + return bIsImpossible; + } + + OUString getExemplarLangTagForCodePoint(sal_uInt32 currentChar) + { + if (isEmoji(currentChar)) + return "und-zsye"; + int32_t script = u_getIntPropertyValue(currentChar, UCHAR_SCRIPT); + UScriptCode eScript = static_cast(script); + OStringBuffer aBuf(unicode::getExemplarLanguageForUScriptCode(eScript)); + if (const char* pScriptCode = uscript_getShortName(eScript)) + aBuf.append(OStringChar('-') + pScriptCode); + return OStringToOUString(aBuf, RTL_TEXTENCODING_UTF8); + } +} + +IMPL_LINK_NOARG(PrintFontManager, autoInstallFontLangSupport, Timer *, void) +{ + try + { + using namespace org::freedesktop::PackageKit; + css::uno::Reference xSyncDbusSessionHelper(SyncDbusSessionHelper::create(comphelper::getProcessComponentContext())); + xSyncDbusSessionHelper->InstallFontconfigResources(comphelper::containerToSequence(m_aCurrentRequests), "hide-finished"); + } + catch (const css::uno::Exception&) + { + TOOLS_INFO_EXCEPTION("vcl.fonts", "InstallFontconfigResources problem"); + // Disable this method from now on. It's simply not available on some systems + // and leads to an error dialog being shown each time this is called tdf#104883 + std::shared_ptr batch( comphelper::ConfigurationChanges::create() ); + officecfg::Office::Common::PackageKit::EnableFontInstallation::set(false, batch); + batch->commit(); + } + + m_aCurrentRequests.clear(); +} + +void PrintFontManager::Substitute(vcl::font::FontSelectPattern &rPattern, OUString& rMissingCodes) +{ + FontCfgWrapper& rWrapper = FontCfgWrapper::get(); + + // build pattern argument for fontconfig query + FcPattern* pPattern = FcPatternCreate(); + + // Prefer scalable fonts + FcPatternAddBool(pPattern, FC_SCALABLE, FcTrue); + + const OString aTargetName = OUStringToOString( rPattern.maTargetName, RTL_TEXTENCODING_UTF8 ); + const FcChar8* pTargetNameUtf8 = reinterpret_cast(aTargetName.getStr()); + FcPatternAddString(pPattern, FC_FAMILY, pTargetNameUtf8); + + LanguageTag aLangTag(rPattern.meLanguage); + OString aLangAttrib = mapToFontConfigLangTag(aLangTag); + + bool bMissingJustBullet = false; + + // Add required Unicode characters, if any + if ( !rMissingCodes.isEmpty() ) + { + FcCharSet *codePoints = FcCharSetCreate(); + bMissingJustBullet = rMissingCodes.getLength() == 1 && rMissingCodes[0] == 0xb7; + for( sal_Int32 nStrIndex = 0; nStrIndex < rMissingCodes.getLength(); ) + { + // also handle unicode surrogates + const sal_uInt32 nCode = rMissingCodes.iterateCodePoints( &nStrIndex ); + FcCharSetAddChar( codePoints, nCode ); + //if the codepoint is impossible for this lang tag, then clear it + //and autodetect something useful + if (!aLangAttrib.isEmpty() && (isImpossibleCodePointForLang(aLangTag, nCode) || isEmoji(nCode))) + aLangAttrib.clear(); + //#i105784#/rhbz#527719 improve selection of fallback font + if (aLangAttrib.isEmpty()) + { + aLangTag.reset(getExemplarLangTagForCodePoint(nCode)); + aLangAttrib = mapToFontConfigLangTag(aLangTag); + } + } + FcPatternAddCharSet(pPattern, FC_CHARSET, codePoints); + FcCharSetDestroy(codePoints); + } + + if (!aLangAttrib.isEmpty()) + FcPatternAddString(pPattern, FC_LANG, reinterpret_cast(aLangAttrib.getStr())); + + addtopattern(pPattern, rPattern.GetItalic(), rPattern.GetWeight(), + rPattern.GetWidthType(), rPattern.GetPitch()); + + // query fontconfig for a substitute + FcConfigSubstitute(FcConfigGetCurrent(), pPattern, FcMatchPattern); + FcDefaultSubstitute(pPattern); + + // process the result of the fontconfig query + FcResult eResult = FcResultNoMatch; + FcFontSet* pFontSet = rWrapper.getFontSet(); + FcPattern* pResult = FcFontSetMatch(FcConfigGetCurrent(), &pFontSet, 1, pPattern, &eResult); + FcPatternDestroy( pPattern ); + + FcFontSet* pSet = nullptr; + if( pResult ) + { + pSet = FcFontSetCreate(); + // info: destroying the pSet destroys pResult implicitly + // since pResult was "added" to pSet + FcFontSetAdd( pSet, pResult ); + } + + if( pSet ) + { + if( pSet->nfont > 0 ) + { + bool bRet = false; + + //extract the closest match + FcChar8* file = nullptr; + FcResult eFileRes = FcPatternGetString(pSet->fonts[0], FC_FILE, 0, &file); + int nEntryId = 0; + FcResult eIndexRes = FcPatternGetInteger(pSet->fonts[0], FC_INDEX, 0, &nEntryId); + if (eIndexRes != FcResultMatch) + nEntryId = 0; + if( eFileRes == FcResultMatch ) + { + OString aDir, aBase, aOrgPath( reinterpret_cast(file) ); + splitPath( aOrgPath, aDir, aBase ); + int nDirID = getDirectoryAtom( aDir ); + fontID nFontID = findFontFileID(nDirID, aBase, GetCollectionIndex(nEntryId), GetVariationIndex(nEntryId)); + auto const* pFont = getFont(nFontID); + if (pFont) + { + rPattern.maSearchName = pFont->m_aFontAttributes.GetFamilyName(); + bRet = true; + } + } + + SAL_WARN_IF(!bRet, "vcl.fonts", "no FC_FILE found, falling back to name search"); + + if (!bRet) + { + FcChar8* family = nullptr; + FcResult eFamilyRes = FcPatternGetString( pSet->fonts[0], FC_FAMILY, 0, &family ); + + // get the family name + if( eFamilyRes == FcResultMatch ) + { + OString sFamily(reinterpret_cast(family)); + std::unordered_map< OString, OString >::const_iterator aI = + rWrapper.m_aFontNameToLocalized.find(sFamily); + if (aI != rWrapper.m_aFontNameToLocalized.end()) + sFamily = aI->second; + rPattern.maSearchName = OStringToOUString( sFamily, RTL_TEXTENCODING_UTF8 ); + bRet = true; + } + } + + if (bRet) + { + int val = 0; + if (FcResultMatch == FcPatternGetInteger(pSet->fonts[0], FC_WEIGHT, 0, &val)) + rPattern.SetWeight( convertWeight(val) ); + if (FcResultMatch == FcPatternGetInteger(pSet->fonts[0], FC_SLANT, 0, &val)) + rPattern.SetItalic( convertSlant(val) ); + if (FcResultMatch == FcPatternGetInteger(pSet->fonts[0], FC_SPACING, 0, &val)) + rPattern.SetPitch ( convertSpacing(val) ); + if (FcResultMatch == FcPatternGetInteger(pSet->fonts[0], FC_WIDTH, 0, &val)) + rPattern.SetWidthType ( convertWidth(val) ); + FcBool bEmbolden; + if (FcResultMatch == FcPatternGetBool(pSet->fonts[0], FC_EMBOLDEN, 0, &bEmbolden)) + rPattern.mbEmbolden = bEmbolden; + FcMatrix *pMatrix = nullptr; + if (FcResultMatch == FcPatternGetMatrix(pSet->fonts[0], FC_MATRIX, 0, &pMatrix)) + { + rPattern.maItalicMatrix.xx = pMatrix->xx; + rPattern.maItalicMatrix.xy = pMatrix->xy; + rPattern.maItalicMatrix.yx = pMatrix->yx; + rPattern.maItalicMatrix.yy = pMatrix->yy; + } + } + + // update rMissingCodes by removing resolved code points + if( !rMissingCodes.isEmpty() ) + { + std::unique_ptr const pRemainingCodes(new sal_uInt32[rMissingCodes.getLength()]); + int nRemainingLen = 0; + FcCharSet* codePoints; + if (!FcPatternGetCharSet(pSet->fonts[0], FC_CHARSET, 0, &codePoints)) + { + for( sal_Int32 nStrIndex = 0; nStrIndex < rMissingCodes.getLength(); ) + { + // also handle surrogates + const sal_uInt32 nCode = rMissingCodes.iterateCodePoints( &nStrIndex ); + if (FcCharSetHasChar(codePoints, nCode) != FcTrue) + pRemainingCodes[ nRemainingLen++ ] = nCode; + } + } + OUString sStillMissing(pRemainingCodes.get(), nRemainingLen); + if (!Application::IsHeadlessModeEnabled() && officecfg::Office::Common::PackageKit::EnableFontInstallation::get()) + { + if (sStillMissing == rMissingCodes) //replaced nothing + { + //It'd be better if we could ask packagekit using the + //missing codepoints or some such rather than using + //"language" as a proxy to how fontconfig considers + //scripts to default to a given language. + for (sal_Int32 i = 0; i < nRemainingLen; ++i) + { + LanguageTag aOurTag(getExemplarLangTagForCodePoint(pRemainingCodes[i])); + OString sTag = OUStringToOString(aOurTag.getBcp47(), RTL_TEXTENCODING_UTF8); + if (!m_aPreviousLangSupportRequests.insert(sTag).second) + continue; + sTag = mapToFontConfigLangTag(aOurTag); + if (!sTag.isEmpty() && m_aPreviousLangSupportRequests.find(sTag) == m_aPreviousLangSupportRequests.end()) + { + OString sReq = OString::Concat(":lang=") + sTag; + m_aCurrentRequests.push_back(OUString::fromUtf8(sReq)); + m_aPreviousLangSupportRequests.insert(sTag); + } + } + } + if (!m_aCurrentRequests.empty()) + m_aFontInstallerTimer.Start(); + } + rMissingCodes = sStillMissing; + } + } + + FcFontSetDestroy( pSet ); + } + + SAL_INFO("vcl.fonts", "PrintFontManager::Substitute: replacing missing font: '" + << rPattern.maTargetName << "' with '" << rPattern.maSearchName + << "'"); + + static const bool bAbortOnFontSubstitute = [] { + const char* pEnv = getenv("SAL_NON_APPLICATION_FONT_USE"); + return pEnv && strcmp(pEnv, "abort") == 0; + }(); + if (bAbortOnFontSubstitute && rPattern.maTargetName != rPattern.maSearchName) + { + if (bMissingJustBullet) + { + // Some fonts exist in "more_fonts", but have no U+00B7 MIDDLE DOT + // so will always glyph fallback on measuring mnBulletOffset in + // FontMetricData::ImplInitTextLineSize + return; + } + if (rPattern.maTargetName == "Linux Libertine G" && rPattern.maSearchName == "Linux Libertine O") + return; + SAL_WARN("vcl.fonts", "PrintFontManager::Substitute: missing font: '" << rPattern.maTargetName << + "' try: " << rPattern.maSearchName << " instead"); + std::cerr << "terminating test due to missing font: " << rPattern.maTargetName << std::endl; + std::abort(); + } +} + +FontConfigFontOptions::~FontConfigFontOptions() +{ + FcPatternDestroy(mpPattern); +} + +FcPattern *FontConfigFontOptions::GetPattern() const +{ + return mpPattern; +} + +void FontConfigFontOptions::SyncPattern(const OString& rFileName, sal_uInt32 nIndex, sal_uInt32 nVariation, bool bEmbolden) +{ + FcPatternDel(mpPattern, FC_FILE); + FcPatternAddString(mpPattern, FC_FILE, reinterpret_cast(rFileName.getStr())); + FcPatternDel(mpPattern, FC_INDEX); + sal_uInt32 nFcIndex = (nVariation << 16) | nIndex; + FcPatternAddInteger(mpPattern, FC_INDEX, nFcIndex); + FcPatternDel(mpPattern, FC_EMBOLDEN); + FcPatternAddBool(mpPattern, FC_EMBOLDEN, bEmbolden ? FcTrue : FcFalse); +} + +std::unique_ptr PrintFontManager::getFontOptions(const FontAttributes& rInfo, int nSize) +{ + FontOptionsKey aKey{ rInfo.GetFamilyName(), nSize, rInfo.GetItalic(), + rInfo.GetWeight(), rInfo.GetWidthType(), rInfo.GetPitch() }; + + FontCfgWrapper& rWrapper = FontCfgWrapper::get(); + + std::unique_ptr pOptions = rWrapper.m_aCachedFontOptions.lookup(aKey); + if (pOptions) + return pOptions; + + FcConfig* pConfig = FcConfigGetCurrent(); + FcPattern* pPattern = FcPatternCreate(); + + OString sFamily = OUStringToOString(aKey.m_sFamilyName, RTL_TEXTENCODING_UTF8); + + std::unordered_map< OString, OString >::const_iterator aI = rWrapper.m_aLocalizedToCanonical.find(sFamily); + if (aI != rWrapper.m_aLocalizedToCanonical.end()) + sFamily = aI->second; + if( !sFamily.isEmpty() ) + FcPatternAddString(pPattern, FC_FAMILY, reinterpret_cast(sFamily.getStr())); + + addtopattern(pPattern, aKey.m_eItalic, aKey.m_eWeight, aKey.m_eWidth, aKey.m_ePitch); + FcPatternAddDouble(pPattern, FC_PIXEL_SIZE, nSize); + + FcConfigSubstitute(pConfig, pPattern, FcMatchPattern); + FontConfigFontOptions::cairo_font_options_substitute(pPattern); + FcDefaultSubstitute(pPattern); + + FcResult eResult = FcResultNoMatch; + FcFontSet* pFontSet = rWrapper.getFontSet(); + if (FcPattern* pResult = FcFontSetMatch(pConfig, &pFontSet, 1, pPattern, &eResult)) + { + rWrapper.m_aCachedFontOptions.cache(aKey, pResult); + pOptions.reset(new FontConfigFontOptions(pResult)); + } + + // cleanup + FcPatternDestroy( pPattern ); + + return pOptions; +} + + +bool PrintFontManager::matchFont(FontAttributes& rDFA, const css::lang::Locale& rLocale) +{ + bool bFound = false; + FontCfgWrapper& rWrapper = FontCfgWrapper::get(); + + FcConfig* pConfig = FcConfigGetCurrent(); + FcPattern* pPattern = FcPatternCreate(); + + // populate pattern with font characteristics + const LanguageTag aLangTag(rLocale); + const OString aLangAttrib = mapToFontConfigLangTag(aLangTag); + if (!aLangAttrib.isEmpty()) + FcPatternAddString(pPattern, FC_LANG, reinterpret_cast(aLangAttrib.getStr())); + + OString aFamily = OUStringToOString(rDFA.GetFamilyName(), RTL_TEXTENCODING_UTF8); + if( !aFamily.isEmpty() ) + FcPatternAddString(pPattern, FC_FAMILY, reinterpret_cast(aFamily.getStr())); + + addtopattern(pPattern, rDFA.GetItalic(), rDFA.GetWeight(), rDFA.GetWidthType(), rDFA.GetPitch()); + + FcConfigSubstitute(pConfig, pPattern, FcMatchPattern); + FcDefaultSubstitute(pPattern); + FcResult eResult = FcResultNoMatch; + FcFontSet *pFontSet = rWrapper.getFontSet(); + FcPattern* pResult = FcFontSetMatch(pConfig, &pFontSet, 1, pPattern, &eResult); + if( pResult ) + { + FcFontSet* pSet = FcFontSetCreate(); + FcFontSetAdd( pSet, pResult ); + if( pSet->nfont > 0 ) + { + //extract the closest match + FcChar8* file = nullptr; + FcResult eFileRes = FcPatternGetString(pSet->fonts[0], FC_FILE, 0, &file); + int nEntryId = 0; + FcResult eIndexRes = FcPatternGetInteger(pSet->fonts[0], FC_INDEX, 0, &nEntryId); + if (eIndexRes != FcResultMatch) + nEntryId = 0; + if( eFileRes == FcResultMatch ) + { + OString aDir, aBase, aOrgPath( reinterpret_cast(file) ); + splitPath( aOrgPath, aDir, aBase ); + int nDirID = getDirectoryAtom( aDir ); + fontID nFontID = findFontFileID(nDirID, aBase, + GetCollectionIndex(nEntryId), + GetVariationIndex(nEntryId)); + auto const* pFont = getFont(nFontID); + if (pFont) + { + rDFA = pFont->m_aFontAttributes; + bFound = true; + } + } + } + // info: destroying the pSet destroys pResult implicitly + // since pResult was "added" to pSet + FcFontSetDestroy( pSet ); + } + + // cleanup + FcPatternDestroy( pPattern ); + + return bFound; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/fontmanager/fontmanager.cxx b/vcl/unx/generic/fontmanager/fontmanager.cxx new file mode 100644 index 0000000000..a9ab5202cb --- /dev/null +++ b/vcl/unx/generic/fontmanager/fontmanager.cxx @@ -0,0 +1,736 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include + +#include +#include + +#include + +#include + +#if OSL_DEBUG_LEVEL > 1 +#include +#include +#endif + +#include +#include + +#ifdef CALLGRIND_COMPILE +#include +#endif + +#include + +using namespace vcl; +using namespace utl; +using namespace psp; +using namespace osl; +using namespace com::sun::star::uno; +using namespace com::sun::star::beans; +using namespace com::sun::star::lang; + +/* + * static helpers + */ + +static sal_uInt16 getUInt16BE( const sal_uInt8*& pBuffer ) +{ + sal_uInt16 nRet = static_cast(pBuffer[1]) | + (static_cast(pBuffer[0]) << 8); + pBuffer+=2; + return nRet; +} + +/* + * PrintFont implementations + */ +PrintFontManager::PrintFont::PrintFont() +: m_nDirectory(0) +, m_nCollectionEntry(0) +, m_nVariationEntry(0) +{ +} + +/* + * one instance only + */ +PrintFontManager& PrintFontManager::get() +{ + GenericUnixSalData* const pSalData(GetGenericUnixSalData()); + assert(pSalData); + return *pSalData->GetPrintFontManager(); +} + +/* + * the PrintFontManager + */ + +PrintFontManager::PrintFontManager() + : m_nNextFontID( 1 ) + , m_nNextDirAtom( 1 ) + , m_aFontInstallerTimer("PrintFontManager m_aFontInstallerTimer") +{ + m_aFontInstallerTimer.SetInvokeHandler(LINK(this, PrintFontManager, autoInstallFontLangSupport)); + m_aFontInstallerTimer.SetTimeout(5000); +} + +PrintFontManager::~PrintFontManager() +{ + m_aFontInstallerTimer.Stop(); + deinitFontconfig(); +} + +OString PrintFontManager::getDirectory( int nAtom ) const +{ + std::unordered_map< int, OString >::const_iterator it( m_aAtomToDir.find( nAtom ) ); + return it != m_aAtomToDir.end() ? it->second : OString(); +} + +int PrintFontManager::getDirectoryAtom( const OString& rDirectory ) +{ + int nAtom = 0; + std::unordered_map< OString, int >::const_iterator it + ( m_aDirToAtom.find( rDirectory ) ); + if( it != m_aDirToAtom.end() ) + nAtom = it->second; + else + { + nAtom = m_nNextDirAtom++; + m_aDirToAtom[ rDirectory ] = nAtom; + m_aAtomToDir[ nAtom ] = rDirectory; + } + return nAtom; +} + +std::vector PrintFontManager::addFontFile( std::u16string_view rFileUrl ) +{ + rtl_TextEncoding aEncoding = osl_getThreadTextEncoding(); + INetURLObject aPath( rFileUrl ); + OString aName(OUStringToOString(aPath.GetLastName(INetURLObject::DecodeMechanism::WithCharset, aEncoding), aEncoding)); + OString aDir( OUStringToOString( + INetURLObject::decode( aPath.GetPath(), INetURLObject::DecodeMechanism::WithCharset, aEncoding ), aEncoding ) ); + + int nDirID = getDirectoryAtom( aDir ); + std::vector aFontIds = findFontFileIDs( nDirID, aName ); + if( aFontIds.empty() ) + { + addFontconfigFile(OUStringToOString(aPath.GetFull(), osl_getThreadTextEncoding())); + + std::vector aNewFonts = analyzeFontFile(nDirID, aName); + for (auto & font : aNewFonts) + { + fontID nFontId = m_nNextFontID++; + m_aFonts[nFontId] = std::move(font); + m_aFontFileToFontID[ aName ].insert( nFontId ); + aFontIds.push_back(nFontId); + } + } + return aFontIds; +} + +std::vector PrintFontManager::analyzeFontFile( int nDirID, const OString& rFontFile, const char *pFormat ) const +{ + std::vector aNewFonts; + + OString aDir( getDirectory( nDirID ) ); + + OString aFullPath = aDir + "/" + rFontFile; + + bool bSupported; + int nFD; + int n; + if (sscanf(aFullPath.getStr(), "/:FD:/%d%n", &nFD, &n) == 1 && aFullPath.getStr()[n] == '\0') + { + // Hack, pathname that actually means we will use a pre-opened file descriptor + bSupported = true; + } + else + { + // #i1872# reject unreadable files + if( access( aFullPath.getStr(), R_OK ) ) + return aNewFonts; + + bSupported = false; + if (pFormat) + { + if (!strcmp(pFormat, "TrueType") || + !strcmp(pFormat, "CFF")) + bSupported = true; + } + if (!bSupported) + { + OString aExt( rFontFile.copy( rFontFile.lastIndexOf( '.' )+1 ) ); + if( aExt.equalsIgnoreAsciiCase("ttf") + || aExt.equalsIgnoreAsciiCase("ttc") + || aExt.equalsIgnoreAsciiCase("tte") // #i33947# for Gaiji support + || aExt.equalsIgnoreAsciiCase("otf") ) // check for TTF- and PS-OpenType too + bSupported = true; + } + } + + if (bSupported) + { + // get number of ttc entries + int nLength = CountTTCFonts( aFullPath.getStr() ); + if (nLength > 0) + { + SAL_INFO("vcl.fonts", "ttc: " << aFullPath << " contains " << nLength << " fonts"); + + for( int i = 0; i < nLength; i++ ) + { + PrintFont aFont; + aFont.m_nDirectory = nDirID; + aFont.m_aFontFile = rFontFile; + aFont.m_nCollectionEntry = i; + if (analyzeSfntFile(aFont)) + aNewFonts.push_back(aFont); + } + } + else + { + PrintFont aFont; + aFont.m_nDirectory = nDirID; + aFont.m_aFontFile = rFontFile; + aFont.m_nCollectionEntry = 0; + + // need to read the font anyway to get aliases inside the font file + if (analyzeSfntFile(aFont)) + aNewFonts.push_back(aFont); + } + } + return aNewFonts; +} + +fontID PrintFontManager::findFontFileID(int nDirID, const OString& rFontFile, int nFaceIndex, int nVariationIndex) const +{ + fontID nID = 0; + + auto set_it = m_aFontFileToFontID.find( rFontFile ); + if( set_it == m_aFontFileToFontID.end() ) + return nID; + + for (fontID elem : set_it->second) + { + auto it = m_aFonts.find(elem); + if( it == m_aFonts.end() ) + continue; + const PrintFont& rFont = (*it).second; + if (rFont.m_nDirectory == nDirID && + rFont.m_aFontFile == rFontFile && + rFont.m_nCollectionEntry == nFaceIndex && + rFont.m_nVariationEntry == nVariationIndex) + { + nID = it->first; + if (nID) + break; + } + } + + return nID; +} + +std::vector PrintFontManager::findFontFileIDs( int nDirID, const OString& rFontFile ) const +{ + std::vector aIds; + + auto set_it = m_aFontFileToFontID.find( rFontFile ); + if( set_it == m_aFontFileToFontID.end() ) + return aIds; + + for (auto const& elem : set_it->second) + { + auto it = m_aFonts.find(elem); + if( it == m_aFonts.end() ) + continue; + const PrintFont& rFont = (*it).second; + if (rFont.m_nDirectory == nDirID && + rFont.m_aFontFile == rFontFile) + aIds.push_back(it->first); + } + + return aIds; +} + +namespace { + +OUString convertSfntName( const NameRecord& rNameRecord ) +{ + OUString aValue; + if( + ( rNameRecord.platformID == 3 && ( rNameRecord.encodingID == 0 || rNameRecord.encodingID == 1 ) ) // MS, Unicode + || + ( rNameRecord.platformID == 0 ) // Apple, Unicode + ) + { + OUStringBuffer aName( rNameRecord.sptr.size()/2 ); + const sal_uInt8* pNameBuffer = rNameRecord.sptr.data(); + for(size_t n = 0; n < rNameRecord.sptr.size()/2; n++ ) + aName.append( static_cast(getUInt16BE( pNameBuffer )) ); + aValue = aName.makeStringAndClear(); + } + else if( rNameRecord.platformID == 3 ) + { + if( rNameRecord.encodingID >= 2 && rNameRecord.encodingID <= 6 ) + { + /* + * and now for a special kind of madness: + * some fonts encode their byte value string as BE uint16 + * (leading to stray zero bytes in the string) + * while others code two bytes as a uint16 and swap to BE + */ + OStringBuffer aName; + const sal_uInt8* pNameBuffer = rNameRecord.sptr.data(); + for(size_t n = 0; n < rNameRecord.sptr.size()/2; n++ ) + { + sal_Unicode aCode = static_cast(getUInt16BE( pNameBuffer )); + char aChar = aCode >> 8; + if( aChar ) + aName.append( aChar ); + aChar = aCode & 0x00ff; + if( aChar ) + aName.append( aChar ); + } + switch( rNameRecord.encodingID ) + { + case 2: + aValue = OStringToOUString( aName, RTL_TEXTENCODING_MS_932 ); + break; + case 3: + aValue = OStringToOUString( aName, RTL_TEXTENCODING_MS_936 ); + break; + case 4: + aValue = OStringToOUString( aName, RTL_TEXTENCODING_MS_950 ); + break; + case 5: + aValue = OStringToOUString( aName, RTL_TEXTENCODING_MS_949 ); + break; + case 6: + aValue = OStringToOUString( aName, RTL_TEXTENCODING_MS_1361 ); + break; + } + } + } + else if( rNameRecord.platformID == 1 ) + { + std::string_view aName(reinterpret_cast(rNameRecord.sptr.data()), rNameRecord.sptr.size()); + rtl_TextEncoding eEncoding = RTL_TEXTENCODING_DONTKNOW; + switch (rNameRecord.encodingID) + { + case 0: + eEncoding = RTL_TEXTENCODING_APPLE_ROMAN; + break; + case 1: + eEncoding = RTL_TEXTENCODING_APPLE_JAPANESE; + break; + case 2: + eEncoding = RTL_TEXTENCODING_APPLE_CHINTRAD; + break; + case 3: + eEncoding = RTL_TEXTENCODING_APPLE_KOREAN; + break; + case 4: + eEncoding = RTL_TEXTENCODING_APPLE_ARABIC; + break; + case 5: + eEncoding = RTL_TEXTENCODING_APPLE_HEBREW; + break; + case 6: + eEncoding = RTL_TEXTENCODING_APPLE_GREEK; + break; + case 7: + eEncoding = RTL_TEXTENCODING_APPLE_CYRILLIC; + break; + case 9: + eEncoding = RTL_TEXTENCODING_APPLE_DEVANAGARI; + break; + case 10: + eEncoding = RTL_TEXTENCODING_APPLE_GURMUKHI; + break; + case 11: + eEncoding = RTL_TEXTENCODING_APPLE_GUJARATI; + break; + case 21: + eEncoding = RTL_TEXTENCODING_APPLE_THAI; + break; + case 25: + eEncoding = RTL_TEXTENCODING_APPLE_CHINSIMP; + break; + case 29: + eEncoding = RTL_TEXTENCODING_APPLE_CENTEURO; + break; + case 32: //Uninterpreted + eEncoding = RTL_TEXTENCODING_UTF8; + break; + default: + if (o3tl::starts_with(aName, "Khmer OS") || // encoding '20' (Khmer) isn't implemented + o3tl::starts_with(aName, "YoavKtav")) // tdf#152278 + { + eEncoding = RTL_TEXTENCODING_UTF8; + } + SAL_WARN_IF(eEncoding == RTL_TEXTENCODING_DONTKNOW, "vcl.fonts", "mac encoding " << + rNameRecord.encodingID << " in font '" << aName << "'" << + (rNameRecord.encodingID > 32 ? " is invalid" : " has unimplemented conversion")); + break; + } + if (eEncoding != RTL_TEXTENCODING_DONTKNOW) + aValue = OStringToOUString(aName, eEncoding); + } + + return aValue; +} + +OUString analyzeSfntFamilyName(void const * pTTFont) +{ + OUString aFamily; + + std::vector aNameRecords; + GetTTNameRecords( static_cast(pTTFont), aNameRecords ); + if( !aNameRecords.empty() ) + { + LanguageTag aUILang(SvtSysLocaleOptions().GetRealUILanguageTag()); + LanguageType eLang = aUILang.getLanguageType(); + int nLastMatch = -1; + for( size_t i = 0; i < aNameRecords.size(); i++ ) + { + if( aNameRecords[i].nameID != 1 || aNameRecords[i].sptr.empty() ) + continue; + int nMatch = -1; + if( aNameRecords[i].platformID == 0 ) // Unicode + nMatch = 4000; + else if( aNameRecords[i].platformID == 3 ) + { + // this bases on the LanguageType actually being a Win LCID + if (aNameRecords[i].languageID == eLang) + nMatch = 8000; + else if( aNameRecords[i].languageID == LANGUAGE_ENGLISH_US ) + nMatch = 2000; + else if( aNameRecords[i].languageID == LANGUAGE_ENGLISH || + aNameRecords[i].languageID == LANGUAGE_ENGLISH_UK ) + nMatch = 1500; + else + nMatch = 1000; + } + else if (aNameRecords[i].platformID == 1) + { + AppleLanguageId aAppleId = static_cast(static_cast(aNameRecords[i].languageID)); + LanguageTag aApple(makeLanguageTagFromAppleLanguageId(aAppleId)); + if (aApple == aUILang) + nMatch = 8000; + else if (aAppleId == AppleLanguageId::ENGLISH) + nMatch = 2000; + else + nMatch = 1000; + } + OUString aName = convertSfntName( aNameRecords[i] ); + if (!(aName.isEmpty()) && nMatch > nLastMatch) + { + nLastMatch = nMatch; + aFamily = aName; + } + } + } + + return aFamily; +} + +} + +bool PrintFontManager::analyzeSfntFile( PrintFont& rFont ) const +{ + bool bSuccess = false; + rtl_TextEncoding aEncoding = osl_getThreadTextEncoding(); + OString aFile = getFontFile( rFont ); + TrueTypeFont* pTTFont = nullptr; + + auto& rDFA = rFont.m_aFontAttributes; + rDFA.SetQuality(512); + + auto const e = OpenTTFontFile( aFile.getStr(), rFont.m_nCollectionEntry, &pTTFont ); + if( e == SFErrCodes::Ok ) + { + TTGlobalFontInfo aInfo; + GetTTGlobalFontInfo( pTTFont, & aInfo ); + + if (rDFA.GetFamilyName().isEmpty()) + { + OUString aFamily = analyzeSfntFamilyName(pTTFont); + if (aFamily.isEmpty()) + { + // poor font does not have a family name + // name it to file name minus the extension + sal_Int32 dotIndex = rFont.m_aFontFile.lastIndexOf('.'); + if ( dotIndex == -1 ) + dotIndex = rFont.m_aFontFile.getLength(); + aFamily = OStringToOUString(rFont.m_aFontFile.subView(0, dotIndex), aEncoding); + } + + rDFA.SetFamilyName(aFamily); + } + + if( !aInfo.usubfamily.isEmpty() ) + rDFA.SetStyleName(aInfo.usubfamily); + + rDFA.SetFamilyType(matchFamilyName(rDFA.GetFamilyName())); + + switch( aInfo.weight ) + { + case FW_THIN: rDFA.SetWeight(WEIGHT_THIN); break; + case FW_EXTRALIGHT: rDFA.SetWeight(WEIGHT_ULTRALIGHT); break; + case FW_LIGHT: rDFA.SetWeight(WEIGHT_LIGHT); break; + case FW_MEDIUM: rDFA.SetWeight(WEIGHT_MEDIUM); break; + case FW_SEMIBOLD: rDFA.SetWeight(WEIGHT_SEMIBOLD); break; + case FW_BOLD: rDFA.SetWeight(WEIGHT_BOLD); break; + case FW_EXTRABOLD: rDFA.SetWeight(WEIGHT_ULTRABOLD); break; + case FW_BLACK: rDFA.SetWeight(WEIGHT_BLACK); break; + + case FW_NORMAL: + default: rDFA.SetWeight(WEIGHT_NORMAL); break; + } + + switch( aInfo.width ) + { + case FWIDTH_ULTRA_CONDENSED: rDFA.SetWidthType(WIDTH_ULTRA_CONDENSED); break; + case FWIDTH_EXTRA_CONDENSED: rDFA.SetWidthType(WIDTH_EXTRA_CONDENSED); break; + case FWIDTH_CONDENSED: rDFA.SetWidthType(WIDTH_CONDENSED); break; + case FWIDTH_SEMI_CONDENSED: rDFA.SetWidthType(WIDTH_SEMI_CONDENSED); break; + case FWIDTH_SEMI_EXPANDED: rDFA.SetWidthType(WIDTH_SEMI_EXPANDED); break; + case FWIDTH_EXPANDED: rDFA.SetWidthType(WIDTH_EXPANDED); break; + case FWIDTH_EXTRA_EXPANDED: rDFA.SetWidthType(WIDTH_EXTRA_EXPANDED); break; + case FWIDTH_ULTRA_EXPANDED: rDFA.SetWidthType(WIDTH_ULTRA_EXPANDED); break; + + case FWIDTH_NORMAL: + default: rDFA.SetWidthType(WIDTH_NORMAL); break; + } + + rDFA.SetPitch(aInfo.pitch ? PITCH_FIXED : PITCH_VARIABLE); + rDFA.SetItalic(aInfo.italicAngle == 0 ? ITALIC_NONE : (aInfo.italicAngle < 0 ? ITALIC_NORMAL : ITALIC_OBLIQUE)); + // #104264# there are fonts that set italic angle 0 although they are + // italic; use macstyle bit here + if( aInfo.italicAngle == 0 && (aInfo.macStyle & 2) ) + rDFA.SetItalic(ITALIC_NORMAL); + + rDFA.SetMicrosoftSymbolEncoded(aInfo.microsoftSymbolEncoded); + + CloseTTFont( pTTFont ); + bSuccess = true; + } + else + SAL_WARN("vcl.fonts", "Could not OpenTTFont \"" << aFile << "\": " << int(e)); + + return bSuccess; +} + +void PrintFontManager::initialize() +{ + #ifdef CALLGRIND_COMPILE + CALLGRIND_TOGGLE_COLLECT(); + CALLGRIND_ZERO_STATS(); + #endif + + // initialize can be called more than once, e.g. + // gtk-fontconfig-timestamp changes to reflect new font installed and + // PrintFontManager::initialize called again + { + m_nNextFontID = 1; + m_aFonts.clear(); + } +#if OSL_DEBUG_LEVEL > 1 + clock_t aStart; + clock_t aStep1; + clock_t aStep2; + + struct tms tms; + + aStart = times( &tms ); +#endif + + // first try fontconfig + initFontconfig(); + + // part one - look for downloadable fonts + rtl_TextEncoding aEncoding = osl_getThreadTextEncoding(); + const OUString &rSalPrivatePath = psp::getFontPath(); + + // search for the fonts in SAL_PRIVATE_FONTPATH first; those are + // the fonts installed with the office + if( !rSalPrivatePath.isEmpty() ) + { + OString aPath = OUStringToOString( rSalPrivatePath, aEncoding ); + sal_Int32 nIndex = 0; + do + { + OString aToken = aPath.getToken( 0, ';', nIndex ); + normPath( aToken ); + if (!aToken.isEmpty()) + addFontconfigDir(aToken); + } while( nIndex >= 0 ); + } + + countFontconfigFonts(); + +#if OSL_DEBUG_LEVEL > 1 + aStep1 = times( &tms ); + + aStep2 = times( &tms ); + SAL_INFO("vcl.fonts", "PrintFontManager::initialize: collected " + << m_aFonts.size() + << " fonts."); + double fTick = (double)sysconf( _SC_CLK_TCK ); + SAL_INFO("vcl.fonts", "Step 1 took " + << ((double)(aStep1 - aStart)/fTick) + << " seconds."); + SAL_INFO("vcl.fonts", "Step 2 took " + << ((double)(aStep2 - aStep1)/fTick) + << " seconds."); +#endif + + #ifdef CALLGRIND_COMPILE + CALLGRIND_DUMP_STATS(); + CALLGRIND_TOGGLE_COLLECT(); + #endif +} + +void PrintFontManager::getFontList( ::std::vector< fontID >& rFontIDs ) +{ + rFontIDs.clear(); + + for (auto const& font : m_aFonts) + rFontIDs.push_back(font.first); +} + +int PrintFontManager::getFontFaceNumber( fontID nFontID ) const +{ + int nRet = 0; + const PrintFont* pFont = getFont( nFontID ); + if (pFont) + { + nRet = pFont->m_nCollectionEntry; + if (nRet < 0) + nRet = 0; + } + return nRet; +} + +int PrintFontManager::getFontFaceVariation( fontID nFontID ) const +{ + int nRet = 0; + const PrintFont* pFont = getFont( nFontID ); + if (pFont) + { + nRet = pFont->m_nVariationEntry; + if (nRet < 0) + nRet = 0; + } + return nRet; +} + +FontFamily PrintFontManager::matchFamilyName( std::u16string_view rFamily ) +{ + struct family_t { + const char* mpName; + sal_uInt16 mnLength; + FontFamily meType; + }; + +#define InitializeClass( p, a ) p, sizeof(p) - 1, a + static const family_t pFamilyMatch[] = { + { InitializeClass( "arial", FAMILY_SWISS ) }, + { InitializeClass( "arioso", FAMILY_SCRIPT ) }, + { InitializeClass( "avant garde", FAMILY_SWISS ) }, + { InitializeClass( "avantgarde", FAMILY_SWISS ) }, + { InitializeClass( "bembo", FAMILY_ROMAN ) }, + { InitializeClass( "bookman", FAMILY_ROMAN ) }, + { InitializeClass( "conga", FAMILY_ROMAN ) }, + { InitializeClass( "courier", FAMILY_MODERN ) }, + { InitializeClass( "curl", FAMILY_SCRIPT ) }, + { InitializeClass( "fixed", FAMILY_MODERN ) }, + { InitializeClass( "gill", FAMILY_SWISS ) }, + { InitializeClass( "helmet", FAMILY_MODERN ) }, + { InitializeClass( "helvetica", FAMILY_SWISS ) }, + { InitializeClass( "international", FAMILY_MODERN ) }, + { InitializeClass( "lucida", FAMILY_SWISS ) }, + { InitializeClass( "new century schoolbook", FAMILY_ROMAN ) }, + { InitializeClass( "palatino", FAMILY_ROMAN ) }, + { InitializeClass( "roman", FAMILY_ROMAN ) }, + { InitializeClass( "sans serif", FAMILY_SWISS ) }, + { InitializeClass( "sansserif", FAMILY_SWISS ) }, + { InitializeClass( "serf", FAMILY_ROMAN ) }, + { InitializeClass( "serif", FAMILY_ROMAN ) }, + { InitializeClass( "times", FAMILY_ROMAN ) }, + { InitializeClass( "utopia", FAMILY_ROMAN ) }, + { InitializeClass( "zapf chancery", FAMILY_SCRIPT ) }, + { InitializeClass( "zapfchancery", FAMILY_SCRIPT ) } + }; + + OString aFamily = OUStringToOString( rFamily, RTL_TEXTENCODING_ASCII_US ); + sal_uInt32 nLower = 0; + sal_uInt32 nUpper = SAL_N_ELEMENTS(pFamilyMatch); + + while( nLower < nUpper ) + { + sal_uInt32 nCurrent = (nLower + nUpper) / 2; + const family_t* pHaystack = pFamilyMatch + nCurrent; + sal_Int32 nComparison = + rtl_str_compareIgnoreAsciiCase_WithLength + ( + aFamily.getStr(), aFamily.getLength(), + pHaystack->mpName, pHaystack->mnLength + ); + + if( nComparison < 0 ) + nUpper = nCurrent; + else + if( nComparison > 0 ) + nLower = nCurrent + 1; + else + return pHaystack->meType; + } + + return FAMILY_DONTKNOW; +} + +OString PrintFontManager::getFontFile(const PrintFont& rFont) const +{ + std::unordered_map< int, OString >::const_iterator it = m_aAtomToDir.find(rFont.m_nDirectory); + assert(it != m_aAtomToDir.end()); + OString aPath = it->second + "/" + rFont.m_aFontFile; + return aPath; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/fontmanager/fontsubst.cxx b/vcl/unx/generic/fontmanager/fontsubst.cxx new file mode 100644 index 0000000000..d4fae2f790 --- /dev/null +++ b/vcl/unx/generic/fontmanager/fontsubst.cxx @@ -0,0 +1,223 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include +#include +#include + +// platform specific font substitution hooks + +namespace { + +class FcPreMatchSubstitution +: public vcl::font::PreMatchFontSubstitution +{ +public: + bool FindFontSubstitute( vcl::font::FontSelectPattern& ) const override; + typedef ::std::pair value_type; +private: + typedef ::std::list CachedFontMapType; + mutable CachedFontMapType maCachedFontMap; +}; + +class FcGlyphFallbackSubstitution +: public vcl::font::GlyphFallbackFontSubstitution +{ + // TODO: add a cache +public: + bool FindFontSubstitute(vcl::font::FontSelectPattern&, LogicalFontInstance* pLogicalFont, OUString& rMissingCodes) const override; +}; + +} + +void SalGenericInstance::RegisterFontSubstitutors(vcl::font::PhysicalFontCollection* pFontCollection) +{ + // register font fallback substitutions + static FcPreMatchSubstitution aSubstPreMatch; + pFontCollection->SetPreMatchHook( &aSubstPreMatch ); + + // register glyph fallback substitutions + static FcGlyphFallbackSubstitution aSubstFallback; + pFontCollection->SetFallbackHook( &aSubstFallback ); +} + +static vcl::font::FontSelectPattern GetFcSubstitute(const vcl::font::FontSelectPattern &rFontSelData, OUString& rMissingCodes) +{ + vcl::font::FontSelectPattern aSubstituted(rFontSelData); + psp::PrintFontManager& rMgr = psp::PrintFontManager::get(); + rMgr.Substitute(aSubstituted, rMissingCodes); + return aSubstituted; +} + +namespace +{ + bool uselessmatch(const vcl::font::FontSelectPattern &rOrig, const vcl::font::FontSelectPattern &rNew) + { + return + ( + rOrig.maTargetName == rNew.maSearchName && + rOrig.GetWeight() == rNew.GetWeight() && + rOrig.GetItalic() == rNew.GetItalic() && + rOrig.GetPitch() == rNew.GetPitch() && + rOrig.GetWidthType() == rNew.GetWidthType() + ); + } + + class equal + { + private: + const vcl::font::FontSelectPattern& mrAttributes; + public: + explicit equal(const vcl::font::FontSelectPattern& rAttributes) + : mrAttributes(rAttributes) + { + } + bool operator()(const FcPreMatchSubstitution::value_type& rOther) const + { return rOther.first == mrAttributes; } + }; +} + +bool FcPreMatchSubstitution::FindFontSubstitute(vcl::font::FontSelectPattern &rFontSelData) const +{ + // We don't actually want to talk to Fontconfig at all for symbol fonts + if( rFontSelData.IsMicrosoftSymbolEncoded() ) + return false; + // OpenSymbol is a unicode font, but it still deserves to be treated as a symbol font + if ( IsOpenSymbol(rFontSelData.maSearchName) ) + return false; + + //see fdo#41556 and fdo#47636 + //fontconfig can return e.g. an italic font for a non-italic input and/or + //different fonts depending on fontsize, bold, etc settings so don't cache + //just on the name, cache map all the input and all the output not just map + //from original selection to output fontname + vcl::font::FontSelectPattern& rPatternAttributes = rFontSelData; + CachedFontMapType &rCachedFontMap = maCachedFontMap; + CachedFontMapType::iterator itr = std::find_if(rCachedFontMap.begin(), rCachedFontMap.end(), equal(rPatternAttributes)); + if (itr != rCachedFontMap.end()) + { + // Cached substitution + rFontSelData = itr->second; + if (itr != rCachedFontMap.begin()) + { + // MRU, move it to the front + rCachedFontMap.splice(rCachedFontMap.begin(), rCachedFontMap, itr); + } + return true; + } + + OUString aDummy; + const vcl::font::FontSelectPattern aOut = GetFcSubstitute( rFontSelData, aDummy ); + + if( aOut.maSearchName.isEmpty() ) + return false; + + const bool bHaveSubstitute = !uselessmatch( rFontSelData, aOut ); + +#ifdef DEBUG + std::ostringstream oss; + oss << "FcPreMatchSubstitution \"" + << rFontSelData.maTargetName + << "\" bipw=" + << rFontSelData.GetWeight() + << rFontSelData.GetItalic() + << rFontSelData.GetPitch() + << rFontSelData.GetWidthType() + << " -> "; + if( !bHaveSubstitute ) + oss << "no substitute available."; + else + oss << "\"" + << aOut.maSearchName + << "\" bipw=" + << aOut.GetWeight() + << aOut.GetItalic() + << aOut.GetPitch() + << aOut.GetWidthType(); + SAL_INFO("vcl.fonts", oss.str()); +#endif + + if( bHaveSubstitute ) + { + rCachedFontMap.push_front(value_type(rFontSelData, aOut)); + // Fairly arbitrary limit in this case, but I recall measuring max 8 + // fonts as the typical max amount of fonts in medium sized documents, so make it + // a fair chunk larger to accommodate weird documents./ + if (rCachedFontMap.size() > 256) + rCachedFontMap.pop_back(); + rFontSelData = aOut; + } + + return bHaveSubstitute; +} + +bool FcGlyphFallbackSubstitution::FindFontSubstitute(vcl::font::FontSelectPattern& rFontSelData, + LogicalFontInstance* /*pLogicalFont*/, + OUString& rMissingCodes ) const +{ + // We don't actually want to talk to Fontconfig at all for symbol fonts + if( rFontSelData.IsMicrosoftSymbolEncoded() ) + return false; + // OpenSymbol is a unicode font, but it still deserves to be treated as a symbol font + if ( IsOpenSymbol(rFontSelData.maSearchName) ) + return false; + + const vcl::font::FontSelectPattern aOut = GetFcSubstitute( rFontSelData, rMissingCodes ); + // TODO: cache the unicode + srcfont specific result + // FC doing it would be preferable because it knows the invariables + // e.g. FC knows the FC rule that all Arial gets replaced by LiberationSans + // whereas we would have to check for every size or attribute + if( aOut.maSearchName.isEmpty() ) + return false; + + const bool bHaveSubstitute = !uselessmatch( rFontSelData, aOut ); + +#ifdef DEBUG + std::ostringstream oss; + oss << "FcGFSubstitution \"" + << rFontSelData.maTargetName + << "\" bipw=" + << rFontSelData.GetWeight() + << rFontSelData.GetItalic() + << rFontSelData.GetPitch() + << rFontSelData.GetWidthType() + << " -> "; + if( !bHaveSubstitute ) + oss << "no substitute available."; + else + oss << "\"" + << aOut.maSearchName + << "\" bipw=" + << aOut.GetWeight() + << aOut.GetItalic() + << aOut.GetPitch() + << aOut.GetWidthType(); + SAL_INFO("vcl.fonts", oss.str()); +#endif + + if( bHaveSubstitute ) + rFontSelData = aOut; + + return bHaveSubstitute; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/fontmanager/helper.cxx b/vcl/unx/generic/fontmanager/helper.cxx new file mode 100644 index 0000000000..afa6d9cb76 --- /dev/null +++ b/vcl/unx/generic/fontmanager/helper.cxx @@ -0,0 +1,261 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using ::rtl::Bootstrap; + +namespace psp { + +OUString getOfficePath( whichOfficePath ePath ) +{ + static const auto aPaths = [] { + OUString sRoot, sUser, sConfig; + Bootstrap::get("BRAND_BASE_DIR", sRoot); + Bootstrap aBootstrap(sRoot + "/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE("bootstrap")); + aBootstrap.getFrom("UserInstallation", sUser); + aBootstrap.getFrom("CustomDataUrl", sConfig); + OUString aUPath = sUser + "/user/psprint"; + if (sRoot.startsWith("file://")) + { + OUString aSysPath; + if (osl::FileBase::getSystemPathFromFileURL(sRoot, aSysPath) == osl::FileBase::E_None) + sRoot = aSysPath; + } + if (sUser.startsWith("file://")) + { + OUString aSysPath; + if (osl::FileBase::getSystemPathFromFileURL(sUser, aSysPath) == osl::FileBase::E_None) + sUser = aSysPath; + } + if (sConfig.startsWith("file://")) + { + OUString aSysPath; + if (osl::FileBase::getSystemPathFromFileURL(sConfig, aSysPath) == osl::FileBase::E_None) + sConfig = aSysPath; + } + + // ensure user path exists + SAL_INFO("vcl.fonts", "Trying to create: " << aUPath); + osl::Directory::createPath(aUPath); + + return std::make_tuple(sRoot, sUser, sConfig); + }(); + const auto& [aInstallationRootPath, aUserPath, aConfigPath] = aPaths; + + switch( ePath ) + { + case whichOfficePath::ConfigPath: return aConfigPath; + case whichOfficePath::InstallationRootPath: return aInstallationRootPath; + case whichOfficePath::UserPath: return aUserPath; + } + return OUString(); +} + +static OString getEnvironmentPath( const char* pKey ) +{ + OString aPath; + + const char* pValue = getenv( pKey ); + if( pValue && *pValue ) + { + aPath = OString( pValue ); + } + return aPath; +} + +} // namespace psp + +void psp::getPrinterPathList( std::vector< OUString >& rPathList, const char* pSubDir ) +{ + rPathList.clear(); + rtl_TextEncoding aEncoding = osl_getThreadTextEncoding(); + + OUStringBuffer aPathBuffer( 256 ); + + // append net path + aPathBuffer.append( getOfficePath( whichOfficePath::InstallationRootPath ) ); + if( !aPathBuffer.isEmpty() ) + { + aPathBuffer.append( "/" LIBO_SHARE_FOLDER "/psprint" ); + if( pSubDir ) + { + aPathBuffer.append( '/' ); + aPathBuffer.appendAscii( pSubDir ); + } + rPathList.push_back( aPathBuffer.makeStringAndClear() ); + } + // append user path + aPathBuffer.append( getOfficePath( whichOfficePath::UserPath ) ); + if( !aPathBuffer.isEmpty() ) + { + aPathBuffer.append( "/user/psprint" ); + if( pSubDir ) + { + aPathBuffer.append( '/' ); + aPathBuffer.appendAscii( pSubDir ); + } + rPathList.push_back( aPathBuffer.makeStringAndClear() ); + } + + OString aPath( getEnvironmentPath("SAL_PSPRINT") ); + sal_Int32 nIndex = 0; + do + { + OString aDir( aPath.getToken( 0, ':', nIndex ) ); + if( aDir.isEmpty() ) + continue; + + if( pSubDir ) + { + aDir += OString::Concat("/") + pSubDir; + } + struct stat aStat; + if( stat( aDir.getStr(), &aStat ) || ! S_ISDIR( aStat.st_mode ) ) + continue; + + rPathList.push_back( OStringToOUString( aDir, aEncoding ) ); + } while( nIndex != -1 ); + + #ifdef SYSTEM_PPD_DIR + if( pSubDir && rtl_str_compare( pSubDir, PRINTER_PPDDIR ) == 0 ) + { + rPathList.push_back( OStringToOUString( OString( SYSTEM_PPD_DIR ), RTL_TEXTENCODING_UTF8 ) ); + } + #endif + + if( !rPathList.empty() ) + return; + + // last resort: next to program file (mainly for setup) + OUString aExe; + if( osl_getExecutableFile( &aExe.pData ) == osl_Process_E_None ) + { + INetURLObject aDir( aExe ); + aDir.removeSegment(); + aExe = aDir.GetMainURL( INetURLObject::DecodeMechanism::NONE ); + OUString aSysPath; + if( osl_getSystemPathFromFileURL( aExe.pData, &aSysPath.pData ) == osl_File_E_None ) + { + rPathList.push_back( aSysPath ); + } + } +} + +OUString const & psp::getFontPath() +{ + static OUString aPath; + + if (aPath.isEmpty()) + { + OUStringBuffer aPathBuffer( 512 ); + + OUString aConfigPath( getOfficePath( whichOfficePath::ConfigPath ) ); + OUString aInstallationRootPath( getOfficePath( whichOfficePath::InstallationRootPath ) ); + OUString aUserPath( getOfficePath( whichOfficePath::UserPath ) ); + if (!aInstallationRootPath.isEmpty()) + { + // internal font resources, required for normal operation, like OpenSymbol + aPathBuffer.append(aInstallationRootPath + + "/" LIBO_SHARE_RESOURCE_FOLDER "/common/fonts;"); + } + if( !aConfigPath.isEmpty() ) + { + // #i53530# Path from CustomDataUrl will completely + // replace net share and user paths if the path exists + OUString sPath = aConfigPath + "/" LIBO_SHARE_FOLDER "/fonts"; + // check existence of config path + struct stat aStat; + if( 0 != stat( OUStringToOString( sPath, osl_getThreadTextEncoding() ).getStr(), &aStat ) + || ! S_ISDIR( aStat.st_mode ) ) + aConfigPath.clear(); + else + { + aPathBuffer.append(sPath); + } + } + if( aConfigPath.isEmpty() ) + { + if( !aInstallationRootPath.isEmpty() ) + { + aPathBuffer.append( aInstallationRootPath + + "/" LIBO_SHARE_FOLDER "/fonts/truetype;"); + } + if( !aUserPath.isEmpty() ) + { + aPathBuffer.append( aUserPath + "/user/fonts" ); + } + } + + aPath = aPathBuffer.makeStringAndClear(); + SAL_INFO("vcl.fonts", "Initializing font path to: " << aPath); + } + return aPath; +} + +void psp::normPath( OString& rPath ) +{ + char buf[PATH_MAX]; + + // double slashes and slash at end are probably + // removed by realpath anyway, but since this runs + // on many different platforms let's play it safe + OString aPath = rPath.replaceAll("//"_ostr, "/"_ostr); + + if( aPath.endsWith("/") ) + aPath = aPath.copy(0, aPath.getLength()-1); + + if( ( aPath.indexOf("./") != -1 || + aPath.indexOf( '~' ) != -1 ) + && realpath( aPath.getStr(), buf ) ) + { + rPath = buf; + } + else + { + rPath = aPath; + } +} + +void psp::splitPath( OString& rPath, OString& rDir, OString& rBase ) +{ + normPath( rPath ); + sal_Int32 nIndex = rPath.lastIndexOf( '/' ); + if( nIndex > 0 ) + rDir = rPath.copy( 0, nIndex ); + else if( nIndex == 0 ) // root dir + rDir = rPath.copy( 0, 1 ); + if( rPath.getLength() > nIndex+1 ) + rBase = rPath.copy( nIndex+1 ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/gdi/X11CairoSalGraphicsImpl.cxx b/vcl/unx/generic/gdi/X11CairoSalGraphicsImpl.cxx new file mode 100644 index 0000000000..5a751f9ea5 --- /dev/null +++ b/vcl/unx/generic/gdi/X11CairoSalGraphicsImpl.cxx @@ -0,0 +1,243 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "X11CairoSalGraphicsImpl.hxx" + +#include +#include +#include + +X11CairoSalGraphicsImpl::X11CairoSalGraphicsImpl(X11SalGraphics& rParent, CairoCommon& rCairoCommon) + : mrParent(rParent) + , mrCairoCommon(rCairoCommon) +{ +} + +tools::Long X11CairoSalGraphicsImpl::GetGraphicsWidth() const +{ + if (mrParent.m_pFrame) + return mrParent.m_pFrame->maGeometry.width(); + return mrCairoCommon.m_pSurface ? mrCairoCommon.m_aFrameSize.getX() : 0; +} + +void X11CairoSalGraphicsImpl::drawRect(tools::Long nX, tools::Long nY, tools::Long nWidth, + tools::Long nHeight) +{ + mrCairoCommon.drawRect(nX, nY, nWidth, nHeight, getAntiAlias()); +} + +void X11CairoSalGraphicsImpl::drawPolygon(sal_uInt32 nPoints, const Point* pPtAry) +{ + mrCairoCommon.drawPolygon(nPoints, pPtAry, getAntiAlias()); +} + +void X11CairoSalGraphicsImpl::drawPolyPolygon(sal_uInt32 nPoly, const sal_uInt32* pPointCounts, + const Point** pPtAry) +{ + mrCairoCommon.drawPolyPolygon(nPoly, pPointCounts, pPtAry, getAntiAlias()); +} + +void X11CairoSalGraphicsImpl::drawPolyPolygon(const basegfx::B2DHomMatrix& rObjectToDevice, + const basegfx::B2DPolyPolygon& rPolyPolygon, + double fTransparency) +{ + mrCairoCommon.drawPolyPolygon(rObjectToDevice, rPolyPolygon, fTransparency, getAntiAlias()); +} + +void X11CairoSalGraphicsImpl::drawPixel(tools::Long nX, tools::Long nY) +{ + mrCairoCommon.drawPixel(mrCairoCommon.m_oLineColor, nX, nY, getAntiAlias()); +} + +void X11CairoSalGraphicsImpl::drawPixel(tools::Long nX, tools::Long nY, Color aColor) +{ + mrCairoCommon.drawPixel(aColor, nX, nY, getAntiAlias()); +} + +Color X11CairoSalGraphicsImpl::getPixel(tools::Long nX, tools::Long nY) +{ + return CairoCommon::getPixel(mrCairoCommon.m_pSurface, nX, nY); +} + +void X11CairoSalGraphicsImpl::drawLine(tools::Long nX1, tools::Long nY1, tools::Long nX2, + tools::Long nY2) +{ + mrCairoCommon.drawLine(nX1, nY1, nX2, nY2, getAntiAlias()); +} + +void X11CairoSalGraphicsImpl::drawPolyLine(sal_uInt32 nPoints, const Point* pPtAry) +{ + mrCairoCommon.drawPolyLine(nPoints, pPtAry, getAntiAlias()); +} + +bool X11CairoSalGraphicsImpl::drawPolyLine(const basegfx::B2DHomMatrix& rObjectToDevice, + const basegfx::B2DPolygon& rPolyLine, + double fTransparency, double fLineWidth, + const std::vector* pStroke, + basegfx::B2DLineJoin eLineJoin, + css::drawing::LineCap eLineCap, + double fMiterMinimumAngle, bool bPixelSnapHairline) +{ + return mrCairoCommon.drawPolyLine(rObjectToDevice, rPolyLine, fTransparency, fLineWidth, + pStroke, eLineJoin, eLineCap, fMiterMinimumAngle, + bPixelSnapHairline, getAntiAlias()); +} + +bool X11CairoSalGraphicsImpl::drawAlphaRect(tools::Long nX, tools::Long nY, tools::Long nWidth, + tools::Long nHeight, sal_uInt8 nTransparency) +{ + return mrCairoCommon.drawAlphaRect(nX, nY, nWidth, nHeight, nTransparency, getAntiAlias()); +} + +bool X11CairoSalGraphicsImpl::drawGradient(const tools::PolyPolygon& rPolyPolygon, + const Gradient& rGradient) +{ + return mrCairoCommon.drawGradient(rPolyPolygon, rGradient, getAntiAlias()); +} + +bool X11CairoSalGraphicsImpl::implDrawGradient(basegfx::B2DPolyPolygon const& rPolyPolygon, + SalGradient const& rGradient) +{ + return mrCairoCommon.implDrawGradient(rPolyPolygon, rGradient, getAntiAlias()); +} + +void X11CairoSalGraphicsImpl::invert(tools::Long nX, tools::Long nY, tools::Long nWidth, + tools::Long nHeight, SalInvert nFlags) +{ + mrCairoCommon.invert(nX, nY, nWidth, nHeight, nFlags, getAntiAlias()); +} + +void X11CairoSalGraphicsImpl::invert(sal_uInt32 nPoints, const Point* pPtAry, SalInvert nFlags) +{ + mrCairoCommon.invert(nPoints, pPtAry, nFlags, getAntiAlias()); +} + +bool X11CairoSalGraphicsImpl::hasFastDrawTransformedBitmap() const +{ + return CairoCommon::hasFastDrawTransformedBitmap(); +} + +bool X11CairoSalGraphicsImpl::supportsOperation(OutDevSupportType eType) const +{ + return CairoCommon::supportsOperation(eType); +} + +void X11CairoSalGraphicsImpl::copyArea(tools::Long nDestX, tools::Long nDestY, tools::Long nSrcX, + tools::Long nSrcY, tools::Long nSrcWidth, + tools::Long nSrcHeight, bool /*bWindowInvalidate*/) +{ + SalTwoRect aTR(nSrcX, nSrcY, nSrcWidth, nSrcHeight, nDestX, nDestY, nSrcWidth, nSrcHeight); + + cairo_surface_t* source = mrCairoCommon.m_pSurface; + mrCairoCommon.copyBitsCairo(aTR, source, getAntiAlias()); +} + +void X11CairoSalGraphicsImpl::copyBits(const SalTwoRect& rTR, SalGraphics* pSrcGraphics) +{ + cairo_surface_t* source = nullptr; + + if (pSrcGraphics) + { + X11CairoSalGraphicsImpl* pSrc + = static_cast(pSrcGraphics->GetImpl()); + source = pSrc->mrCairoCommon.m_pSurface; + } + else + { + source = mrCairoCommon.m_pSurface; + } + + mrCairoCommon.copyBitsCairo(rTR, source, getAntiAlias()); +} + +void X11CairoSalGraphicsImpl::drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap) +{ + mrCairoCommon.drawBitmap(rPosAry, rSalBitmap, getAntiAlias()); +} + +void X11CairoSalGraphicsImpl::drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap, + const SalBitmap& rTransparentBitmap) +{ + drawAlphaBitmap(rPosAry, rSalBitmap, rTransparentBitmap); +} + +bool X11CairoSalGraphicsImpl::drawAlphaBitmap(const SalTwoRect& rTR, const SalBitmap& rSrcBitmap, + const SalBitmap& rAlphaBmp) +{ + return mrCairoCommon.drawAlphaBitmap(rTR, rSrcBitmap, rAlphaBmp, getAntiAlias()); +} + +void X11CairoSalGraphicsImpl::drawMask(const SalTwoRect& rTR, const SalBitmap& rSalBitmap, + Color nMaskColor) +{ + mrCairoCommon.drawMask(rTR, rSalBitmap, nMaskColor, getAntiAlias()); +} + +std::shared_ptr X11CairoSalGraphicsImpl::getBitmap(tools::Long nX, tools::Long nY, + tools::Long nWidth, + tools::Long nHeight) +{ + return mrCairoCommon.getBitmap(nX, nY, nWidth, nHeight); +} + +void X11CairoSalGraphicsImpl::Init() {} + +void X11CairoSalGraphicsImpl::freeResources() {} + +bool X11CairoSalGraphicsImpl::drawPolyLineBezier(sal_uInt32, const Point*, const PolyFlags*) +{ + return false; +} + +bool X11CairoSalGraphicsImpl::drawPolygonBezier(sal_uInt32, const Point*, const PolyFlags*) +{ + return false; +} + +bool X11CairoSalGraphicsImpl::drawPolyPolygonBezier(sal_uInt32, const sal_uInt32*, + const Point* const*, const PolyFlags* const*) +{ + return false; +} + +bool X11CairoSalGraphicsImpl::drawEPS(tools::Long, tools::Long, tools::Long, tools::Long, void*, + sal_uInt32) +{ + return false; +} + +bool X11CairoSalGraphicsImpl::blendBitmap(const SalTwoRect&, const SalBitmap&) { return false; } + +bool X11CairoSalGraphicsImpl::blendAlphaBitmap(const SalTwoRect&, const SalBitmap&, + const SalBitmap&, const SalBitmap&) +{ + return false; +} + +bool X11CairoSalGraphicsImpl::drawTransformedBitmap(const basegfx::B2DPoint& rNull, + const basegfx::B2DPoint& rX, + const basegfx::B2DPoint& rY, + const SalBitmap& rSourceBitmap, + const SalBitmap* pAlphaBitmap, double fAlpha) +{ + return mrCairoCommon.drawTransformedBitmap(rNull, rX, rY, rSourceBitmap, pAlphaBitmap, fAlpha, + getAntiAlias()); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/gdi/X11CairoSalGraphicsImpl.hxx b/vcl/unx/generic/gdi/X11CairoSalGraphicsImpl.hxx new file mode 100644 index 0000000000..23547daa05 --- /dev/null +++ b/vcl/unx/generic/gdi/X11CairoSalGraphicsImpl.hxx @@ -0,0 +1,188 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include +#include "cairo_xlib_cairo.hxx" + +#include + +class X11CairoSalGraphicsImpl : public SalGraphicsImpl, public X11GraphicsImpl +{ +private: + X11SalGraphics& mrParent; + CairoCommon& mrCairoCommon; + +public: + X11CairoSalGraphicsImpl(X11SalGraphics& rParent, CairoCommon& rCairoCommon); + + void Init() override; + + OUString getRenderBackendName() const override { return "gen"; } + + // get the depth of the device + sal_uInt16 GetBitCount() const override { return mrParent.GetVisual().GetDepth(); } + + // get the width of the device + tools::Long GetGraphicsWidth() const override; + + void ResetClipRegion() override { mrCairoCommon.m_aClipRegion.SetNull(); } + + void setClipRegion(const vcl::Region& i_rClip) override + { + mrCairoCommon.m_aClipRegion = i_rClip; + } + + void SetLineColor() override { mrCairoCommon.m_oLineColor = std::nullopt; } + + void SetLineColor(Color nColor) override { mrCairoCommon.m_oLineColor = nColor; } + + void SetFillColor() override { mrCairoCommon.m_oFillColor = std::nullopt; } + + void SetFillColor(Color nColor) override { mrCairoCommon.m_oFillColor = nColor; } + + void SetXORMode(bool bSet, bool bInvertOnly) override + { + mrCairoCommon.SetXORMode(bSet, bInvertOnly); + } + + void SetROPLineColor(SalROPColor nROPColor) override + { + mrCairoCommon.SetROPLineColor(nROPColor); + } + + void SetROPFillColor(SalROPColor nROPColor) override + { + mrCairoCommon.SetROPFillColor(nROPColor); + } + + void clipRegion(cairo_t* cr) { CairoCommon::clipRegion(cr, mrCairoCommon.m_aClipRegion); } + + void drawPixel(tools::Long nX, tools::Long nY) override; + void drawPixel(tools::Long nX, tools::Long nY, Color nColor) override; + Color getPixel(tools::Long nX, tools::Long nY) override; + + void drawLine(tools::Long nX1, tools::Long nY1, tools::Long nX2, tools::Long nY2) override; + + void drawRect(tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight) override; + + void drawPolygon(sal_uInt32 nPoints, const Point* pPtAry) override; + + void drawPolyPolygon(sal_uInt32 nPoly, const sal_uInt32* pPoints, + const Point** pPtAry) override; + + void drawPolyPolygon(const basegfx::B2DHomMatrix& rObjectToDevice, + const basegfx::B2DPolyPolygon& rPolyPolygon, + double fTransparency) override; + + void drawPolyLine(sal_uInt32 nPoints, const Point* pPtAry) override; + + bool drawPolyLine(const basegfx::B2DHomMatrix& rObjectToDevice, + const basegfx::B2DPolygon& rPolygon, double fTransparency, double fLineWidth, + const std::vector* pStroke, basegfx::B2DLineJoin eLineJoin, + css::drawing::LineCap eLineCap, double fMiterMinimumAngle, + bool bPixelSnapHairline) override; + + /** Render solid rectangle with given transparency + + @param nTransparency + Transparency value (0-255) to use. 0 blits and opaque, 255 a + fully transparent rectangle + */ + bool drawAlphaRect(tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight, + sal_uInt8 nTransparency) override; + + bool drawGradient(const tools::PolyPolygon& rPolygon, const Gradient& rGradient) override; + + bool implDrawGradient(basegfx::B2DPolyPolygon const& rPolyPolygon, + SalGradient const& rGradient) override; + + void invert(tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight, + SalInvert nFlags) override; + + void invert(sal_uInt32 nPoints, const Point* pPtAry, SalInvert nFlags) override; + + // CopyArea --> No RasterOp, but ClipRegion + void copyArea(tools::Long nDestX, tools::Long nDestY, tools::Long nSrcX, tools::Long nSrcY, + tools::Long nSrcWidth, tools::Long nSrcHeight, bool bWindowInvalidate) override; + + // CopyBits and DrawBitmap --> RasterOp and ClipRegion + // CopyBits() --> pSrcGraphics == NULL, then CopyBits on same Graphics + void copyBits(const SalTwoRect& rPosAry, SalGraphics* pSrcGraphics) override; + + void drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap) override; + + void drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap, + const SalBitmap& rMaskBitmap) override; + + /** Render bitmap with alpha channel + + @param rSourceBitmap + Source bitmap to blit + + @param rAlphaBitmap + Alpha channel to use for blitting + + @return true, if the operation succeeded, and false + otherwise. In this case, clients should try to emulate alpha + compositing themselves + */ + bool drawAlphaBitmap(const SalTwoRect&, const SalBitmap& rSourceBitmap, + const SalBitmap& rAlphaBitmap) override; + + void drawMask(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap, + Color nMaskColor) override; + + std::shared_ptr getBitmap(tools::Long nX, tools::Long nY, tools::Long nWidth, + tools::Long nHeight) override; + + bool drawPolyLineBezier(sal_uInt32 nPoints, const Point* pPtAry, + const PolyFlags* pFlgAry) override; + + bool drawPolygonBezier(sal_uInt32 nPoints, const Point* pPtAry, + const PolyFlags* pFlgAry) override; + + bool drawPolyPolygonBezier(sal_uInt32 nPoly, const sal_uInt32* pPoints, + const Point* const* pPtAry, + const PolyFlags* const* pFlgAry) override; + + bool drawEPS(tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight, + void* pPtr, sal_uInt32 nSize) override; + + bool hasFastDrawTransformedBitmap() const override; + + /** draw transformed bitmap (maybe with alpha) where Null, X, Y define the coordinate system */ + bool drawTransformedBitmap(const basegfx::B2DPoint& rNull, const basegfx::B2DPoint& rX, + const basegfx::B2DPoint& rY, const SalBitmap& rSourceBitmap, + const SalBitmap* pAlphaBitmap, double fAlpha) override; + + /** Blend bitmap with color channels */ + bool blendBitmap(const SalTwoRect&, const SalBitmap& rBitmap) override; + + /** Render bitmap by blending using the mask and alpha channel */ + bool blendAlphaBitmap(const SalTwoRect&, const SalBitmap& rSrcBitmap, + const SalBitmap& rMaskBitmap, const SalBitmap& rAlphaBitmap) override; + + bool supportsOperation(OutDevSupportType eType) const override; + + void freeResources() override; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/gdi/cairo_xlib_cairo.cxx b/vcl/unx/generic/gdi/cairo_xlib_cairo.cxx new file mode 100644 index 0000000000..87758f24d9 --- /dev/null +++ b/vcl/unx/generic/gdi/cairo_xlib_cairo.cxx @@ -0,0 +1,277 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include + +#include "cairo_xlib_cairo.hxx" + +#include +#include +#include +#include +#include + +#include +#include + +namespace +{ + Pixmap limitXCreatePixmap(Display *display, Drawable d, unsigned int width, unsigned int height, unsigned int depth) + { + // The X protocol request CreatePixmap puts an upper bound + // of 16 bit to the size. And in practice some drivers + // fall over with values close to the max. + + // see, e.g. moz#424333, fdo#48961, rhbz#1086714 + // we've a duplicate of this in vcl :-( + if (width > SAL_MAX_INT16-10 || height > SAL_MAX_INT16-10) + { + SAL_WARN("canvas", "overlarge pixmap: " << width << " x " << height); + return None; + } + return XCreatePixmap(display, d, width, height, depth); + } +} + +namespace cairo +{ + + X11SysData::X11SysData() : + pDisplay(nullptr), + hDrawable(0), + pVisual(nullptr), + nScreen(0) + {} + + X11SysData::X11SysData( const SystemGraphicsData& pSysDat ) : + pDisplay(static_cast<_XDisplay*>(pSysDat.pDisplay)), + hDrawable(pSysDat.hDrawable), + pVisual(static_cast(pSysDat.pVisual)), + nScreen(pSysDat.nScreen) + {} + + X11SysData::X11SysData( const SystemEnvData& pSysDat, const SalFrame* pReference ) : + pDisplay(static_cast<_XDisplay*>(pSysDat.pDisplay)), + hDrawable(pSysDat.GetWindowHandle(pReference)), + pVisual(static_cast(pSysDat.pVisual)), + nScreen(pSysDat.nScreen) + {} + + X11Pixmap::~X11Pixmap() + { + if( mpDisplay && mhDrawable ) + XFreePixmap( mpDisplay, mhDrawable ); + } + + /** + * Surface::Surface: Create Canvas surface with existing data + * @param pSysData Platform native system environment data (struct SystemEnvData in vcl/inc/sysdata.hxx) + * @param pSurface Cairo surface + * + * pSysData contains the platform native Drawable reference + * This constructor only stores data, it does no processing. + * It is used by e.g. Surface::getSimilar() + * + * Set the mpSurface as pSurface + **/ + X11Surface::X11Surface( const X11SysData& rSysData, + X11PixmapSharedPtr rPixmap, + CairoSurfaceSharedPtr pSurface ) : + maSysData(rSysData), + mpPixmap(std::move(rPixmap)), + mpSurface(std::move(pSurface)) + {} + + /** + * Surface::Surface: Create generic Canvas surface using given Cairo Surface + * + * @param pSurface Cairo Surface + * + * This constructor only stores data, it does no processing. + * It is used with e.g. cairo_image_surface_create_for_data() + * Unlike other constructors, mpSysData is set to NULL + * + * Set the mpSurface as pSurface + **/ + X11Surface::X11Surface( CairoSurfaceSharedPtr pSurface ) : + maSysData(), + mpSurface(std::move(pSurface)) + {} + + /** + * Surface::Surface: Create Canvas surface from Window reference. + * @param pSysData Platform native system environment data (struct SystemEnvData in vcl/inc/sysdata.hxx) + * @param x horizontal location of the new surface + * @param y vertical location of the new surface + * @param width width of the new surface + * @param height height of the new surface + * + * pSysData contains the platform native Window reference. + * + * pSysData is used to create a surface on the Window + * + * Set the mpSurface to the new surface or NULL + **/ + X11Surface::X11Surface( const X11SysData& rSysData, int x, int y, int width, int height ) : + maSysData(rSysData), + mpSurface( + cairo_xlib_surface_create( rSysData.pDisplay, + rSysData.hDrawable, + rSysData.pVisual, + width + x, height + y ), + &cairo_surface_destroy) + { + cairo_surface_set_device_offset(mpSurface.get(), x, y ); + } + + /** + * Surface::Surface: Create platform native Canvas surface from BitmapSystemData + * @param pSysData Platform native system environment data (struct SystemEnvData in vcl/inc/sysdata.hxx) + * @param pBmpData Platform native image data (struct BitmapSystemData in vcl/inc/bitmap.hxx) + * @param width width of the new surface + * @param height height of the new surface + * + * The pBmpData provides the imagedata that the created surface should contain. + * + * Set the mpSurface to the new surface or NULL + **/ + X11Surface::X11Surface( const X11SysData& rSysData, + const BitmapSystemData& rData ) : + maSysData( rSysData ), + mpSurface( + cairo_xlib_surface_create( rSysData.pDisplay, + reinterpret_cast(rData.aPixmap), + rSysData.pVisual, + rData.mnWidth, rData.mnHeight ), + &cairo_surface_destroy) + { + } + + /** + * Surface::getCairo: Create Cairo (drawing object) for the Canvas surface + * + * @return new Cairo or NULL + **/ + CairoSharedPtr X11Surface::getCairo() const + { + return CairoSharedPtr( cairo_create(mpSurface.get()), + &cairo_destroy ); + } + + /** + * Surface::getSimilar: Create new similar Canvas surface + * @param cairo_content_type format of the new surface (cairo_content_t from cairo/src/cairo.h) + * @param width width of the new surface + * @param height height of the new surface + * + * Creates a new Canvas surface. This normally creates platform native surface, even though + * generic function is used. + * + * Cairo surface from cairo_content_type (cairo_content_t) + * + * @return new surface or NULL + **/ + SurfaceSharedPtr X11Surface::getSimilar(int cairo_content_type, int width, int height ) const + { + if( maSysData.pDisplay && maSysData.hDrawable ) + { + XRenderPictFormat* pFormat; + int nFormat; + + switch (cairo_content_type) + { + case CAIRO_CONTENT_ALPHA: + nFormat = PictStandardA8; + break; + case CAIRO_CONTENT_COLOR: + nFormat = PictStandardRGB24; + break; + case CAIRO_CONTENT_COLOR_ALPHA: + default: + nFormat = PictStandardARGB32; + break; + } + + pFormat = XRenderFindStandardFormat( maSysData.pDisplay, nFormat ); + Pixmap hPixmap = limitXCreatePixmap( maSysData.pDisplay, maSysData.hDrawable, + width > 0 ? width : 1, height > 0 ? height : 1, + pFormat->depth ); + + return SurfaceSharedPtr( + new X11Surface( maSysData, + std::make_shared(hPixmap, maSysData.pDisplay), + CairoSurfaceSharedPtr( + cairo_xlib_surface_create_with_xrender_format( + maSysData.pDisplay, + hPixmap, + ScreenOfDisplay(maSysData.pDisplay, maSysData.nScreen), + pFormat, width, height ), + &cairo_surface_destroy) )); + } + else + return SurfaceSharedPtr( + new X11Surface( maSysData, + X11PixmapSharedPtr(), + CairoSurfaceSharedPtr( + cairo_surface_create_similar( mpSurface.get(), + static_cast(cairo_content_type), width, height ), + &cairo_surface_destroy ))); + } + + VclPtr X11Surface::createVirtualDevice() const + { + SystemGraphicsData aSystemGraphicsData; + + cairo_surface_t* pSurface = mpSurface.get(); + + aSystemGraphicsData.nSize = sizeof(SystemGraphicsData); + aSystemGraphicsData.hDrawable = mpPixmap ? mpPixmap->mhDrawable : maSysData.hDrawable; + aSystemGraphicsData.pSurface = pSurface; + + int width = cairo_xlib_surface_get_width(pSurface); + int height = cairo_xlib_surface_get_height(pSurface); + + return VclPtr::Create(aSystemGraphicsData, + Size(width, height), + DeviceFormat::WITHOUT_ALPHA); + } + + /** + * Surface::Resize: Resizes the Canvas surface. + * @param width new width of the surface + * @param height new height of the surface + * + * Only used on X11. + * + * @return The new surface or NULL + **/ + bool X11Surface::Resize(int width, int height) + { + cairo_xlib_surface_set_size(mpSurface.get(), width, height); + return true; + } + + void X11Surface::flush() const + { + XSync( maSysData.pDisplay, false ); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/gdi/cairo_xlib_cairo.hxx b/vcl/unx/generic/gdi/cairo_xlib_cairo.hxx new file mode 100644 index 0000000000..f0b47a3744 --- /dev/null +++ b/vcl/unx/generic/gdi/cairo_xlib_cairo.hxx @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include +#include +#include + +struct BitmapSystemData; +class SalFrame; +struct SystemEnvData; +struct SystemGraphicsData; + +namespace cairo { + + /// Holds all X11-output relevant data + struct X11SysData + { + X11SysData(); + explicit X11SysData( const SystemGraphicsData& ); + explicit X11SysData( const SystemEnvData&, const SalFrame* pReference ); + + _XDisplay* pDisplay; // the relevant display connection + Drawable hDrawable; // a drawable + Visual* pVisual; // the visual in use + int nScreen; // the current screen of the drawable + }; + + /// RAII wrapper for a pixmap + struct X11Pixmap + { + _XDisplay* mpDisplay; // the relevant display connection + Pixmap mhDrawable; // a drawable + + X11Pixmap( Pixmap hDrawable, _XDisplay* pDisplay ) : + mpDisplay(pDisplay), + mhDrawable(hDrawable) + {} + + ~X11Pixmap(); + }; + + typedef std::shared_ptr X11PixmapSharedPtr; + + class X11Surface : public Surface + { + const X11SysData maSysData; + X11PixmapSharedPtr mpPixmap; + CairoSurfaceSharedPtr mpSurface; + + X11Surface( const X11SysData& rSysData, X11PixmapSharedPtr aPixmap, CairoSurfaceSharedPtr pSurface ); + + public: + /// takes over ownership of passed cairo_surface + explicit X11Surface( CairoSurfaceSharedPtr pSurface ); + /// create surface on subarea of given drawable + X11Surface( const X11SysData& rSysData, int x, int y, int width, int height ); + /// create surface for given bitmap data + X11Surface( const X11SysData& rSysData, const BitmapSystemData& rBmpData ); + + // Surface interface + virtual CairoSharedPtr getCairo() const override; + virtual CairoSurfaceSharedPtr getCairoSurface() const override { return mpSurface; } + virtual SurfaceSharedPtr getSimilar(int cairo_content_type, int width, int height) const override; + + virtual VclPtr createVirtualDevice() const override; + + virtual bool Resize( int width, int height ) override; + + virtual void flush() const override; + + const X11PixmapSharedPtr& getPixmap() const { return mpPixmap; } + }; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/gdi/cairotextrender.cxx b/vcl/unx/generic/gdi/cairotextrender.cxx new file mode 100644 index 0000000000..7e7ce9ca70 --- /dev/null +++ b/vcl/unx/generic/gdi/cairotextrender.cxx @@ -0,0 +1,526 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#if defined(CAIRO_HAS_SVG_SURFACE) +#include +#elif defined(CAIRO_HAS_PDF_SURFACE) +#include +#endif + +#include + +namespace { + +typedef struct FT_FaceRec_* FT_Face; + +class CairoFontsCache +{ +public: + struct CacheId + { + FT_Face maFace; + const FontConfigFontOptions *mpOptions; + bool mbEmbolden; + bool mbVerticalMetrics; + bool operator ==(const CacheId& rOther) const + { + return maFace == rOther.maFace && + mpOptions == rOther.mpOptions && + mbEmbolden == rOther.mbEmbolden && + mbVerticalMetrics == rOther.mbVerticalMetrics; + } + }; + +private: + typedef std::deque< std::pair > LRUFonts; + static LRUFonts maLRUFonts; +public: + CairoFontsCache() = delete; + + static void CacheFont(cairo_font_face_t* pFont, const CacheId &rId); + static cairo_font_face_t* FindCachedFont(const CacheId &rId); +}; + +CairoFontsCache::LRUFonts CairoFontsCache::maLRUFonts; + +void CairoFontsCache::CacheFont(cairo_font_face_t* pFont, const CairoFontsCache::CacheId &rId) +{ + maLRUFonts.push_front( std::pair(pFont, rId) ); + if (maLRUFonts.size() > 8) + { + cairo_font_face_destroy(maLRUFonts.back().first); + maLRUFonts.pop_back(); + } +} + +cairo_font_face_t* CairoFontsCache::FindCachedFont(const CairoFontsCache::CacheId &rId) +{ + auto aI = std::find_if(maLRUFonts.begin(), maLRUFonts.end(), + [&rId](const LRUFonts::value_type& rFont) { return rFont.second == rId; }); + if (aI != maLRUFonts.end()) + return aI->first; + return nullptr; +} + +} + +namespace +{ + bool hasRotation(int nRotation) + { + return nRotation != 0; + } + + double toRadian(Degree10 nDegree10th) + { + return toRadians(3600_deg10 - nDegree10th); + } + + cairo_t* syncCairoContext(cairo_t* cr) + { + //rhbz#1283420 tdf#117413 bodge to force a read from the underlying surface which has + //the side effect of making the mysterious xrender related problem go away + cairo_surface_t *target = cairo_get_target(cr); + if (cairo_surface_get_type(target) == CAIRO_SURFACE_TYPE_XLIB) + { + cairo_surface_t *throw_away = cairo_surface_create_similar(target, cairo_surface_get_content(target), 1, 1); + cairo_t *force_read_cr = cairo_create(throw_away); + cairo_set_source_surface(force_read_cr, target, 0, 0); + cairo_paint(force_read_cr); + cairo_destroy(force_read_cr); + cairo_surface_destroy(throw_away); + } + return cr; + } +} + +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) +extern "C" +{ + __attribute__((weak)) void __lsan_disable(); + __attribute__((weak)) void __lsan_enable(); +} +#endif + +namespace { + struct CairoFontOptions + { + // https://gitlab.freedesktop.org/cairo/cairo/-/merge_requests/235 + // I don't want to have CAIRO_ROUND_GLYPH_POS_ON set in the cairo + // surfaces font_options, but that's private, so tricky to achieve + cairo_font_options_t* mpRoundGlyphPosOffOptions; + + CairoFontOptions() + { + // https://gitlab.freedesktop.org/cairo/cairo/-/merge_requests/235 + // I don't want to have CAIRO_ROUND_GLYPH_POS_ON set in the cairo surfaces + // font_options when trying subpixel rendering, but that's a private + // feature of cairo_font_options_t, so tricky to achieve. Hack this by + // getting the font options of a backend known to set this private feature + // to CAIRO_ROUND_GLYPH_POS_OFF and then set to defaults the public + // features and the result can be merged with new font options to set + // CAIRO_ROUND_GLYPH_POS_OFF in those + mpRoundGlyphPosOffOptions = cairo_font_options_create(); +#if defined(CAIRO_HAS_SVG_SURFACE) + // svg, pdf and ps backends have CAIRO_ROUND_GLYPH_POS_OFF by default + cairo_surface_t* hack = cairo_svg_surface_create(nullptr, 1, 1); +#elif defined(CAIRO_HAS_PDF_SURFACE) + cairo_surface_t* hack = cairo_pdf_surface_create(nullptr, 1, 1); +#endif + cairo_surface_get_font_options(hack, mpRoundGlyphPosOffOptions); + cairo_surface_destroy(hack); + cairo_font_options_set_antialias(mpRoundGlyphPosOffOptions, CAIRO_ANTIALIAS_DEFAULT); + cairo_font_options_set_subpixel_order(mpRoundGlyphPosOffOptions, CAIRO_SUBPIXEL_ORDER_DEFAULT); + cairo_font_options_set_hint_style(mpRoundGlyphPosOffOptions, CAIRO_HINT_STYLE_DEFAULT); + cairo_font_options_set_hint_metrics(mpRoundGlyphPosOffOptions, CAIRO_HINT_METRICS_DEFAULT); + } + ~CairoFontOptions() + { + cairo_font_options_destroy(mpRoundGlyphPosOffOptions); + } + static const cairo_font_options_t *get() + { + static CairoFontOptions opts; + return opts.mpRoundGlyphPosOffOptions; + } + }; +} + +CairoTextRender::CairoTextRender(CairoCommon& rCairoCommon) + : mrCairoCommon(rCairoCommon) +{ +} + +CairoTextRender::~CairoTextRender() +{ +} + +static void ApplyFont(cairo_t* cr, const CairoFontsCache::CacheId& rId, double nWidth, double nHeight, int nGlyphRotation, + const GenericSalLayout& rLayout) +{ + cairo_font_face_t* font_face = CairoFontsCache::FindCachedFont(rId); + if (!font_face) + { + const FontConfigFontOptions *pOptions = rId.mpOptions; + FcPattern *pPattern = pOptions->GetPattern(); + font_face = cairo_ft_font_face_create_for_pattern(pPattern); + CairoFontsCache::CacheFont(font_face, rId); + } + cairo_set_font_face(cr, font_face); + + cairo_set_font_size(cr, nHeight); + + cairo_matrix_t m; + cairo_matrix_init_identity(&m); + + if (rLayout.GetOrientation()) + cairo_matrix_rotate(&m, toRadian(rLayout.GetOrientation())); + + cairo_matrix_scale(&m, nWidth, nHeight); + + if (nGlyphRotation) + cairo_matrix_rotate(&m, toRadian(Degree10(nGlyphRotation * 900))); + + const LogicalFontInstance& rInstance = rLayout.GetFont(); + if (rInstance.NeedsArtificialItalic()) + { + cairo_matrix_t shear; + cairo_matrix_init_identity(&shear); + shear.xy = -shear.xx * ARTIFICIAL_ITALIC_SKEW; + cairo_matrix_multiply(&m, &shear, &m); + } + + cairo_set_font_matrix(cr, &m); +} + +static CairoFontsCache::CacheId makeCacheId(const GenericSalLayout& rLayout) +{ + const FreetypeFontInstance& rInstance = static_cast(rLayout.GetFont()); + const FreetypeFont& rFont = rInstance.GetFreetypeFont(); + + FT_Face aFace = rFont.GetFtFace(); + CairoFontsCache::CacheId aId; + aId.maFace = aFace; + aId.mpOptions = rFont.GetFontOptions(); + aId.mbEmbolden = rInstance.NeedsArtificialBold(); + aId.mbVerticalMetrics = false; + + return aId; +} + +void CairoTextRender::DrawTextLayout(const GenericSalLayout& rLayout, const SalGraphics& rGraphics) +{ + const LogicalFontInstance& rInstance = rLayout.GetFont(); + + const bool bSubpixelPositioning = rLayout.GetSubpixelPositioning(); + + /* + * It might be ideal to cache surface and cairo context between calls and + * only destroy it when the drawable changes, but to do that we need to at + * least change the SalFrame etc impls to dtor the SalGraphics *before* the + * destruction of the windows they reference + */ + cairo_t *cr = syncCairoContext(getCairoContext()); + if (!cr) + { + SAL_WARN("vcl", "no cairo context for text"); + return; + } + comphelper::ScopeGuard releaseContext([this, cr]() { releaseCairoContext(cr); }); + + std::vector cairo_glyphs; + std::vector glyph_extrarotation; + cairo_glyphs.reserve( 256 ); + + double nSnapToSubPixelDiff = 0.0; + double nXScale, nYScale; + dl_cairo_surface_get_device_scale(cairo_get_target(cr), &nXScale, &nYScale); + + basegfx::B2DPoint aPos; + const GlyphItem* pGlyph; + const GlyphItem* pPrevGlyph = nullptr; + int nStart = 0; + while (rLayout.GetNextGlyph(&pGlyph, aPos, nStart)) + { + cairo_glyph_t aGlyph; + aGlyph.index = pGlyph->glyphId(); + aGlyph.x = aPos.getX(); + aGlyph.y = aPos.getY(); + + const bool bVertical = pGlyph->IsVertical(); + glyph_extrarotation.push_back(bVertical ? 1 : 0); + + if (bSubpixelPositioning) + { + // tdf#150507 like skia, even when subpixel rendering pixel, snap y + if (!bVertical) + aGlyph.y = std::floor(aGlyph.y + 0.5); + else + aGlyph.x = std::floor(aGlyph.x + 0.5); + + // tdf#152094 snap to 1/4 of a pixel after a run of whitespace, + // probably a little dubious, but maybe worth a shot for lodpi + double& rGlyphDimension = !bVertical ? aGlyph.x : aGlyph.y; + const int nSubPixels = 4 * (!bVertical ? nXScale : nYScale); + if (pGlyph->IsSpacing()) + nSnapToSubPixelDiff = 0; + else if (pPrevGlyph && pPrevGlyph->IsSpacing()) + { + double nSnapToSubPixel = std::floor(rGlyphDimension * nSubPixels) / nSubPixels; + nSnapToSubPixelDiff = rGlyphDimension - nSnapToSubPixel; + rGlyphDimension = nSnapToSubPixel; + } + else + rGlyphDimension -= nSnapToSubPixelDiff; + + pPrevGlyph = pGlyph; + } + + cairo_glyphs.push_back(aGlyph); + } + + const size_t nGlyphs = cairo_glyphs.size(); + if (!nGlyphs) + return; + + const vcl::font::FontSelectPattern& rFSD = rInstance.GetFontSelectPattern(); + double nHeight = rFSD.mnHeight; + double nWidth = rFSD.mnWidth ? rFSD.mnWidth : nHeight; + if (nWidth == 0 || nHeight == 0) + return; + + if (nHeight > SAL_MAX_UINT16) + { + // as seen with freetype 2.11.0, so cairo surface status is "fail" + // ("error occurred in libfreetype") and no further operations are + // executed, so this error then leads to later leaks + SAL_WARN("vcl", "rendering text would fail with height: " << nHeight); + return; + } + +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) + if (nHeight > 8000) + { + SAL_WARN("vcl", "rendering text would use > 2G Memory: " << nHeight); + return; + } + + if (nWidth > 2000) + { + SAL_WARN("vcl", "rendering text would use > 2G Memory: " << nWidth); + return; + } +#endif + + clipRegion(cr); + + cairo_set_source_rgba(cr, + mnTextColor.GetRed()/255.0, + mnTextColor.GetGreen()/255.0, + mnTextColor.GetBlue()/255.0, + mnTextColor.GetAlpha()/255.0); + + int nRatio = nWidth * 10 / nHeight; + + // tdf#132112 excessive stretch of underbrace and overbrace can trigger freetype into an error, which propagates to cairo + // and once a cairo surface is in an error state, that cannot be cleared and all subsequent drawing fails, so bodge that + // with a high degree of stretch we draw the brace without stretch to a temp surface and stretch that to give a far + // poorer visual result, but one that can be rendered. + if (nGlyphs == 1 && nRatio > 100 && (cairo_glyphs[0].index == 974 || cairo_glyphs[0].index == 975) && + rFSD.maTargetName == "OpenSymbol" && !glyph_extrarotation.back() && !rLayout.GetOrientation()) + { + CairoFontsCache::CacheId aId = makeCacheId(rLayout); + + ApplyFont(cr, aId, nWidth, nHeight, 0, rLayout); + cairo_text_extents_t stretched_extents; + cairo_glyph_extents(cr, cairo_glyphs.data(), nGlyphs, &stretched_extents); + + ApplyFont(cr, aId, nHeight, nHeight, 0, rLayout); + cairo_text_extents_t unstretched_extents; + cairo_glyph_extents(cr, cairo_glyphs.data(), nGlyphs, &unstretched_extents); + + cairo_surface_t *target = cairo_get_target(cr); + cairo_surface_t *temp_surface = cairo_surface_create_similar(target, cairo_surface_get_content(target), + unstretched_extents.width, unstretched_extents.height); + cairo_t *temp_cr = cairo_create(temp_surface); + cairo_glyph_t glyph; + glyph.x = -unstretched_extents.x_bearing; + glyph.y = -unstretched_extents.y_bearing; + glyph.index = cairo_glyphs[0].index; + + ApplyFont(temp_cr, aId, nHeight, nHeight, 0, rLayout); + + cairo_set_source_rgb(temp_cr, + mnTextColor.GetRed()/255.0, + mnTextColor.GetGreen()/255.0, + mnTextColor.GetBlue()/255.0); + + cairo_show_glyphs(temp_cr, &glyph, 1); + cairo_destroy(temp_cr); + + cairo_set_source_surface(cr, temp_surface, cairo_glyphs[0].x, cairo_glyphs[0].y + stretched_extents.y_bearing); + + cairo_pattern_t* sourcepattern = cairo_get_source(cr); + cairo_matrix_t matrix; + cairo_pattern_get_matrix(sourcepattern, &matrix); + cairo_matrix_scale(&matrix, unstretched_extents.width / stretched_extents.width, 1); + cairo_pattern_set_matrix(sourcepattern, &matrix); + + cairo_rectangle(cr, cairo_glyphs[0].x, cairo_glyphs[0].y + stretched_extents.y_bearing, stretched_extents.width, stretched_extents.height); + cairo_fill(cr); + + cairo_surface_destroy(temp_surface); + + return; + } + + if (nRatio >= 5120) + { + // as seen with freetype 2.12.1, so cairo surface status is "fail" + SAL_WARN("vcl", "rendering text would fail with stretch of: " << nRatio / 10.0); + return; + } + +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) + if (__lsan_disable) + __lsan_disable(); +#endif + + const StyleSettings& rStyleSettings = Application::GetSettings().GetStyleSettings(); + const bool bDisableAA = !rStyleSettings.GetUseFontAAFromSystem() && !rGraphics.getAntiAlias(); + static bool bAllowDefaultHinting = getenv("SAL_ALLOW_DEFAULT_HINTING") != nullptr; + + const cairo_font_options_t* pFontOptions = GetSalInstance()->GetCairoFontOptions(); + if (pFontOptions || bDisableAA || bSubpixelPositioning) + { + cairo_hint_style_t eHintStyle = pFontOptions ? cairo_font_options_get_hint_style(pFontOptions) : CAIRO_HINT_STYLE_DEFAULT; + bool bAllowedHintStyle = !bSubpixelPositioning || bAllowDefaultHinting || (eHintStyle == CAIRO_HINT_STYLE_NONE || eHintStyle == CAIRO_HINT_STYLE_SLIGHT); + + if (bDisableAA || !bAllowedHintStyle || bSubpixelPositioning) + { + // Disable font AA in case global AA setting is supposed to affect + // font rendering (not the default) and AA is disabled. + cairo_font_options_t* pOptions = pFontOptions ? cairo_font_options_copy(pFontOptions) : cairo_font_options_create(); + + if (bDisableAA) + cairo_font_options_set_antialias(pOptions, CAIRO_ANTIALIAS_NONE); + if (!bAllowedHintStyle) + cairo_font_options_set_hint_style(pOptions, CAIRO_HINT_STYLE_SLIGHT); + if (bSubpixelPositioning) + { + // Disable private CAIRO_ROUND_GLYPH_POS_ON by merging with + // font options known to have CAIRO_ROUND_GLYPH_POS_OFF + cairo_font_options_merge(pOptions, CairoFontOptions::get()); + + // a) tdf#153699 skip this with cairo 1.17.8 as it has a problem + // See: https://gitlab.freedesktop.org/cairo/cairo/-/issues/643 + // b) tdf#152675 a similar report for cairo: 1.16.0-4ubuntu1, + // assume that everything <= 1.17.8 is unsafe to disable this + if (cairo_version() > CAIRO_VERSION_ENCODE(1, 17, 8)) + cairo_font_options_set_hint_metrics(pOptions, CAIRO_HINT_METRICS_OFF); + } + cairo_set_font_options(cr, pOptions); + cairo_font_options_destroy(pOptions); + } + else if (pFontOptions) + cairo_set_font_options(cr, pFontOptions); + } + + CairoFontsCache::CacheId aId = makeCacheId(rLayout); + + std::vector::const_iterator aEnd = glyph_extrarotation.end(); + std::vector::const_iterator aStart = glyph_extrarotation.begin(); + std::vector::const_iterator aI = aStart; + while (aI != aEnd) + { + int nGlyphRotation = *aI; + + std::vector::const_iterator aNext = nGlyphRotation?(aI+1):std::find_if(aI+1, aEnd, hasRotation); + + size_t nStartIndex = std::distance(aStart, aI); + size_t nLen = std::distance(aI, aNext); + + aId.mbVerticalMetrics = nGlyphRotation != 0.0; + + ApplyFont(cr, aId, nWidth, nHeight, nGlyphRotation, rLayout); + + cairo_show_glyphs(cr, &cairo_glyphs[nStartIndex], nLen); + if (cairo_status(cr) != CAIRO_STATUS_SUCCESS) + { + SAL_WARN("vcl", "rendering text failed with stretch ratio of: " << nRatio << ", " << cairo_status_to_string(cairo_status(cr))); + } + +#if OSL_DEBUG_LEVEL > 2 + //draw origin + cairo_save (cr); + cairo_rectangle (cr, cairo_glyphs[nStartIndex].x, cairo_glyphs[nStartIndex].y, 5, 5); + cairo_set_source_rgba (cr, 1, 0, 0, 0.80); + cairo_fill (cr); + cairo_restore (cr); +#endif + + aI = aNext; + } + +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) + if (__lsan_enable) + __lsan_enable(); +#endif +} + +cairo_t* CairoTextRender::getCairoContext() +{ + // Note that cairo_set_antialias (bAntiAlias property) doesn't affect cairo + // text rendering. That's affected by cairo_font_options_set_antialias instead. + return mrCairoCommon.getCairoContext(/*bXorModeAllowed*/false, /*bAntiAlias*/true); +} + +void CairoTextRender::clipRegion(cairo_t* cr) +{ + mrCairoCommon.clipRegion(cr); +} + +void CairoTextRender::releaseCairoContext(cairo_t* cr) +{ + mrCairoCommon.releaseCairoContext(cr, /*bXorModeAllowed*/false, basegfx::B2DRange()); +} + +void FontConfigFontOptions::cairo_font_options_substitute(FcPattern* pPattern) +{ + const cairo_font_options_t* pFontOptions = GetSalInstance()->GetCairoFontOptions(); + if( !pFontOptions ) + return; + cairo_ft_font_options_substitute(pFontOptions, pPattern); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/gdi/font.cxx b/vcl/unx/generic/gdi/font.cxx new file mode 100644 index 0000000000..19887a9af2 --- /dev/null +++ b/vcl/unx/generic/gdi/font.cxx @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include + +#include +#include +#include + +void X11SalGraphics::DrawTextLayout(const GenericSalLayout& rLayout) +{ + mxTextRenderImpl->DrawTextLayout(rLayout, *this); +} + +FontCharMapRef X11SalGraphics::GetFontCharMap() const +{ + return mxTextRenderImpl->GetFontCharMap(); +} + +bool X11SalGraphics::GetFontCapabilities(vcl::FontCapabilities &rGetImplFontCapabilities) const +{ + return mxTextRenderImpl->GetFontCapabilities(rGetImplFontCapabilities); +} + +// SalGraphics +void X11SalGraphics::SetFont(LogicalFontInstance* pEntry, int nFallbackLevel) +{ + mxTextRenderImpl->SetFont(pEntry, nFallbackLevel); +} + +void +X11SalGraphics::SetTextColor( Color nColor ) +{ + mxTextRenderImpl->SetTextColor(nColor); +} + +bool X11SalGraphics::AddTempDevFont( vcl::font::PhysicalFontCollection* pFontCollection, + const OUString& rFileURL, + const OUString& rFontName ) +{ + return mxTextRenderImpl->AddTempDevFont(pFontCollection, rFileURL, rFontName); +} + +void X11SalGraphics::ClearDevFontCache() +{ + mxTextRenderImpl->ClearDevFontCache(); +} + +void X11SalGraphics::GetDevFontList( vcl::font::PhysicalFontCollection* pFontCollection ) +{ + mxTextRenderImpl->GetDevFontList(pFontCollection); +} + +void +X11SalGraphics::GetFontMetric( FontMetricDataRef &rxFontMetric, int nFallbackLevel ) +{ + mxTextRenderImpl->GetFontMetric(rxFontMetric, nFallbackLevel); +} + +std::unique_ptr X11SalGraphics::GetTextLayout(int nFallbackLevel) +{ + return mxTextRenderImpl->GetTextLayout(nFallbackLevel); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/gdi/freetypetextrender.cxx b/vcl/unx/generic/gdi/freetypetextrender.cxx new file mode 100644 index 0000000000..b524a45ee7 --- /dev/null +++ b/vcl/unx/generic/gdi/freetypetextrender.cxx @@ -0,0 +1,185 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +FreeTypeTextRenderImpl::FreeTypeTextRenderImpl() + : mnTextColor(Color(0x00, 0x00, 0x00)) //black +{ +} + +FreeTypeTextRenderImpl::~FreeTypeTextRenderImpl() +{ + ReleaseFonts(); +} + +void FreeTypeTextRenderImpl::SetFont(LogicalFontInstance *pEntry, int nFallbackLevel) +{ + // release all no longer needed font resources + for( int i = nFallbackLevel; i < MAX_FALLBACK; ++i ) + { + // old server side font is no longer referenced + mpFreetypeFont[i] = nullptr; + } + + // return early if there is no new font + if( !pEntry ) + return; + + FreetypeFontInstance* pFreetypeFont = static_cast(pEntry); + mpFreetypeFont[ nFallbackLevel ] = pFreetypeFont; + + // ignore fonts with e.g. corrupted font files + if (!mpFreetypeFont[nFallbackLevel]->GetFreetypeFont().TestFont()) + mpFreetypeFont[nFallbackLevel] = nullptr; +} + +FontCharMapRef FreeTypeTextRenderImpl::GetFontCharMap() const +{ + if (!mpFreetypeFont[0]) + return nullptr; + return mpFreetypeFont[0]->GetFontFace()->GetFontCharMap(); +} + +bool FreeTypeTextRenderImpl::GetFontCapabilities(vcl::FontCapabilities &rGetImplFontCapabilities) const +{ + if (!mpFreetypeFont[0]) + return false; + return mpFreetypeFont[0]->GetFontFace()->GetFontCapabilities(rGetImplFontCapabilities); +} + +// SalGraphics +void +FreeTypeTextRenderImpl::SetTextColor( Color nColor ) +{ + if( mnTextColor != nColor ) + { + mnTextColor = nColor; + } +} + +bool FreeTypeTextRenderImpl::AddTempDevFont(vcl::font::PhysicalFontCollection* pFontCollection, + const OUString& rFileURL, const OUString& rFontName) +{ + // inform PSP font manager + psp::PrintFontManager& rMgr = psp::PrintFontManager::get(); + std::vector aFontIds = rMgr.addFontFile(rFileURL); + if (aFontIds.empty()) + return false; + + FreetypeManager& rFreetypeManager = FreetypeManager::get(); + for (auto const& nFontId : aFontIds) + { + // prepare font data + auto const* pFont = rMgr.getFont(nFontId); + if (!pFont) + continue; + + // inform glyph cache of new font + FontAttributes aDFA = pFont->m_aFontAttributes; + aDFA.IncreaseQualityBy(5800); + if (!rFontName.isEmpty()) + aDFA.SetFamilyName(rFontName); + + int nFaceNum = rMgr.getFontFaceNumber(nFontId); + int nVariantNum = rMgr.getFontFaceVariation(nFontId); + + const OString& rFileName = rMgr.getFontFileSysPath(nFontId); + rFreetypeManager.AddFontFile(rFileName, nFaceNum, nVariantNum, nFontId, aDFA); + } + + // announce new font to device's font list + rFreetypeManager.AnnounceFonts(pFontCollection); + return true; +} + +void FreeTypeTextRenderImpl::ClearDevFontCache() +{ + FreetypeManager::get().ClearFontCache(); +} + +void FreeTypeTextRenderImpl::GetDevFontList(vcl::font::PhysicalFontCollection* pFontCollection) +{ + // prepare the FreetypeManager using psprint's font infos + FreetypeManager& rFreetypeManager = FreetypeManager::get(); + + psp::PrintFontManager& rMgr = psp::PrintFontManager::get(); + ::std::vector aList; + rMgr.getFontList(aList); + for (auto const& nFontId : aList) + { + auto const* pFont = rMgr.getFont(nFontId); + if (!pFont) + continue; + + // normalize face number to the FreetypeManager + int nFaceNum = rMgr.getFontFaceNumber(nFontId); + int nVariantNum = rMgr.getFontFaceVariation(nFontId); + + // inform FreetypeManager about this font provided by the PsPrint subsystem + FontAttributes aDFA = pFont->m_aFontAttributes; + aDFA.IncreaseQualityBy(4096); + const OString& rFileName = rMgr.getFontFileSysPath(nFontId); + rFreetypeManager.AddFontFile(rFileName, nFaceNum, nVariantNum, nFontId, aDFA); + } + + // announce glyphcache fonts + rFreetypeManager.AnnounceFonts(pFontCollection); + + // register platform specific font substitutions if available + SalGenericInstance::RegisterFontSubstitutors(pFontCollection); +} + +void FreeTypeTextRenderImpl::GetFontMetric( FontMetricDataRef& rxFontMetric, int nFallbackLevel ) +{ + if( nFallbackLevel >= MAX_FALLBACK ) + return; + + if (mpFreetypeFont[nFallbackLevel]) + mpFreetypeFont[nFallbackLevel]->GetFreetypeFont().GetFontMetric(rxFontMetric); +} + +std::unique_ptr FreeTypeTextRenderImpl::GetTextLayout(int nFallbackLevel) +{ + assert(mpFreetypeFont[nFallbackLevel]); + if (!mpFreetypeFont[nFallbackLevel]) + return nullptr; + return std::make_unique(*mpFreetypeFont[nFallbackLevel]); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/gdi/salgdi.cxx b/vcl/unx/generic/gdi/salgdi.cxx new file mode 100644 index 0000000000..f296e3cf34 --- /dev/null +++ b/vcl/unx/generic/gdi/salgdi.cxx @@ -0,0 +1,298 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#if HAVE_FEATURE_SKIA +#include +#include +#endif + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include "cairo_xlib_cairo.hxx" +#include + +#include "X11CairoSalGraphicsImpl.hxx" + + +// X11Common + +X11Common::X11Common() + : m_hDrawable(None) + , m_pColormap(nullptr) +{} + +// X11SalGraphics + +X11SalGraphics::X11SalGraphics(): + m_pFrame(nullptr), + m_pVDev(nullptr), + m_nXScreen( 0 ) +{ +#if HAVE_FEATURE_SKIA + if (SkiaHelper::isVCLSkiaEnabled()) + { + mxImpl.reset(new X11SkiaSalGraphicsImpl(*this)); + mxTextRenderImpl.reset(new SkiaTextRender); + } + else +#endif + { + mxImpl.reset(new X11CairoSalGraphicsImpl(*this, maCairoCommon)); + mxTextRenderImpl.reset(new CairoTextRender(maCairoCommon)); + } +} + +X11SalGraphics::~X11SalGraphics() COVERITY_NOEXCEPT_FALSE +{ + DeInit(); + ReleaseFonts(); + freeResources(); +} + +void X11SalGraphics::freeResources() +{ + mxImpl->freeResources(); + + if( m_pDeleteColormap ) + { + m_pDeleteColormap.reset(); + maX11Common.m_pColormap = nullptr; + } +} + +SalGraphicsImpl* X11SalGraphics::GetImpl() const +{ + return mxImpl.get(); +} + +void X11SalGraphics::SetDrawable(Drawable aDrawable, cairo_surface_t* pSurface, SalX11Screen nXScreen) +{ + maCairoCommon.m_pSurface = pSurface; + if (maCairoCommon.m_pSurface) + { + maCairoCommon.m_aFrameSize.setX(cairo_xlib_surface_get_width(pSurface)); + maCairoCommon.m_aFrameSize.setY(cairo_xlib_surface_get_height(pSurface)); + dl_cairo_surface_get_device_scale(pSurface, &maCairoCommon.m_fScale, nullptr); + } + + // shortcut if nothing changed + if( maX11Common.m_hDrawable == aDrawable ) + return; + + // free screen specific resources if needed + if( nXScreen != m_nXScreen ) + { + freeResources(); + maX11Common.m_pColormap = &vcl_sal::getSalDisplay(GetGenericUnixSalData())->GetColormap( nXScreen ); + m_nXScreen = nXScreen; + } + + maX11Common.m_hDrawable = aDrawable; +} + +void X11SalGraphics::Init( X11SalFrame& rFrame, Drawable aTarget, + SalX11Screen nXScreen ) +{ + maX11Common.m_pColormap = &vcl_sal::getSalDisplay(GetGenericUnixSalData())->GetColormap(nXScreen); + m_nXScreen = nXScreen; + + m_pFrame = &rFrame; + m_pVDev = nullptr; + + SetDrawable(aTarget, rFrame.GetSurface(), nXScreen); + mxImpl->Init(); +} + +void X11SalGraphics::DeInit() +{ + mxImpl->DeInit(); + SetDrawable(None, nullptr, m_nXScreen); +} + +void X11SalGraphics::GetResolution( sal_Int32 &rDPIX, sal_Int32 &rDPIY ) // const +{ + char* pForceDpi; + if ((pForceDpi = getenv("SAL_FORCEDPI"))) + { + OString sForceDPI(pForceDpi); + rDPIX = rDPIY = sForceDPI.toInt32(); + return; + } + + const SalDisplay *pDisplay = GetDisplay(); + if (!pDisplay) + { + SAL_WARN( "vcl", "Null display"); + rDPIX = rDPIY = 96; + return; + } + + Pair dpi = pDisplay->GetResolution(); + rDPIX = dpi.A(); + rDPIY = dpi.B(); + + if ( rDPIY > 200 ) + { + rDPIX = Divide( rDPIX * 200, rDPIY ); + rDPIY = 200; + } + + // #i12705# equalize x- and y-resolution if they are close enough + if( rDPIX == rDPIY ) + return; + + // different x- and y- resolutions are usually artifacts of + // a wrongly calculated screen size. +#ifdef DEBUG + SAL_INFO("vcl.gdi", "Forcing Resolution from " + << std::hex << rDPIX + << std::dec << rDPIX + << " to " + << std::hex << rDPIY + << std::dec << rDPIY); +#endif + rDPIX = rDPIY; // y-resolution is more trustworthy +} + +SystemGraphicsData X11SalGraphics::GetGraphicsData() const +{ + SystemGraphicsData aRes; + + aRes.nSize = sizeof(aRes); + aRes.pDisplay = GetXDisplay(); + aRes.hDrawable = maX11Common.m_hDrawable; + aRes.pVisual = GetVisual().visual; + aRes.nScreen = m_nXScreen.getXScreen(); + return aRes; +} + +void X11SalGraphics::Flush() +{ + if( X11GraphicsImpl* x11Impl = dynamic_cast< X11GraphicsImpl* >( mxImpl.get())) + x11Impl->Flush(); +} + +#if ENABLE_CAIRO_CANVAS + +bool X11SalGraphics::SupportsCairo() const +{ + return true; +} + +cairo::SurfaceSharedPtr X11SalGraphics::CreateSurface(const cairo::CairoSurfaceSharedPtr& rSurface) const +{ + return std::make_shared(rSurface); +} + +namespace +{ + cairo::X11SysData getSysData( const vcl::Window& rWindow ) + { + const SystemEnvData* pSysData = rWindow.GetSystemData(); + + if( !pSysData ) + return cairo::X11SysData(); + else + return cairo::X11SysData(*pSysData, rWindow.ImplGetFrame()); + } + + cairo::X11SysData getSysData( const VirtualDevice& rVirDev ) + { + return cairo::X11SysData( rVirDev.GetSystemGfxData() ); + } +} + +cairo::SurfaceSharedPtr X11SalGraphics::CreateSurface( const OutputDevice& rRefDevice, + int x, int y, int width, int height ) const +{ + if( rRefDevice.GetOutDevType() == OUTDEV_WINDOW ) + return std::make_shared(getSysData(*rRefDevice.GetOwnerWindow()), + x,y,width,height); + if( rRefDevice.IsVirtual() ) + return std::make_shared(getSysData(static_cast(rRefDevice)), + x,y,width,height); + return cairo::SurfaceSharedPtr(); +} + +cairo::SurfaceSharedPtr X11SalGraphics::CreateBitmapSurface( const OutputDevice& rRefDevice, + const BitmapSystemData& rData, + const Size& rSize ) const +{ + SAL_INFO("vcl", "requested size: " << rSize.Width() << " x " << rSize.Height() + << " available size: " << rData.mnWidth << " x " + << rData.mnHeight); + if ( rData.mnWidth == rSize.Width() && rData.mnHeight == rSize.Height() ) + { + if( rRefDevice.GetOutDevType() == OUTDEV_WINDOW ) + return std::make_shared(getSysData(*rRefDevice.GetOwnerWindow()), rData ); + else if( rRefDevice.IsVirtual() ) + return std::make_shared(getSysData(static_cast(rRefDevice)), rData ); + } + + return cairo::SurfaceSharedPtr(); +} + +css::uno::Any X11SalGraphics::GetNativeSurfaceHandle(cairo::SurfaceSharedPtr& rSurface, const basegfx::B2ISize& /*rSize*/) const +{ + cairo::X11Surface& rXlibSurface=dynamic_cast(*rSurface); + css::uno::Sequence< css::uno::Any > args{ + css::uno::Any(false), // do not call XFreePixmap on it + css::uno::Any(sal_Int64(rXlibSurface.getPixmap()->mhDrawable)) + }; + return css::uno::Any(args); +} + +#endif // ENABLE_CAIRO_CANVAS + +SalGeometryProvider *X11SalGraphics::GetGeometryProvider() const +{ + if (m_pFrame) + return static_cast< SalGeometryProvider * >(m_pFrame); + else + return static_cast< SalGeometryProvider * >(m_pVDev); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/gdi/salvd.cxx b/vcl/unx/generic/gdi/salvd.cxx new file mode 100644 index 0000000000..e0a9d33f6e --- /dev/null +++ b/vcl/unx/generic/gdi/salvd.cxx @@ -0,0 +1,241 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include +#if HAVE_FEATURE_SKIA +#include +#endif +#include + +std::unique_ptr X11SalInstance::CreateX11VirtualDevice(const SalGraphics& rGraphics, + tools::Long &nDX, tools::Long &nDY, DeviceFormat eFormat, const SystemGraphicsData *pData, + std::unique_ptr pNewGraphics) +{ + assert(pNewGraphics); +#if HAVE_FEATURE_SKIA + if (SkiaHelper::isVCLSkiaEnabled()) + return std::unique_ptr(new X11SkiaSalVirtualDevice(rGraphics, nDX, nDY, pData, std::move(pNewGraphics))); + else +#endif + return std::unique_ptr(new X11SalVirtualDevice(rGraphics, nDX, nDY, eFormat, pData, std::move(pNewGraphics))); +} + +std::unique_ptr X11SalInstance::CreateVirtualDevice(SalGraphics& rGraphics, + tools::Long &nDX, tools::Long &nDY, DeviceFormat eFormat, const SystemGraphicsData *pData) +{ + return CreateX11VirtualDevice(rGraphics, nDX, nDY, eFormat, pData, std::make_unique()); +} + +void X11SalGraphics::Init(X11SalVirtualDevice *pDevice, SalColormap* pColormap, bool bDeleteColormap) +{ + SalDisplay *pDisplay = pDevice->GetDisplay(); + m_nXScreen = pDevice->GetXScreenNumber(); + + int nVisualDepth = pDisplay->GetColormap( m_nXScreen ).GetVisual().GetDepth(); + int nDeviceDepth = pDevice->GetDepth(); + + if( pColormap ) + { + maX11Common.m_pColormap = pColormap; + if( bDeleteColormap ) + m_pDeleteColormap.reset(pColormap); + } + else if( nDeviceDepth == nVisualDepth ) + maX11Common.m_pColormap = &pDisplay->GetColormap( m_nXScreen ); + else if( nDeviceDepth == 1 ) + { + m_pDeleteColormap.reset(new SalColormap()); + maX11Common.m_pColormap = m_pDeleteColormap.get(); + } + + m_pVDev = pDevice; + m_pFrame = nullptr; + + SetDrawable(pDevice->GetDrawable(), pDevice->GetSurface(), m_nXScreen); + mxImpl->Init(); +} + +X11SalVirtualDevice::X11SalVirtualDevice(const SalGraphics& rGraphics, tools::Long &nDX, tools::Long &nDY, + DeviceFormat /*eFormat*/, const SystemGraphicsData *pData, + std::unique_ptr pNewGraphics) : + pGraphics_(std::move(pNewGraphics)), + m_nXScreen(0), + bGraphics_(false) +{ + SalColormap* pColormap = nullptr; + bool bDeleteColormap = false; + + sal_uInt16 nBitCount = rGraphics.GetBitCount(); + pDisplay_ = vcl_sal::getSalDisplay(GetGenericUnixSalData()); + nDepth_ = nBitCount; + + if( pData && pData->hDrawable != None ) + { + ::Window aRoot; + int x, y; + unsigned int w = 0, h = 0, bw, d; + Display* pDisp = pDisplay_->GetDisplay(); + XGetGeometry( pDisp, pData->hDrawable, + &aRoot, &x, &y, &w, &h, &bw, &d ); + int nScreen = 0; + while( nScreen < ScreenCount( pDisp ) ) + { + if( RootWindow( pDisp, nScreen ) == aRoot ) + break; + nScreen++; + } + nDX_ = static_cast(w); + nDY_ = static_cast(h); + nDX = nDX_; + nDY = nDY_; + m_nXScreen = SalX11Screen( nScreen ); + hDrawable_ = pData->hDrawable; + bExternPixmap_ = true; + } + else + { + nDX_ = nDX; + nDY_ = nDY; + m_nXScreen = static_cast(rGraphics).GetScreenNumber(); + hDrawable_ = limitXCreatePixmap( GetXDisplay(), + pDisplay_->GetDrawable( m_nXScreen ), + nDX_, nDY_, + GetDepth() ); + bExternPixmap_ = false; + } + + if( nBitCount != pDisplay_->GetVisual( m_nXScreen ).GetDepth() ) + { + pColormap = new SalColormap( nBitCount ); + bDeleteColormap = true; + } + + pGraphics_->SetLayout( SalLayoutFlags::NONE ); // by default no! mirroring for VirtualDevices, can be enabled with EnableRTL() + + // tdf#127529 see SvpSalInstance::CreateVirtualDevice for the rare case of a non-null pPreExistingTarget + cairo_surface_t* pPreExistingTarget = pData ? static_cast(pData->pSurface) : nullptr; + if (pPreExistingTarget) + { + m_bOwnsSurface = false; + m_pSurface = pPreExistingTarget; + } + else + { + m_bOwnsSurface = true; + m_pSurface = cairo_xlib_surface_create(GetXDisplay(), hDrawable_, + pDisplay_->GetColormap(m_nXScreen).GetVisual().visual, + nDX_, nDY_); + } + + pGraphics_->Init(this, pColormap, bDeleteColormap); +} + +X11SalVirtualDevice::~X11SalVirtualDevice() +{ + pGraphics_.reset(); + + if (m_bOwnsSurface) + cairo_surface_destroy(m_pSurface); + + if( GetDrawable() && !bExternPixmap_ ) + XFreePixmap( GetXDisplay(), GetDrawable() ); +} + +SalGraphics* X11SalVirtualDevice::AcquireGraphics() +{ + if( bGraphics_ ) + return nullptr; + + if( pGraphics_ ) + bGraphics_ = true; + + return pGraphics_.get(); +} + +void X11SalVirtualDevice::ReleaseGraphics( SalGraphics* ) +{ bGraphics_ = false; } + +bool X11SalVirtualDevice::SetSize( tools::Long nDX, tools::Long nDY ) +{ + if( bExternPixmap_ ) + return false; + + if( !nDX ) nDX = 1; + if( !nDY ) nDY = 1; + + if (m_bOwnsSurface) + cairo_surface_destroy(m_pSurface); + + Pixmap h = limitXCreatePixmap( GetXDisplay(), + pDisplay_->GetDrawable( m_nXScreen ), + nDX, nDY, nDepth_ ); + + if( !h ) + { + if( !GetDrawable() ) + { + hDrawable_ = limitXCreatePixmap( GetXDisplay(), + pDisplay_->GetDrawable( m_nXScreen ), + 1, 1, nDepth_ ); + nDX_ = 1; + nDY_ = 1; + } + + if (m_bOwnsSurface) + { + m_pSurface = cairo_xlib_surface_create(GetXDisplay(), hDrawable_, + pDisplay_->GetColormap(m_nXScreen).GetVisual().visual, + nDX_, nDY_); + } + + return false; + } + + if( GetDrawable() ) + XFreePixmap( GetXDisplay(), GetDrawable() ); + hDrawable_ = h; + + nDX_ = nDX; + nDY_ = nDY; + + if (m_bOwnsSurface) + { + m_pSurface = cairo_xlib_surface_create(GetXDisplay(), hDrawable_, + pDisplay_->GetColormap(m_nXScreen).GetVisual().visual, + nDX_, nDY_); + } + + if( pGraphics_ ) + pGraphics_->Init( this ); + + return true; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/glyphs/freetype_glyphcache.cxx b/vcl/unx/generic/glyphs/freetype_glyphcache.cxx new file mode 100644 index 0000000000..5745f94a22 --- /dev/null +++ b/vcl/unx/generic/glyphs/freetype_glyphcache.cxx @@ -0,0 +1,824 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include +#include + +#include + +#include +#include + +#include + +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include FT_FREETYPE_H +#include FT_GLYPH_H +#include FT_MULTIPLE_MASTERS_H +#include FT_OUTLINE_H +#include FT_SIZES_H +#include FT_SYNTHESIS_H +#include FT_TRUETYPE_TABLES_H + +#include + +// TODO: move file mapping stuff to OSL +#include +#include +#include +#include +#include +#include +#include + +static FT_Library aLibFT = nullptr; + +// TODO: remove when the priorities are selected by UI +// if (AH==0) => disable autohinting +// if (AA==0) => disable antialiasing +// if (EB==0) => disable embedded bitmaps +// if (AA prio <= AH prio) => antialias + autohint +// if (AH do not autohint when antialiasing +// if (EB do not autohint for monochrome +static int nDefaultPrioEmbedded = 2; +static int nDefaultPrioAntiAlias = 1; + +FreetypeFontFile::FreetypeFontFile( OString aNativeFileName ) +: maNativeFileName(std::move( aNativeFileName )), + mpFileMap( nullptr ), + mnFileSize( 0 ), + mnRefCount( 0 ), + mnLangBoost( 0 ) +{ + // boost font preference if UI language is mentioned in filename + int nPos = maNativeFileName.lastIndexOf( '_' ); + if( nPos == -1 || maNativeFileName[nPos+1] == '.' ) + mnLangBoost += 0x1000; // no langinfo => good + else + { + static const char* pLangBoost = nullptr; + static bool bOnce = true; + if( bOnce ) + { + bOnce = false; + pLangBoost = vcl::getLangBoost(); + } + + if( pLangBoost && !strncasecmp( pLangBoost, &maNativeFileName.getStr()[nPos+1], 3 ) ) + mnLangBoost += 0x2000; // matching langinfo => better + } +} + +bool FreetypeFontFile::Map() +{ + if (mnRefCount++ == 0) + { + const char* pFileName = maNativeFileName.getStr(); + int nFile; + int nFD; + int n; + if( sscanf( pFileName, "/:FD:/%d%n", &nFD, &n ) == 1 && pFileName[n] == '\0' ) + { + lseek( nFD, 0, SEEK_SET ); + nFile = dup( nFD ); + } + else + nFile = open( pFileName, O_RDONLY ); + if( nFile < 0 ) + { + SAL_WARN("vcl.unx.freetype", "open('" << maNativeFileName << "') failed: " << strerror(errno)); + return false; + } + + struct stat aStat; + int nRet = fstat( nFile, &aStat ); + if (nRet < 0) + { + SAL_WARN("vcl.unx.freetype", "fstat on '" << maNativeFileName << "' failed: " << strerror(errno)); + close (nFile); + return false; + } + mnFileSize = aStat.st_size; + mpFileMap = static_cast( + mmap( nullptr, mnFileSize, PROT_READ, MAP_SHARED, nFile, 0 )); + if( mpFileMap == MAP_FAILED ) + { + SAL_WARN("vcl.unx.freetype", "mmap of '" << maNativeFileName << "' failed: " << strerror(errno)); + mpFileMap = nullptr; + } + else + SAL_INFO("vcl.unx.freetype", "mmap'ed '" << maNativeFileName << "' successfully"); + close( nFile ); + } + + return (mpFileMap != nullptr); +} + +void FreetypeFontFile::Unmap() +{ + if (--mnRefCount != 0) + return; + assert(mnRefCount >= 0 && "how did this go negative\n"); + if (mpFileMap) + { + munmap(mpFileMap, mnFileSize); + mpFileMap = nullptr; + } +} + +FreetypeFontInfo::FreetypeFontInfo( FontAttributes aDevFontAttributes, + FreetypeFontFile* const pFontFile, int nFaceNum, int nFaceVariation, sal_IntPtr nFontId) +: + maFaceFT( nullptr ), + mpFontFile(pFontFile), + mnFaceNum( nFaceNum ), + mnFaceVariation( nFaceVariation ), + mnRefCount( 0 ), + mnFontId( nFontId ), + maDevFontAttributes(std::move( aDevFontAttributes )) +{ + // prefer font with low ID + maDevFontAttributes.IncreaseQualityBy( 10000 - nFontId ); + // prefer font with matching file names + maDevFontAttributes.IncreaseQualityBy( mpFontFile->GetLangBoost() ); +} + +FreetypeFontInfo::~FreetypeFontInfo() +{ +} + +namespace +{ + void dlFT_Done_MM_Var(FT_Library library, FT_MM_Var *amaster) + { +#if !HAVE_DLAPI + FT_Done_MM_Var(library, amaster); +#else + static auto func = reinterpret_cast( + osl_getAsciiFunctionSymbol(nullptr, "FT_Done_MM_Var")); + if (func) + func(library, amaster); + else + free(amaster); +#endif + } +} + +FT_FaceRec_* FreetypeFontInfo::GetFaceFT() +{ + if (!maFaceFT && mpFontFile->Map()) + { + FT_Error rc = FT_New_Memory_Face( aLibFT, + mpFontFile->GetBuffer(), + mpFontFile->GetFileSize(), mnFaceNum, &maFaceFT ); + if( (rc != FT_Err_Ok) || (maFaceFT->num_glyphs <= 0) ) + maFaceFT = nullptr; + + if (maFaceFT && mnFaceVariation) + { + FT_MM_Var *pFtMMVar; + if (FT_Get_MM_Var(maFaceFT, &pFtMMVar) == 0) + { + if (o3tl::make_unsigned(mnFaceVariation) <= pFtMMVar->num_namedstyles) + { + FT_Var_Named_Style *instance = &pFtMMVar->namedstyle[mnFaceVariation - 1]; + FT_Set_Var_Design_Coordinates(maFaceFT, pFtMMVar->num_axis, instance->coords); + } + dlFT_Done_MM_Var(aLibFT, pFtMMVar); + } + } + } + + ++mnRefCount; + return maFaceFT; +} + +void FreetypeFontInfo::ReleaseFaceFT() +{ + if (--mnRefCount == 0) + { + if (maFaceFT) + { + FT_Done_Face(maFaceFT); + maFaceFT = nullptr; + } + mpFontFile->Unmap(); + } + assert(mnRefCount >= 0 && "how did this go negative\n"); +} + +void FreetypeFontInfo::AnnounceFont( vcl::font::PhysicalFontCollection* pFontCollection ) +{ + rtl::Reference pFD(new FreetypeFontFace( this, maDevFontAttributes )); + pFontCollection->Add( pFD.get() ); +} + +void FreetypeManager::InitFreetype() +{ + /*FT_Error rcFT =*/ FT_Init_FreeType( &aLibFT ); + + // TODO: remove when the priorities are selected by UI + char* pEnv; + pEnv = ::getenv( "SAL_EMBEDDED_BITMAP_PRIORITY" ); + if( pEnv ) + nDefaultPrioEmbedded = pEnv[0] - '0'; + pEnv = ::getenv( "SAL_ANTIALIASED_TEXT_PRIORITY" ); + if( pEnv ) + nDefaultPrioAntiAlias = pEnv[0] - '0'; +} + +FT_Face FreetypeFont::GetFtFace() const +{ + FT_Activate_Size( maSizeFT ); + + return maFaceFT; +} + +void FreetypeManager::AddFontFile(const OString& rNormalizedName, + int nFaceNum, int nVariantNum, sal_IntPtr nFontId, const FontAttributes& rDevFontAttr) +{ + if( rNormalizedName.isEmpty() ) + return; + + if( m_aFontInfoList.find( nFontId ) != m_aFontInfoList.end() ) + return; + + FreetypeFontInfo* pFontInfo = new FreetypeFontInfo( rDevFontAttr, + FindFontFile(rNormalizedName), nFaceNum, nVariantNum, nFontId); + m_aFontInfoList[ nFontId ].reset(pFontInfo); +} + +void FreetypeManager::AnnounceFonts( vcl::font::PhysicalFontCollection* pToAdd ) const +{ + for (auto const& font : m_aFontInfoList) + { + FreetypeFontInfo* pFreetypeFontInfo = font.second.get(); + pFreetypeFontInfo->AnnounceFont( pToAdd ); + } +} + +FreetypeFont* FreetypeManager::CreateFont(FreetypeFontInstance* pFontInstance) +{ + // find a FontInfo matching to the font id + if (!pFontInstance) + return nullptr; + + const vcl::font::PhysicalFontFace* pFontFace = pFontInstance->GetFontFace(); + if (!pFontFace) + return nullptr; + + sal_IntPtr nFontId = pFontFace->GetFontId(); + FontInfoList::iterator it = m_aFontInfoList.find(nFontId); + + if (it == m_aFontInfoList.end()) + return nullptr; + + return new FreetypeFont(*pFontInstance, it->second); +} + +FreetypeFontFace::FreetypeFontFace( FreetypeFontInfo* pFI, const FontAttributes& rDFA ) +: vcl::font::PhysicalFontFace( rDFA ), + mpFreetypeFontInfo( pFI ) +{ + assert(mpFreetypeFontInfo); +} + +rtl::Reference FreetypeFontFace::CreateFontInstance(const vcl::font::FontSelectPattern& rFSD) const +{ + return new FreetypeFontInstance(*this, rFSD); +} + +namespace +{ +hb_blob_t* CreateHbBlob(FreetypeFontFile* pFontFile) +{ + auto pFileName = pFontFile->GetFileName().getStr(); + int nFD; + int n; + if (sscanf(pFileName, "/:FD:/%d%n", &nFD, &n) == 1 && pFileName[n] == '\0') + { + if (pFontFile->Map()) + return hb_blob_create( + reinterpret_cast(pFontFile->GetBuffer()), pFontFile->GetFileSize(), + HB_MEMORY_MODE_READONLY, pFontFile, + [](void* data) { static_cast(data)->Unmap(); }); + pFontFile->Unmap(); + return hb_blob_get_empty(); + } + return hb_blob_create_from_file(pFileName); +} +} + +hb_face_t* FreetypeFontFace::GetHbFace() const +{ + if (!mpHbFace) + { + auto pFontFile = mpFreetypeFontInfo->GetFontFile(); + auto nIndex = mpFreetypeFontInfo->GetFontFaceIndex(); + auto pHbBlob = CreateHbBlob(pFontFile); + mpHbFace = hb_face_create(pHbBlob, nIndex); + hb_blob_destroy(pHbBlob); + } + return mpHbFace; +} + +hb_blob_t* FreetypeFontFace::GetHbTable(hb_tag_t nTag) const +{ + return hb_face_reference_table(mpHbFace, nTag); +} + +const std::vector& FreetypeFontFace::GetVariations(const LogicalFontInstance&) const +{ + if (!mxVariations) + { + mxVariations.emplace(); + FT_Face aFaceFT = mpFreetypeFontInfo->GetFaceFT(); + sal_uInt32 nFaceVariation = mpFreetypeFontInfo->GetFontFaceVariation(); + if (!(aFaceFT && nFaceVariation)) + return *mxVariations; + + FT_MM_Var* pFtMMVar; + if (FT_Get_MM_Var(aFaceFT, &pFtMMVar) != 0) + return *mxVariations; + + if (nFaceVariation <= pFtMMVar->num_namedstyles) + { + FT_Var_Named_Style* instance = &pFtMMVar->namedstyle[nFaceVariation - 1]; + mxVariations->resize(pFtMMVar->num_axis); + for (FT_UInt i = 0; i < pFtMMVar->num_axis; ++i) + { + (*mxVariations)[i].tag = pFtMMVar->axis[i].tag; + (*mxVariations)[i].value = instance->coords[i] / 65536.0; + } + } + dlFT_Done_MM_Var(aLibFT, pFtMMVar); + } + + return *mxVariations; +} + +// FreetypeFont + +FreetypeFont::FreetypeFont(FreetypeFontInstance& rFontInstance, std::shared_ptr xFI) +: mrFontInstance(rFontInstance), + mnCos( 0x10000), + mnSin( 0 ), + mnPrioAntiAlias(nDefaultPrioAntiAlias), + mxFontInfo(std::move(xFI)), + maFaceFT( nullptr ), + maSizeFT( nullptr ), + mbFaceOk( false ) +{ + maFaceFT = mxFontInfo->GetFaceFT(); + + const vcl::font::FontSelectPattern& rFSD = rFontInstance.GetFontSelectPattern(); + + if( rFSD.mnOrientation ) + { + const double dRad = toRadians(rFSD.mnOrientation); + mnCos = static_cast( 0x10000 * cos( dRad ) + 0.5 ); + mnSin = static_cast( 0x10000 * sin( dRad ) + 0.5 ); + } + + // set the pixel size of the font instance + mnWidth = rFSD.mnWidth; + if( !mnWidth ) + mnWidth = rFSD.mnHeight; + if (rFSD.mnHeight == 0) + { + SAL_WARN("vcl", "FreetypeFont divide by zero"); + mfStretch = 1.0; + return; + } + mfStretch = static_cast(mnWidth) / rFSD.mnHeight; + // sanity checks (e.g. #i66394#, #i66244#, #i66537#) + if (mnWidth < 0) + { + SAL_WARN("vcl", "FreetypeFont negative font width of: " << mnWidth); + return; + } + + SAL_WARN_IF(mfStretch > +64.0 || mfStretch < -64.0, "vcl", "FreetypeFont excessive stretch of: " << mfStretch); + + if( !maFaceFT ) + return; + + FT_New_Size( maFaceFT, &maSizeFT ); + FT_Activate_Size( maSizeFT ); + /* This might fail for color bitmap fonts, but that is fine since we will + * not need any glyph data from FreeType in this case */ + /*FT_Error rc = */ FT_Set_Pixel_Sizes( maFaceFT, mnWidth, rFSD.mnHeight ); + + mbFaceOk = true; +} + +namespace +{ + std::unique_ptr GetFCFontOptions(const FontAttributes& rFontAttributes, int nSize) + { + return psp::PrintFontManager::getFontOptions(rFontAttributes, nSize); + } +} + +const FontConfigFontOptions* FreetypeFont::GetFontOptions() const +{ + if (!mxFontOptions) + { + mxFontOptions = GetFCFontOptions(mxFontInfo->GetFontAttributes(), mrFontInstance.GetFontSelectPattern().mnHeight); + mxFontOptions->SyncPattern(GetFontFileName(), GetFontFaceIndex(), GetFontFaceVariation(), mrFontInstance.NeedsArtificialBold()); + } + return mxFontOptions.get(); +} + +const OString& FreetypeFont::GetFontFileName() const +{ + return mxFontInfo->GetFontFileName(); +} + +int FreetypeFont::GetFontFaceIndex() const +{ + return mxFontInfo->GetFontFaceIndex(); +} + +int FreetypeFont::GetFontFaceVariation() const +{ + return mxFontInfo->GetFontFaceVariation(); +} + +FreetypeFont::~FreetypeFont() +{ + if( maSizeFT ) + FT_Done_Size( maSizeFT ); + + mxFontInfo->ReleaseFaceFT(); +} + +void FreetypeFont::GetFontMetric(FontMetricDataRef const & rxTo) const +{ + rxTo->FontAttributes::operator =(mxFontInfo->GetFontAttributes()); + + rxTo->SetOrientation(mrFontInstance.GetFontSelectPattern().mnOrientation); + + FT_Activate_Size( maSizeFT ); + + rxTo->ImplCalcLineSpacing(&mrFontInstance); + rxTo->ImplInitBaselines(&mrFontInstance); + + rxTo->SetSlant( 0 ); + rxTo->SetWidth( mnWidth ); + + const TT_OS2* pOS2 = static_cast(FT_Get_Sfnt_Table( maFaceFT, ft_sfnt_os2 )); + if( pOS2 && (pOS2->version != 0xFFFF) ) + { + // map the panose info from the OS2 table to their VCL counterparts + switch( pOS2->panose[0] ) + { + case 1: rxTo->SetFamilyType( FAMILY_ROMAN ); break; + case 2: rxTo->SetFamilyType( FAMILY_SWISS ); break; + case 3: rxTo->SetFamilyType( FAMILY_MODERN ); break; + case 4: rxTo->SetFamilyType( FAMILY_SCRIPT ); break; + case 5: rxTo->SetFamilyType( FAMILY_DECORATIVE ); break; + // TODO: is it reasonable to override the attribute with DONTKNOW? + case 0: // fall through + default: rxTo->SetFamilyType( FAMILY_DONTKNOW ); break; + } + + switch( pOS2->panose[3] ) + { + case 2: // fall through + case 3: // fall through + case 4: // fall through + case 5: // fall through + case 6: // fall through + case 7: // fall through + case 8: rxTo->SetPitch( PITCH_VARIABLE ); break; + case 9: rxTo->SetPitch( PITCH_FIXED ); break; + // TODO: is it reasonable to override the attribute with DONTKNOW? + case 0: // fall through + case 1: // fall through + default: rxTo->SetPitch( PITCH_DONTKNOW ); break; + } + } + + // initialize kashida width + rxTo->SetMinKashida(mrFontInstance.GetKashidaWidth()); +} + +void FreetypeFont::ApplyGlyphTransform(bool bVertical, FT_Glyph pGlyphFT ) const +{ + // shortcut most common case + if (!mrFontInstance.GetFontSelectPattern().mnOrientation && !bVertical) + return; + + const FT_Size_Metrics& rMetrics = maFaceFT->size->metrics; + FT_Vector aVector; + FT_Matrix aMatrix; + + bool bStretched = false; + + if (!bVertical) + { + // straight + aVector.x = 0; + aVector.y = 0; + aMatrix.xx = +mnCos; + aMatrix.yy = +mnCos; + aMatrix.xy = -mnSin; + aMatrix.yx = +mnSin; + } + else + { + // left + bStretched = (mfStretch != 1.0); + aVector.x = static_cast(+rMetrics.descender * mfStretch); + aVector.y = -rMetrics.ascender; + aMatrix.xx = static_cast(-mnSin / mfStretch); + aMatrix.yy = static_cast(-mnSin * mfStretch); + aMatrix.xy = static_cast(-mnCos * mfStretch); + aMatrix.yx = static_cast(+mnCos / mfStretch); + } + + if( pGlyphFT->format != FT_GLYPH_FORMAT_BITMAP ) + { + FT_Glyph_Transform( pGlyphFT, nullptr, &aVector ); + + // orthogonal transforms are better handled by bitmap operations + if( bStretched ) + { + // apply non-orthogonal or stretch transformations + FT_Glyph_Transform( pGlyphFT, &aMatrix, nullptr ); + } + } + else + { + // FT<=2005 ignores transforms for bitmaps, so do it manually + FT_BitmapGlyph pBmpGlyphFT = reinterpret_cast(pGlyphFT); + pBmpGlyphFT->left += (aVector.x + 32) >> 6; + pBmpGlyphFT->top += (aVector.y + 32) >> 6; + } +} + +bool FreetypeFont::GetAntialiasAdvice() const +{ + // TODO: also use GASP info + return !mrFontInstance.GetFontSelectPattern().mbNonAntialiased && (mnPrioAntiAlias > 0); +} + +// outline stuff + +namespace { + +class PolyArgs +{ +public: + PolyArgs( tools::PolyPolygon& rPolyPoly, sal_uInt16 nMaxPoints ); + + void AddPoint( tools::Long nX, tools::Long nY, PolyFlags); + void ClosePolygon(); + + tools::Long GetPosX() const { return maPosition.x;} + tools::Long GetPosY() const { return maPosition.y;} + +private: + tools::PolyPolygon& mrPolyPoly; + + std::unique_ptr + mpPointAry; + std::unique_ptr + mpFlagAry; + + FT_Vector maPosition; + sal_uInt16 mnMaxPoints; + sal_uInt16 mnPoints; + sal_uInt16 mnPoly; + bool bHasOffline; + + PolyArgs(const PolyArgs&) = delete; + PolyArgs& operator=(const PolyArgs&) = delete; +}; + +} + +PolyArgs::PolyArgs( tools::PolyPolygon& rPolyPoly, sal_uInt16 nMaxPoints ) +: mrPolyPoly(rPolyPoly), + mnMaxPoints(nMaxPoints), + mnPoints(0), + mnPoly(0), + bHasOffline(false) +{ + mpPointAry.reset( new Point[ mnMaxPoints ] ); + mpFlagAry.reset( new PolyFlags [ mnMaxPoints ] ); + maPosition.x = maPosition.y = 0; +} + +void PolyArgs::AddPoint( tools::Long nX, tools::Long nY, PolyFlags aFlag ) +{ + SAL_WARN_IF( (mnPoints >= mnMaxPoints), "vcl", "FTGlyphOutline: AddPoint overflow!" ); + if( mnPoints >= mnMaxPoints ) + return; + + maPosition.x = nX; + maPosition.y = nY; + mpPointAry[ mnPoints ] = Point( nX, nY ); + mpFlagAry[ mnPoints++ ]= aFlag; + bHasOffline |= (aFlag != PolyFlags::Normal); +} + +void PolyArgs::ClosePolygon() +{ + if( !mnPoly++ ) + return; + + // freetype seems to always close the polygon with an ON_CURVE point + // PolyPoly wants to close the polygon itself => remove last point + SAL_WARN_IF( (mnPoints < 2), "vcl", "FTGlyphOutline: PolyFinishNum failed!" ); + --mnPoints; + SAL_WARN_IF( (mpPointAry[0]!=mpPointAry[mnPoints]), "vcl", "FTGlyphOutline: PolyFinishEq failed!" ); + SAL_WARN_IF( (mpFlagAry[0]!=PolyFlags::Normal), "vcl", "FTGlyphOutline: PolyFinishFE failed!" ); + SAL_WARN_IF( (mpFlagAry[mnPoints]!=PolyFlags::Normal), "vcl", "FTGlyphOutline: PolyFinishFS failed!" ); + + tools::Polygon aPoly( mnPoints, mpPointAry.get(), (bHasOffline ? mpFlagAry.get() : nullptr) ); + + // #i35928# + // This may be an invalid polygon, e.g. the last point is a control point. + // So close the polygon (and add the first point again) if the last point + // is a control point or different from first. + // #i48298# + // Now really duplicating the first point, to close or correct the + // polygon. Also no longer duplicating the flags, but enforcing + // PolyFlags::Normal for the newly added last point. + const sal_uInt16 nPolySize(aPoly.GetSize()); + if(nPolySize) + { + if((aPoly.HasFlags() && PolyFlags::Control == aPoly.GetFlags(nPolySize - 1)) + || (aPoly.GetPoint(nPolySize - 1) != aPoly.GetPoint(0))) + { + aPoly.SetSize(nPolySize + 1); + aPoly.SetPoint(aPoly.GetPoint(0), nPolySize); + + if(aPoly.HasFlags()) + { + aPoly.SetFlags(nPolySize, PolyFlags::Normal); + } + } + } + + mrPolyPoly.Insert( aPoly ); + mnPoints = 0; + bHasOffline = false; +} + +extern "C" { + +// TODO: wait till all compilers accept that calling conventions +// for functions are the same independent of implementation constness, +// then uncomment the const-tokens in the function interfaces below +static int FT_move_to( const FT_Vector* p0, void* vpPolyArgs ) +{ + PolyArgs& rA = *static_cast(vpPolyArgs); + + // move_to implies a new polygon => finish old polygon first + rA.ClosePolygon(); + + rA.AddPoint( p0->x, p0->y, PolyFlags::Normal ); + return 0; +} + +static int FT_line_to( const FT_Vector* p1, void* vpPolyArgs ) +{ + PolyArgs& rA = *static_cast(vpPolyArgs); + rA.AddPoint( p1->x, p1->y, PolyFlags::Normal ); + return 0; +} + +static int FT_conic_to( const FT_Vector* p1, const FT_Vector* p2, void* vpPolyArgs ) +{ + PolyArgs& rA = *static_cast(vpPolyArgs); + + // VCL's Polygon only knows cubic beziers + const tools::Long nX1 = (2 * rA.GetPosX() + 4 * p1->x + 3) / 6; + const tools::Long nY1 = (2 * rA.GetPosY() + 4 * p1->y + 3) / 6; + rA.AddPoint( nX1, nY1, PolyFlags::Control ); + + const tools::Long nX2 = (2 * p2->x + 4 * p1->x + 3) / 6; + const tools::Long nY2 = (2 * p2->y + 4 * p1->y + 3) / 6; + rA.AddPoint( nX2, nY2, PolyFlags::Control ); + + rA.AddPoint( p2->x, p2->y, PolyFlags::Normal ); + return 0; +} + +static int FT_cubic_to( const FT_Vector* p1, const FT_Vector* p2, const FT_Vector* p3, void* vpPolyArgs ) +{ + PolyArgs& rA = *static_cast(vpPolyArgs); + rA.AddPoint( p1->x, p1->y, PolyFlags::Control ); + rA.AddPoint( p2->x, p2->y, PolyFlags::Control ); + rA.AddPoint( p3->x, p3->y, PolyFlags::Normal ); + return 0; +} + +} // extern "C" + +bool FreetypeFont::GetGlyphOutline(sal_GlyphId nId, basegfx::B2DPolyPolygon& rB2DPolyPoly, bool bIsVertical) const +{ + if( maSizeFT ) + FT_Activate_Size( maSizeFT ); + + rB2DPolyPoly.clear(); + + FT_Int nLoadFlags = FT_LOAD_DEFAULT | FT_LOAD_IGNORE_TRANSFORM; + +#ifdef FT_LOAD_TARGET_LIGHT + // enable "light hinting" if available + nLoadFlags |= FT_LOAD_TARGET_LIGHT; +#endif + + FT_Error rc = FT_Load_Glyph(maFaceFT, nId, nLoadFlags); + if( rc != FT_Err_Ok ) + return false; + + if (mrFontInstance.NeedsArtificialBold()) + FT_GlyphSlot_Embolden(maFaceFT->glyph); + + FT_Glyph pGlyphFT; + rc = FT_Get_Glyph( maFaceFT->glyph, &pGlyphFT ); + if( rc != FT_Err_Ok ) + return false; + + if( pGlyphFT->format != FT_GLYPH_FORMAT_OUTLINE ) + { + FT_Done_Glyph( pGlyphFT ); + return false; + } + + if (mrFontInstance.NeedsArtificialItalic()) + { + FT_Matrix aMatrix; + aMatrix.xx = aMatrix.yy = ARTIFICIAL_ITALIC_MATRIX_XX; + aMatrix.xy = ARTIFICIAL_ITALIC_MATRIX_XY; + aMatrix.yx = 0; + FT_Glyph_Transform( pGlyphFT, &aMatrix, nullptr ); + } + + FT_Outline& rOutline = reinterpret_cast(pGlyphFT)->outline; + if( !rOutline.n_points ) // blank glyphs are ok + { + FT_Done_Glyph( pGlyphFT ); + return true; + } + + tools::Long nMaxPoints = 1 + rOutline.n_points * 3; + tools::PolyPolygon aToolPolyPolygon; + PolyArgs aPolyArg( aToolPolyPolygon, nMaxPoints ); + + ApplyGlyphTransform(bIsVertical, pGlyphFT); + + FT_Outline_Funcs aFuncs; + aFuncs.move_to = &FT_move_to; + aFuncs.line_to = &FT_line_to; + aFuncs.conic_to = &FT_conic_to; + aFuncs.cubic_to = &FT_cubic_to; + aFuncs.shift = 0; + aFuncs.delta = 0; + FT_Outline_Decompose( &rOutline, &aFuncs, static_cast(&aPolyArg) ); + aPolyArg.ClosePolygon(); // close last polygon + FT_Done_Glyph( pGlyphFT ); + + // convert to basegfx polypolygon + // TODO: get rid of the intermediate tools polypolygon + rB2DPolyPoly = aToolPolyPolygon.getB2DPolyPolygon(); + rB2DPolyPoly.transform(basegfx::utils::createScaleB2DHomMatrix( +0x1p-6, -0x1p-6 )); + + return true; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/glyphs/glyphcache.cxx b/vcl/unx/generic/glyphs/glyphcache.cxx new file mode 100644 index 0000000000..ac3c5e15ab --- /dev/null +++ b/vcl/unx/generic/glyphs/glyphcache.cxx @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include +#include + +#include + +#include +#include + +FreetypeManager::FreetypeManager() +{ + InitFreetype(); +} + +FreetypeManager::~FreetypeManager() +{ + ClearFontCache(); +} + +void FreetypeManager::ClearFontCache() +{ + m_aFontInfoList.clear(); +} + +FreetypeManager& FreetypeManager::get() +{ + GenericUnixSalData* const pSalData(GetGenericUnixSalData()); + assert(pSalData); + return *pSalData->GetFreetypeManager(); +} + +FreetypeFontFile* FreetypeManager::FindFontFile(const OString& rNativeFileName) +{ + // font file already known? (e.g. for ttc, synthetic, aliased fonts) + const char* pFileName = rNativeFileName.getStr(); + FontFileList::const_iterator it = m_aFontFileList.find(pFileName); + if (it != m_aFontFileList.end()) + return it->second.get(); + + // no => create new one + FreetypeFontFile* pFontFile = new FreetypeFontFile(rNativeFileName); + pFileName = pFontFile->maNativeFileName.getStr(); + m_aFontFileList[pFileName].reset(pFontFile); + return pFontFile; +} + +FreetypeFontInstance::FreetypeFontInstance(const vcl::font::PhysicalFontFace& rPFF, const vcl::font::FontSelectPattern& rFSP) + : LogicalFontInstance(rPFF, rFSP) + , mxFreetypeFont(FreetypeManager::get().CreateFont(this)) +{ +} + +FreetypeFontInstance::~FreetypeFontInstance() +{ +} + +bool FreetypeFontInstance::GetGlyphOutline(sal_GlyphId nId, basegfx::B2DPolyPolygon& rPoly, bool bVertical) const +{ + assert(mxFreetypeFont); + if (!mxFreetypeFont) + return false; + return mxFreetypeFont->GetGlyphOutline(nId, rPoly, bVertical); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/print/genprnpsp.cxx b/vcl/unx/generic/print/genprnpsp.cxx new file mode 100644 index 0000000000..33990decad --- /dev/null +++ b/vcl/unx/generic/print/genprnpsp.cxx @@ -0,0 +1,1097 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +/** + this file implements the sal printer interface (SalPrinter, SalInfoPrinter + and some printer relevant methods of SalInstance and SalGraphicsData) + + as underlying library the printer features of psprint are used. + + The query methods of a SalInfoPrinter are implemented by querying psprint + + The job methods of a SalPrinter are implemented by calling psprint + printer job functions. + */ + +#include + +#include + +// For spawning PDF and FAX generation +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include "prtsetup.hxx" +#include + +#include + +using namespace psp; +using namespace com::sun::star; + +static bool getPdfDir( const PrinterInfo& rInfo, OUString &rDir ) +{ + sal_Int32 nIndex = 0; + while( nIndex != -1 ) + { + OUString aToken( rInfo.m_aFeatures.getToken( 0, ',', nIndex ) ); + if( aToken.startsWith( "pdf=" ) ) + { + sal_Int32 nPos = 0; + rDir = aToken.getToken( 1, '=', nPos ); + if( rDir.isEmpty() && getenv( "HOME" ) ) + rDir = OUString( getenv( "HOME" ), strlen( getenv( "HOME" ) ), osl_getThreadTextEncoding() ); + return true; + } + } + return false; +} + +namespace +{ + class QueryString : public weld::GenericDialogController + { + private: + OUString& m_rReturnValue; + + std::unique_ptr m_xOKButton; + std::unique_ptr m_xFixedText; + std::unique_ptr m_xEdit; + + DECL_LINK( ClickBtnHdl, weld::Button&, void ); + + public: + // parent window, Query text, initial value + QueryString(weld::Window*, OUString const &, OUString &); + }; + + /* + * QueryString + */ + QueryString::QueryString(weld::Window* pParent, OUString const & rQuery, OUString& rRet) + : GenericDialogController(pParent, "vcl/ui/querydialog.ui", "QueryDialog") + , m_rReturnValue( rRet ) + , m_xOKButton(m_xBuilder->weld_button("ok")) + , m_xFixedText(m_xBuilder->weld_label("label")) + , m_xEdit(m_xBuilder->weld_entry("entry")) + { + m_xOKButton->connect_clicked(LINK(this, QueryString, ClickBtnHdl)); + m_xFixedText->set_label(rQuery); + m_xEdit->set_text(m_rReturnValue); + m_xDialog->set_title(rQuery); + } + + IMPL_LINK(QueryString, ClickBtnHdl, weld::Button&, rButton, void) + { + if (&rButton == m_xOKButton.get()) + { + m_rReturnValue = m_xEdit->get_text(); + m_xDialog->response(RET_OK); + } + else + m_xDialog->response(RET_CANCEL); + } + + int QueryFaxNumber(OUString& rNumber) + { + QueryString aQuery(Application::GetDefDialogParent(), VclResId(SV_PRINT_QUERYFAXNUMBER_TXT), rNumber); + return aQuery.run(); + } +} + +static int PtTo10Mu( int nPoints ) { return static_cast((static_cast(nPoints)*35.27777778)+0.5); } + +static int TenMuToPt( int nUnits ) { return static_cast((static_cast(nUnits)/35.27777778)+0.5); } + +static void copyJobDataToJobSetup( ImplJobSetup* pJobSetup, JobData& rData ) +{ + pJobSetup->SetOrientation( rData.m_eOrientation == orientation::Landscape ? + Orientation::Landscape : Orientation::Portrait ); + + // copy page size + OUString aPaper; + int width, height; + + rData.m_aContext.getPageSize( aPaper, width, height ); + pJobSetup->SetPaperFormat( PaperInfo::fromPSName( + OUStringToOString( aPaper, RTL_TEXTENCODING_ISO_8859_1 ))); + + pJobSetup->SetPaperWidth( 0 ); + pJobSetup->SetPaperHeight( 0 ); + if( pJobSetup->GetPaperFormat() == PAPER_USER ) + { + // transform to 100dth mm + width = PtTo10Mu( width ); + height = PtTo10Mu( height ); + + if( rData.m_eOrientation == psp::orientation::Portrait ) + { + pJobSetup->SetPaperWidth( width ); + pJobSetup->SetPaperHeight( height ); + } + else + { + pJobSetup->SetPaperWidth( height ); + pJobSetup->SetPaperHeight( width ); + } + } + + // copy input slot + const PPDKey* pKey = nullptr; + const PPDValue* pValue = nullptr; + + pJobSetup->SetPaperBin( 0 ); + if( rData.m_pParser ) + pKey = rData.m_pParser->getKey( "InputSlot" ); + if( pKey ) + pValue = rData.m_aContext.getValue( pKey ); + if( pKey && pValue ) + { + int nPaperBin; + for( nPaperBin = 0; + pValue != pKey->getValue( nPaperBin ) && + nPaperBin < pKey->countValues(); + nPaperBin++); + pJobSetup->SetPaperBin( + nPaperBin == pKey->countValues() ? 0 : nPaperBin); + } + + // copy duplex + pKey = nullptr; + pValue = nullptr; + + pJobSetup->SetDuplexMode( DuplexMode::Unknown ); + if( rData.m_pParser ) + pKey = rData.m_pParser->getKey( "Duplex" ); + if( pKey ) + pValue = rData.m_aContext.getValue( pKey ); + if( pKey && pValue ) + { + if( pValue->m_aOption.equalsIgnoreAsciiCase( "None" ) || + pValue->m_aOption.startsWithIgnoreAsciiCase( "Simplex" ) + ) + { + pJobSetup->SetDuplexMode( DuplexMode::Off); + } + else if( pValue->m_aOption.equalsIgnoreAsciiCase( "DuplexNoTumble" ) ) + { + pJobSetup->SetDuplexMode( DuplexMode::LongEdge ); + } + else if( pValue->m_aOption.equalsIgnoreAsciiCase( "DuplexTumble" ) ) + { + pJobSetup->SetDuplexMode( DuplexMode::ShortEdge ); + } + } + + // copy the whole context + + sal_uInt32 nBytes; + std::unique_ptr pBuffer; + if( rData.getStreamBuffer( pBuffer, nBytes ) ) + { + pJobSetup->SetDriverData( std::move(pBuffer), nBytes ); + } + else + { + pJobSetup->SetDriverData( nullptr, 0 ); + } + pJobSetup->SetPapersizeFromSetup( rData.m_bPapersizeFromSetup ); +} + +static std::vector getFaxNumbers() +{ + std::vector aFaxNumbers; + + OUString aNewNr; + if (QueryFaxNumber(aNewNr)) + { + for (sal_Int32 nIndex {0}; nIndex >= 0; ) + aFaxNumbers.push_back(aNewNr.getToken( 0, ';', nIndex )); + } + + return aFaxNumbers; +} + +/* + * SalInstance + */ + +void SalGenericInstance::configurePspInfoPrinter(PspSalInfoPrinter *pPrinter, + SalPrinterQueueInfo const * pQueueInfo, ImplJobSetup* pJobSetup) +{ + if( !pJobSetup ) + return; + + PrinterInfoManager& rManager( PrinterInfoManager::get() ); + PrinterInfo aInfo( rManager.getPrinterInfo( pQueueInfo->maPrinterName ) ); + pPrinter->m_aJobData = aInfo; + + if( pJobSetup->GetDriverData() ) + JobData::constructFromStreamBuffer( pJobSetup->GetDriverData(), + pJobSetup->GetDriverDataLen(), aInfo ); + + pJobSetup->SetSystem( JOBSETUP_SYSTEM_UNIX ); + pJobSetup->SetPrinterName( pQueueInfo->maPrinterName ); + pJobSetup->SetDriver( aInfo.m_aDriverName ); + copyJobDataToJobSetup( pJobSetup, aInfo ); +} + +SalInfoPrinter* SalGenericInstance::CreateInfoPrinter( SalPrinterQueueInfo* pQueueInfo, + ImplJobSetup* pJobSetup ) +{ + mbPrinterInit = true; + // create and initialize SalInfoPrinter + PspSalInfoPrinter* pPrinter = new PspSalInfoPrinter(); + configurePspInfoPrinter(pPrinter, pQueueInfo, pJobSetup); + return pPrinter; +} + +void SalGenericInstance::DestroyInfoPrinter( SalInfoPrinter* pPrinter ) +{ + delete pPrinter; +} + +std::unique_ptr SalGenericInstance::CreatePrinter( SalInfoPrinter* pInfoPrinter ) +{ + mbPrinterInit = true; + // create and initialize SalPrinter + PspSalPrinter* pPrinter = new PspSalPrinter( pInfoPrinter ); + pPrinter->m_aJobData = static_cast(pInfoPrinter)->m_aJobData; + + return std::unique_ptr(pPrinter); +} + +void SalGenericInstance::GetPrinterQueueInfo( ImplPrnQueueList* pList ) +{ + mbPrinterInit = true; + PrinterInfoManager& rManager( PrinterInfoManager::get() ); + static const char* pNoSyncDetection = getenv( "SAL_DISABLE_SYNCHRONOUS_PRINTER_DETECTION" ); + if( ! pNoSyncDetection || ! *pNoSyncDetection ) + { + // #i62663# synchronize possible asynchronouse printer detection now + rManager.checkPrintersChanged( true ); + } + ::std::vector< OUString > aPrinters; + rManager.listPrinters( aPrinters ); + + for (auto const& printer : aPrinters) + { + const PrinterInfo& rInfo( rManager.getPrinterInfo(printer) ); + // create new entry + std::unique_ptr pInfo(new SalPrinterQueueInfo); + pInfo->maPrinterName = printer; + pInfo->maDriver = rInfo.m_aDriverName; + pInfo->maLocation = rInfo.m_aLocation; + pInfo->maComment = rInfo.m_aComment; + + OUString sPdfDir; + if (getPdfDir(rInfo, sPdfDir)) + pInfo->maLocation = sPdfDir; + + pList->Add( std::move(pInfo) ); + } +} + +void SalGenericInstance::GetPrinterQueueState( SalPrinterQueueInfo* ) +{ + mbPrinterInit = true; +} + +OUString SalGenericInstance::GetDefaultPrinter() +{ + mbPrinterInit = true; + PrinterInfoManager& rManager( PrinterInfoManager::get() ); + return rManager.getDefaultPrinter(); +} + +PspSalInfoPrinter::PspSalInfoPrinter() +{ +} + +PspSalInfoPrinter::~PspSalInfoPrinter() +{ +} + +void PspSalInfoPrinter::InitPaperFormats( const ImplJobSetup* ) +{ + m_aPaperFormats.clear(); + m_bPapersInit = true; + + if( !m_aJobData.m_pParser ) + return; + + const PPDKey* pKey = m_aJobData.m_pParser->getKey( "PageSize" ); + if( pKey ) + { + int nValues = pKey->countValues(); + for( int i = 0; i < nValues; i++ ) + { + const PPDValue* pValue = pKey->getValue( i ); + int nWidth = 0, nHeight = 0; + m_aJobData.m_pParser->getPaperDimension( pValue->m_aOption, nWidth, nHeight ); + PaperInfo aInfo(PtTo10Mu( nWidth ), PtTo10Mu( nHeight )); + m_aPaperFormats.push_back( aInfo ); + } + } +} + +int PspSalInfoPrinter::GetLandscapeAngle( const ImplJobSetup* ) +{ + return 900; +} + +SalGraphics* PspSalInfoPrinter::AcquireGraphics() +{ + // return a valid pointer only once + // the reasoning behind this is that we could have different + // SalGraphics that can run in multiple threads + // (future plans) + SalGraphics* pRet = nullptr; + if( ! m_pGraphics ) + { + m_pGraphics = GetGenericInstance()->CreatePrintGraphics(); + m_pGraphics->Init(&m_aJobData); + pRet = m_pGraphics.get(); + } + return pRet; +} + +void PspSalInfoPrinter::ReleaseGraphics( SalGraphics* pGraphics ) +{ + if( m_pGraphics.get() == pGraphics ) + { + m_pGraphics.reset(); + } +} + +bool PspSalInfoPrinter::Setup( weld::Window* pFrame, ImplJobSetup* pJobSetup ) +{ + if( ! pFrame || ! pJobSetup ) + return false; + + PrinterInfoManager& rManager = PrinterInfoManager::get(); + + PrinterInfo aInfo( rManager.getPrinterInfo( pJobSetup->GetPrinterName() ) ); + if ( pJobSetup->GetDriverData() ) + { + SetData( JobSetFlags::ALL, pJobSetup ); + JobData::constructFromStreamBuffer( pJobSetup->GetDriverData(), pJobSetup->GetDriverDataLen(), aInfo ); + } + aInfo.m_bPapersizeFromSetup = pJobSetup->GetPapersizeFromSetup(); + aInfo.meSetupMode = pJobSetup->GetPrinterSetupMode(); + + if (SetupPrinterDriver(pFrame, aInfo)) + { + pJobSetup->SetDriverData( nullptr, 0 ); + + sal_uInt32 nBytes; + std::unique_ptr pBuffer; + aInfo.getStreamBuffer( pBuffer, nBytes ); + pJobSetup->SetDriverData( std::move(pBuffer), nBytes ); + + // copy everything to job setup + copyJobDataToJobSetup( pJobSetup, aInfo ); + JobData::constructFromStreamBuffer( pJobSetup->GetDriverData(), pJobSetup->GetDriverDataLen(), m_aJobData ); + return true; + } + return false; +} + +// This function gets the driver data and puts it into pJobSetup +// If pJobSetup->GetDriverData() is NOT NULL, then the independent +// data should be merged into the driver data +// If pJobSetup->GetDriverData() IS NULL, then the driver defaults +// should be merged into the independent data +bool PspSalInfoPrinter::SetPrinterData( ImplJobSetup* pJobSetup ) +{ + if( pJobSetup->GetDriverData() ) + return SetData( JobSetFlags::ALL, pJobSetup ); + + copyJobDataToJobSetup( pJobSetup, m_aJobData ); + + return true; +} + +// This function merges the independent driver data +// and sets the new independent data in pJobSetup +// Only the data must be changed, where the bit +// in nGetDataFlags is set +bool PspSalInfoPrinter::SetData( + JobSetFlags nSetDataFlags, + ImplJobSetup* pJobSetup ) +{ + JobData aData; + JobData::constructFromStreamBuffer( pJobSetup->GetDriverData(), pJobSetup->GetDriverDataLen(), aData ); + + if( aData.m_pParser ) + { + const PPDKey* pKey; + const PPDValue* pValue; + + // merge orientation if necessary + if( nSetDataFlags & JobSetFlags::ORIENTATION ) + aData.m_eOrientation = pJobSetup->GetOrientation() == Orientation::Landscape ? orientation::Landscape : orientation::Portrait; + + // merge papersize if necessary + if( nSetDataFlags & JobSetFlags::PAPERSIZE ) + { + OUString aPaper; + + if( pJobSetup->GetPaperFormat() == PAPER_USER ) + aPaper = aData.m_pParser->matchPaper( + TenMuToPt( pJobSetup->GetPaperWidth() ), + TenMuToPt( pJobSetup->GetPaperHeight() ), + &aData.m_eOrientation ); + else + aPaper = OStringToOUString(PaperInfo::toPSName(pJobSetup->GetPaperFormat()), RTL_TEXTENCODING_ISO_8859_1); + + pKey = aData.m_pParser->getKey( "PageSize" ); + pValue = pKey ? pKey->getValueCaseInsensitive( aPaper ) : nullptr; + + // some PPD files do not specify the standard paper names (e.g. C5 instead of EnvC5) + // try to find the correct paper anyway using the size + if( pKey && ! pValue && pJobSetup->GetPaperFormat() != PAPER_USER ) + { + PaperInfo aInfo( pJobSetup->GetPaperFormat() ); + aPaper = aData.m_pParser->matchPaper( + TenMuToPt( aInfo.getWidth() ), + TenMuToPt( aInfo.getHeight() ), + &aData.m_eOrientation ); + pValue = pKey->getValueCaseInsensitive( aPaper ); + } + + if( ! ( pKey && pValue && aData.m_aContext.setValue( pKey, pValue ) == pValue ) ) + return false; + } + + // merge paperbin if necessary + if( nSetDataFlags & JobSetFlags::PAPERBIN ) + { + pKey = aData.m_pParser->getKey( "InputSlot" ); + if( pKey ) + { + int nPaperBin = pJobSetup->GetPaperBin(); + if( nPaperBin >= pKey->countValues() ) + pValue = pKey->getDefaultValue(); + else + pValue = pKey->getValue( pJobSetup->GetPaperBin() ); + + // may fail due to constraints; + // real paper bin is copied back to jobsetup in that case + aData.m_aContext.setValue( pKey, pValue ); + } + // if printer has no InputSlot key simply ignore this setting + // (e.g. SGENPRT has no InputSlot) + } + + // merge duplex if necessary + if( nSetDataFlags & JobSetFlags::DUPLEXMODE ) + { + pKey = aData.m_pParser->getKey( "Duplex" ); + if( pKey ) + { + pValue = nullptr; + switch( pJobSetup->GetDuplexMode() ) + { + case DuplexMode::Off: + pValue = pKey->getValue( "None" ); + if( pValue == nullptr ) + pValue = pKey->getValue( "SimplexNoTumble" ); + break; + case DuplexMode::ShortEdge: + pValue = pKey->getValue( "DuplexTumble" ); + break; + case DuplexMode::LongEdge: + pValue = pKey->getValue( "DuplexNoTumble" ); + break; + case DuplexMode::Unknown: + default: + pValue = nullptr; + break; + } + if( ! pValue ) + pValue = pKey->getDefaultValue(); + aData.m_aContext.setValue( pKey, pValue ); + } + } + aData.m_bPapersizeFromSetup = pJobSetup->GetPapersizeFromSetup(); + + m_aJobData = aData; + copyJobDataToJobSetup( pJobSetup, aData ); + return true; + } + + return false; +} + +void PspSalInfoPrinter::GetPageInfo( + const ImplJobSetup* pJobSetup, + tools::Long& rOutWidth, tools::Long& rOutHeight, + Point& rPageOffset, + Size& rPaperSize ) +{ + if( ! pJobSetup ) + return; + + JobData aData; + JobData::constructFromStreamBuffer( pJobSetup->GetDriverData(), pJobSetup->GetDriverDataLen(), aData ); + + // get the selected page size + if( !aData.m_pParser ) + return; + + + OUString aPaper; + int width, height; + int left = 0, top = 0, right = 0, bottom = 0; + int nDPI = aData.m_aContext.getRenderResolution(); + + if( aData.m_eOrientation == psp::orientation::Portrait ) + { + aData.m_aContext.getPageSize( aPaper, width, height ); + aData.m_pParser->getMargins( aPaper, left, right, top, bottom ); + } + else + { + aData.m_aContext.getPageSize( aPaper, height, width ); + aData.m_pParser->getMargins( aPaper, top, bottom, right, left ); + } + + rPaperSize.setWidth( width * nDPI / 72 ); + rPaperSize.setHeight( height * nDPI / 72 ); + rPageOffset.setX( left * nDPI / 72 ); + rPageOffset.setY( top * nDPI / 72 ); + rOutWidth = ( width - left - right ) * nDPI / 72; + rOutHeight = ( height - top - bottom ) * nDPI / 72; + +} + +sal_uInt16 PspSalInfoPrinter::GetPaperBinCount( const ImplJobSetup* pJobSetup ) +{ + if( ! pJobSetup ) + return 0; + + JobData aData; + JobData::constructFromStreamBuffer( pJobSetup->GetDriverData(), pJobSetup->GetDriverDataLen(), aData ); + + const PPDKey* pKey = aData.m_pParser ? aData.m_pParser->getKey( "InputSlot" ): nullptr; + return pKey ? pKey->countValues() : 0; +} + +OUString PspSalInfoPrinter::GetPaperBinName( const ImplJobSetup* pJobSetup, sal_uInt16 nPaperBin ) +{ + JobData aData; + JobData::constructFromStreamBuffer( pJobSetup->GetDriverData(), pJobSetup->GetDriverDataLen(), aData ); + + if( aData.m_pParser ) + { + const PPDKey* pKey = aData.m_pParser ? aData.m_pParser->getKey( "InputSlot" ): nullptr; + if( ! pKey || nPaperBin >= o3tl::make_unsigned(pKey->countValues()) ) + return aData.m_pParser->getDefaultInputSlot(); + const PPDValue* pValue = pKey->getValue( nPaperBin ); + if( pValue ) + return aData.m_pParser->translateOption( pKey->getKey(), pValue->m_aOption ); + } + + return OUString(); +} + +sal_uInt32 PspSalInfoPrinter::GetCapabilities( const ImplJobSetup* pJobSetup, PrinterCapType nType ) +{ + switch( nType ) + { + case PrinterCapType::SupportDialog: + return 1; + case PrinterCapType::Copies: + return 0xffff; + case PrinterCapType::CollateCopies: + { + // PPDs don't mention the number of possible collated copies. + // so let's guess as many as we want ? + return 0xffff; + } + case PrinterCapType::SetOrientation: + return 1; + case PrinterCapType::SetPaperSize: + return 1; + case PrinterCapType::SetPaper: + return 0; + case PrinterCapType::Fax: + { + // see if the PPD contains the fax4CUPS "Dial" option and that it's not set + // to "manually" + JobData aData = PrinterInfoManager::get().getPrinterInfo(pJobSetup->GetPrinterName()); + if( pJobSetup->GetDriverData() ) + JobData::constructFromStreamBuffer( pJobSetup->GetDriverData(), pJobSetup->GetDriverDataLen(), aData ); + const PPDKey* pKey = aData.m_pParser ? aData.m_pParser->getKey("Dial") : nullptr; + const PPDValue* pValue = pKey ? aData.m_aContext.getValue(pKey) : nullptr; + if (pValue && !pValue->m_aOption.equalsIgnoreAsciiCase("Manually")) + return 1; + return 0; + } + + case PrinterCapType::PDF: + return 1; + case PrinterCapType::ExternalDialog: + return PrinterInfoManager::get().checkFeatureToken( pJobSetup->GetPrinterName(), "external_dialog" ) ? 1 : 0; + case PrinterCapType::UsePullModel: + return 1; + default: break; + } + return 0; +} + +/* + * SalPrinter + */ +PspSalPrinter::PspSalPrinter( SalInfoPrinter* pInfoPrinter ) + : m_pInfoPrinter( pInfoPrinter ) +{ +} + +PspSalPrinter::~PspSalPrinter() +{ +} + +bool PspSalPrinter::StartJob( + const OUString* /*pFileName*/, + const OUString& /*rJobName*/, + const OUString& /*rAppName*/, + sal_uInt32 /*nCopies*/, + bool /*bCollate*/, + bool /*bDirect*/, + ImplJobSetup* /*pJobSetup*/ ) +{ + OSL_FAIL( "should never be called" ); + return false; +} + +bool PspSalPrinter::EndJob() +{ + GetSalInstance()->jobEndedPrinterUpdate(); + return true; +} + +SalGraphics* PspSalPrinter::StartPage( ImplJobSetup*, bool ) +{ + OSL_FAIL( "should never be called" ); + return nullptr; +} + +void PspSalPrinter::EndPage() +{ + OSL_FAIL( "should never be called" ); +} + +namespace { + +struct PDFNewJobParameters +{ + Size maPageSize; + sal_uInt16 mnPaperBin; + + PDFNewJobParameters( const Size& i_rSize = Size(), + sal_uInt16 i_nPaperBin = 0xffff ) + : maPageSize( i_rSize ), mnPaperBin( i_nPaperBin ) {} + + bool operator==(const PDFNewJobParameters& rComp ) const + { + const tools::Long nRotatedWidth = rComp.maPageSize.Height(); + const tools::Long nRotatedHeight = rComp.maPageSize.Width(); + Size aCompLSSize(nRotatedWidth, nRotatedHeight); + return + (maPageSize == rComp.maPageSize || maPageSize == aCompLSSize) + && mnPaperBin == rComp.mnPaperBin + ; + } + + bool operator!=(const PDFNewJobParameters& rComp) const + { + return ! operator==(rComp); + } +}; + +struct PDFPrintFile +{ + OUString maTmpURL; + PDFNewJobParameters maParameters; + + PDFPrintFile( OUString i_URL, const PDFNewJobParameters& i_rNewParameters ) + : maTmpURL(std::move( i_URL )) + , maParameters( i_rNewParameters ) {} +}; + +} + +bool PspSalPrinter::StartJob( const OUString* i_pFileName, const OUString& i_rJobName, const OUString& i_rAppName, + ImplJobSetup* i_pSetupData, vcl::PrinterController& i_rController ) +{ + SAL_INFO( "vcl.unx.print", "StartJob with controller: pFilename = " << (i_pFileName ? *i_pFileName : "") ); + // reset IsLastPage + i_rController.setLastPage( false ); + // is this a fax device + bool bFax = m_pInfoPrinter->GetCapabilities(i_pSetupData, PrinterCapType::Fax) == 1; + + // update job data + if( i_pSetupData ) + JobData::constructFromStreamBuffer( i_pSetupData->GetDriverData(), i_pSetupData->GetDriverDataLen(), m_aJobData ); + + // possibly create one job for collated output + int nCopies = i_rController.getPrinter()->GetCopyCount(); + bool bCollate = i_rController.getPrinter()->IsCollateCopy(); + bool bSinglePrintJobs = i_rController.getPrinter()->IsSinglePrintJobs(); + + // notify start of real print job + i_rController.jobStarted(); + + // setup PDFWriter context + vcl::PDFWriter::PDFWriterContext aContext; + aContext.Version = vcl::PDFWriter::PDFVersion::PDF_1_4; + aContext.Tagged = false; + aContext.DocumentLocale = Application::GetSettings().GetLanguageTag().getLocale(); + aContext.ColorMode = i_rController.getPrinter()->GetPrinterOptions().IsConvertToGreyscales() + ? vcl::PDFWriter::DrawGreyscale : vcl::PDFWriter::DrawColor; + + // prepare doc info + aContext.DocumentInfo.Title = i_rJobName; + aContext.DocumentInfo.Creator = i_rAppName; + aContext.DocumentInfo.Producer = i_rAppName; + + // define how we handle metafiles in PDFWriter + vcl::PDFWriter::PlayMetafileContext aMtfContext; + aMtfContext.m_bOnlyLosslessCompression = true; + + std::shared_ptr xWriter; + std::vector< PDFPrintFile > aPDFFiles; + VclPtr xPrinter( i_rController.getPrinter() ); + int nAllPages = i_rController.getFilteredPageCount(); + i_rController.createProgressDialog(); + bool bAborted = false; + PDFNewJobParameters aLastParm; + + aContext.DPIx = xPrinter->GetDPIX(); + aContext.DPIy = xPrinter->GetDPIY(); + for( int nPage = 0; nPage < nAllPages && ! bAborted; nPage++ ) + { + if( nPage == nAllPages-1 ) + i_rController.setLastPage( true ); + + // get the page's metafile + GDIMetaFile aPageFile; + vcl::PrinterController::PageSize aPageSize = i_rController.getFilteredPageFile( nPage, aPageFile ); + if( i_rController.isProgressCanceled() ) + { + bAborted = true; + if( nPage != nAllPages-1 ) + { + i_rController.createProgressDialog(); + i_rController.setLastPage( true ); + i_rController.getFilteredPageFile( nPage, aPageFile ); + } + } + else + { + xPrinter->SetMapMode( MapMode( MapUnit::Map100thMM ) ); + xPrinter->SetPaperSizeUser( aPageSize.aSize ); + PDFNewJobParameters aNewParm(xPrinter->GetPaperSize(), xPrinter->GetPaperBin()); + + // create PDF writer on demand + // either on first page + // or on paper format change - cups does not support multiple paper formats per job (yet?) + // so we need to start a new job to get a new paper format from the printer + // orientation switches (that is switch of height and width) is handled transparently by CUPS + if( ! xWriter || + (aNewParm != aLastParm && ! i_pFileName ) ) + { + if( xWriter ) + { + xWriter->Emit(); + } + // produce PDF file + OUString aPDFUrl; + if( i_pFileName ) + aPDFUrl = *i_pFileName; + else + osl_createTempFile( nullptr, nullptr, &aPDFUrl.pData ); + // normalize to file URL + if( !comphelper::isFileUrl(aPDFUrl) ) + { + // this is not a file URL, but it should + // form it into an osl friendly file URL + OUString aTmp; + osl_getFileURLFromSystemPath( aPDFUrl.pData, &aTmp.pData ); + aPDFUrl = aTmp; + } + // save current file and paper format + aLastParm = aNewParm; + aPDFFiles.emplace_back( aPDFUrl, aNewParm ); + // update context + aContext.URL = aPDFUrl; + + // create and initialize PDFWriter + xWriter = std::make_shared( aContext, uno::Reference< beans::XMaterialHolder >() ); + } + + xWriter->NewPage( TenMuToPt( aNewParm.maPageSize.Width() ), + TenMuToPt( aNewParm.maPageSize.Height() ), + vcl::PDFWriter::Orientation::Portrait ); + + xWriter->PlayMetafile( aPageFile, aMtfContext ); + } + } + + // emit the last file + if( xWriter ) + xWriter->Emit(); + + // handle collate, copy count and multiple jobs correctly + int nOuterJobs = 1; + if( bSinglePrintJobs ) + { + nOuterJobs = nCopies; + m_aJobData.m_nCopies = 1; + } + else + { + if( bCollate ) + { + if (aPDFFiles.size() == 1 && xPrinter->HasSupport(PrinterSupport::CollateCopy)) + { + m_aJobData.setCollate( true ); + m_aJobData.m_nCopies = nCopies; + } + else + { + nOuterJobs = nCopies; + m_aJobData.m_nCopies = 1; + } + } + else + { + m_aJobData.setCollate( false ); + m_aJobData.m_nCopies = nCopies; + } + } + + std::vector aFaxNumbers; + + // check for fax numbers + if (!bAborted && bFax) + { + aFaxNumbers = getFaxNumbers(); + bAborted = aFaxNumbers.empty(); + } + + bool bSuccess(true); + // spool files + if( ! i_pFileName && ! bAborted ) + { + do + { + OUString sFaxNumber; + if (!aFaxNumbers.empty()) + { + sFaxNumber = aFaxNumbers.back(); + aFaxNumbers.pop_back(); + } + + bool bFirstJob = true; + for( int nCurJob = 0; nCurJob < nOuterJobs; nCurJob++ ) + { + for( size_t i = 0; i < aPDFFiles.size(); i++ ) + { + oslFileHandle pFile = nullptr; + osl_openFile( aPDFFiles[i].maTmpURL.pData, &pFile, osl_File_OpenFlag_Read ); + if (pFile && (osl_setFilePos(pFile, osl_Pos_Absolut, 0) == osl_File_E_None)) + { + std::vector< char > buffer( 0x10000, 0 ); + // update job data with current page size + Size aPageSize( aPDFFiles[i].maParameters.maPageSize ); + m_aJobData.setPaper( TenMuToPt( aPageSize.Width() ), TenMuToPt( aPageSize.Height() ) ); + // update job data with current paperbin + m_aJobData.setPaperBin( aPDFFiles[i].maParameters.mnPaperBin ); + + // spool current file + FILE* fp = PrinterInfoManager::get().startSpool(xPrinter->GetName(), i_rController.isDirectPrint()); + if( fp ) + { + sal_uInt64 nBytesRead = 0; + do + { + osl_readFile( pFile, buffer.data(), buffer.size(), &nBytesRead ); + if( nBytesRead > 0 ) + { + size_t nBytesWritten = fwrite(buffer.data(), 1, nBytesRead, fp); + OSL_ENSURE(nBytesRead == nBytesWritten, "short write"); + if (nBytesRead != nBytesWritten) + break; + } + } while( nBytesRead == buffer.size() ); + OUStringBuffer aBuf( i_rJobName.getLength() + 8 ); + aBuf.append( i_rJobName ); + if( i > 0 || nCurJob > 0 ) + { + aBuf.append( ' ' ); + aBuf.append( sal_Int32( i + nCurJob * aPDFFiles.size() ) ); + } + bSuccess &= + PrinterInfoManager::get().endSpool(xPrinter->GetName(), aBuf.makeStringAndClear(), fp, m_aJobData, bFirstJob, sFaxNumber); + bFirstJob = false; + } + } + osl_closeFile( pFile ); + } + } + } + while (!aFaxNumbers.empty()); + } + + // job has been spooled + i_rController.setJobState( bAborted + ? view::PrintableState_JOB_ABORTED + : (bSuccess ? view::PrintableState_JOB_SPOOLED + : view::PrintableState_JOB_SPOOLING_FAILED)); + + // clean up the temporary PDF files + if( ! i_pFileName || bAborted ) + { + for(PDFPrintFile & rPDFFile : aPDFFiles) + { + osl_removeFile( rPDFFile.maTmpURL.pData ); + SAL_INFO( "vcl.unx.print", "removed print PDF file " << rPDFFile.maTmpURL ); + } + } + + return true; +} + +namespace { + +class PrinterUpdate +{ + static Idle* pPrinterUpdateIdle; + static int nActiveJobs; + + static void doUpdate(); + DECL_STATIC_LINK( PrinterUpdate, UpdateTimerHdl, Timer*, void ); +public: + static void update(SalGenericInstance const &rInstance); + static void jobEnded(); +}; + +} + +Idle* PrinterUpdate::pPrinterUpdateIdle = nullptr; +int PrinterUpdate::nActiveJobs = 0; + +void PrinterUpdate::doUpdate() +{ + ::psp::PrinterInfoManager& rManager( ::psp::PrinterInfoManager::get() ); + SalGenericInstance *pInst = GetGenericInstance(); + if( pInst && rManager.checkPrintersChanged( false ) ) + pInst->PostPrintersChanged(); +} + +IMPL_STATIC_LINK_NOARG( PrinterUpdate, UpdateTimerHdl, Timer*, void ) +{ + if( nActiveJobs < 1 ) + { + doUpdate(); + delete pPrinterUpdateIdle; + pPrinterUpdateIdle = nullptr; + } + else + pPrinterUpdateIdle->Start(); +} + +void PrinterUpdate::update(SalGenericInstance const &rInstance) +{ + if( Application::GetSettings().GetMiscSettings().GetDisablePrinting() ) + return; + + if( ! rInstance.isPrinterInit() ) + { + // #i45389# start background printer detection + psp::PrinterInfoManager::get(); + return; + } + + if( nActiveJobs < 1 ) + doUpdate(); + else if( ! pPrinterUpdateIdle ) + { + pPrinterUpdateIdle = new Idle("PrinterUpdateTimer"); + pPrinterUpdateIdle->SetPriority( TaskPriority::LOWEST ); + pPrinterUpdateIdle->SetInvokeHandler( LINK( nullptr, PrinterUpdate, UpdateTimerHdl ) ); + pPrinterUpdateIdle->Start(); + } +} + +void SalGenericInstance::updatePrinterUpdate() +{ + PrinterUpdate::update(*this); +} + +void PrinterUpdate::jobEnded() +{ + nActiveJobs--; + if( nActiveJobs < 1 ) + { + if( pPrinterUpdateIdle ) + { + pPrinterUpdateIdle->Stop(); + delete pPrinterUpdateIdle; + pPrinterUpdateIdle = nullptr; + doUpdate(); + } + } +} + +void SalGenericInstance::jobEndedPrinterUpdate() +{ + PrinterUpdate::jobEnded(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/print/genpspgraphics.cxx b/vcl/unx/generic/print/genpspgraphics.cxx new file mode 100644 index 0000000000..66bf452474 --- /dev/null +++ b/vcl/unx/generic/print/genpspgraphics.cxx @@ -0,0 +1,195 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include + +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace psp; + +/******************************************************* + * GenPspGraphics + *******************************************************/ + +GenPspGraphics::GenPspGraphics() + : m_pJobData( nullptr ) + , m_aTextRenderImpl(m_aCairoCommon) + , m_pBackend(new SvpGraphicsBackend(m_aCairoCommon)) +{ +} + +void GenPspGraphics::Init(psp::JobData* pJob) +{ + m_pJobData = pJob; + SetLayout( SalLayoutFlags::NONE ); +} + +GenPspGraphics::~GenPspGraphics() +{ + ReleaseFonts(); +} + +void GenPspGraphics::GetResolution( sal_Int32 &rDPIX, sal_Int32 &rDPIY ) +{ + if (m_pJobData != nullptr) + { + int x = m_pJobData->m_aContext.getRenderResolution(); + + rDPIX = x; + rDPIY = x; + } +} + +void GenPspGraphics::DrawTextLayout(const GenericSalLayout& rLayout) +{ + m_aTextRenderImpl.DrawTextLayout(rLayout, *this); +} + +FontCharMapRef GenPspGraphics::GetFontCharMap() const +{ + return m_aTextRenderImpl.GetFontCharMap(); +} + +bool GenPspGraphics::GetFontCapabilities(vcl::FontCapabilities &rFontCapabilities) const +{ + return m_aTextRenderImpl.GetFontCapabilities(rFontCapabilities); +} + +void GenPspGraphics::SetFont(LogicalFontInstance *pFontInstance, int nFallbackLevel) +{ + m_aTextRenderImpl.SetFont(pFontInstance, nFallbackLevel); +} + +void GenPspGraphics::SetTextColor( Color nColor ) +{ + m_aTextRenderImpl.SetTextColor(nColor); +} + +bool GenPspGraphics::AddTempDevFont( vcl::font::PhysicalFontCollection* pFontCollection, + const OUString& rFileURL, + const OUString& rFontName ) +{ + return m_aTextRenderImpl.AddTempDevFont(pFontCollection, rFileURL, rFontName); +} + +void GenPspGraphics::GetDevFontList( vcl::font::PhysicalFontCollection *pFontCollection ) +{ + m_aTextRenderImpl.GetDevFontList(pFontCollection); +} + +void GenPspGraphics::ClearDevFontCache() +{ + m_aTextRenderImpl.ClearDevFontCache(); +} + +void GenPspGraphics::GetFontMetric(FontMetricDataRef& rxFontMetric, int nFallbackLevel) +{ + m_aTextRenderImpl.GetFontMetric(rxFontMetric, nFallbackLevel); +} + +std::unique_ptr GenPspGraphics::GetTextLayout(int nFallbackLevel) +{ + return m_aTextRenderImpl.GetTextLayout(nFallbackLevel); +} + +namespace vcl +{ + const char* getLangBoost() + { + const char* pLangBoost; + const LanguageType eLang = Application::GetSettings().GetUILanguageTag().getLanguageType(); + if (eLang == LANGUAGE_JAPANESE) + pLangBoost = "jan"; + else if (MsLangId::isKorean(eLang)) + pLangBoost = "kor"; + else if (MsLangId::isSimplifiedChinese(eLang)) + pLangBoost = "zhs"; + else if (MsLangId::isTraditionalChinese(eLang)) + pLangBoost = "zht"; + else + pLangBoost = nullptr; + return pLangBoost; + } +} + +SystemGraphicsData GenPspGraphics::GetGraphicsData() const +{ + return SystemGraphicsData(); +} + +#if ENABLE_CAIRO_CANVAS + +bool GenPspGraphics::SupportsCairo() const +{ + return false; +} + +cairo::SurfaceSharedPtr GenPspGraphics::CreateSurface(const cairo::CairoSurfaceSharedPtr& /*rSurface*/) const +{ + return cairo::SurfaceSharedPtr(); +} + +cairo::SurfaceSharedPtr GenPspGraphics::CreateSurface(const OutputDevice& /*rRefDevice*/, int /*x*/, int /*y*/, int /*width*/, int /*height*/) const +{ + return cairo::SurfaceSharedPtr(); +} + +cairo::SurfaceSharedPtr GenPspGraphics::CreateBitmapSurface(const OutputDevice& /*rRefDevice*/, const BitmapSystemData& /*rData*/, const Size& /*rSize*/) const +{ + return cairo::SurfaceSharedPtr(); +} + +css::uno::Any GenPspGraphics::GetNativeSurfaceHandle(cairo::SurfaceSharedPtr& /*rSurface*/, const basegfx::B2ISize& /*rSize*/) const +{ + return css::uno::Any(); +} + +#endif // ENABLE_CAIRO_CANVAS + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/print/prtsetup.cxx b/vcl/unx/generic/print/prtsetup.cxx new file mode 100644 index 0000000000..4b0a63c1b3 --- /dev/null +++ b/vcl/unx/generic/print/prtsetup.cxx @@ -0,0 +1,465 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "prtsetup.hxx" +#include +#include + +#include + +using namespace psp; + +void RTSDialog::insertAllPPDValues(weld::ComboBox& rBox, const PPDParser* pParser, const PPDKey* pKey ) +{ + if( ! pKey || ! pParser ) + return; + + const PPDValue* pValue = nullptr; + OUString aOptionText; + + for (int i = 0; i < pKey->countValues(); ++i) + { + pValue = pKey->getValue( i ); + if (pValue->m_bCustomOption) + continue; + aOptionText = pParser->translateOption( pKey->getKey(), pValue->m_aOption) ; + + OUString sId(weld::toId(pValue)); + int nCurrentPos = rBox.find_id(sId); + if( m_aJobData.m_aContext.checkConstraints( pKey, pValue ) ) + { + if (nCurrentPos == -1) + rBox.append(sId, aOptionText); + } + else + { + if (nCurrentPos != -1) + rBox.remove(nCurrentPos); + } + } + pValue = m_aJobData.m_aContext.getValue( pKey ); + if (pValue && !pValue->m_bCustomOption) + { + OUString sId(weld::toId(pValue)); + int nPos = rBox.find_id(sId); + if (nPos != -1) + rBox.set_active(nPos); + } +} + +/* + * RTSDialog + */ + +RTSDialog::RTSDialog(const PrinterInfo& rJobData, weld::Window* pParent) + : GenericDialogController(pParent, "vcl/ui/printerpropertiesdialog.ui", "PrinterPropertiesDialog") + , m_aJobData(rJobData) + , m_bDataModified(false) + , m_xTabControl(m_xBuilder->weld_notebook("tabcontrol")) + , m_xOKButton(m_xBuilder->weld_button("ok")) + , m_xCancelButton(m_xBuilder->weld_button("cancel")) + , m_xPaperPage(new RTSPaperPage(m_xTabControl->get_page("paper"), this)) + , m_xDevicePage(new RTSDevicePage(m_xTabControl->get_page("device"), this)) +{ + OUString aTitle(m_xDialog->get_title()); + m_xDialog->set_title(aTitle.replaceAll("%s", m_aJobData.m_aPrinterName)); + + m_xTabControl->connect_enter_page( LINK( this, RTSDialog, ActivatePage ) ); + m_xOKButton->connect_clicked( LINK( this, RTSDialog, ClickButton ) ); + m_xCancelButton->connect_clicked( LINK( this, RTSDialog, ClickButton ) ); + ActivatePage(m_xTabControl->get_current_page_ident()); +} + +RTSDialog::~RTSDialog() +{ +} + +IMPL_LINK(RTSDialog, ActivatePage, const OUString&, rPage, void) +{ + if (rPage == "paper") + m_xPaperPage->update(); +} + +IMPL_LINK( RTSDialog, ClickButton, weld::Button&, rButton, void ) +{ + if (&rButton == m_xOKButton.get()) + { + // refresh the changed values + if (m_xPaperPage) + { + // orientation + m_aJobData.m_eOrientation = m_xPaperPage->getOrientation() == 0 ? + orientation::Portrait : orientation::Landscape; + // assume use of paper size from printer setup if the user + // got here via File > Printer Settings ... + if ( m_aJobData.meSetupMode == PrinterSetupMode::DocumentGlobal ) + m_aJobData.m_bPapersizeFromSetup = true; + } + if( m_xDevicePage ) + { + m_aJobData.m_nColorDepth = m_xDevicePage->getDepth(); + m_aJobData.m_nColorDevice = m_xDevicePage->getColorDevice(); + } + m_xDialog->response(RET_OK); + } + else if (&rButton == m_xCancelButton.get()) + m_xDialog->response(RET_CANCEL); +} + +/* + * RTSPaperPage + */ + +RTSPaperPage::RTSPaperPage(weld::Widget* pPage, RTSDialog* pDialog) + : m_xBuilder(Application::CreateBuilder(pPage, "vcl/ui/printerpaperpage.ui")) + , m_pParent(pDialog) + , m_xContainer(m_xBuilder->weld_widget("PrinterPaperPage")) + , m_xCbFromSetup(m_xBuilder->weld_check_button("papersizefromsetup")) + , m_xPaperText(m_xBuilder->weld_label("paperft")) + , m_xPaperBox(m_xBuilder->weld_combo_box("paperlb")) + , m_xOrientText(m_xBuilder->weld_label("orientft")) + , m_xOrientBox(m_xBuilder->weld_combo_box("orientlb")) + , m_xDuplexText(m_xBuilder->weld_label("duplexft")) + , m_xDuplexBox(m_xBuilder->weld_combo_box("duplexlb")) + , m_xSlotText(m_xBuilder->weld_label("slotft")) + , m_xSlotBox(m_xBuilder->weld_combo_box("slotlb")) +{ + //PrinterPaperPage + m_xPaperBox->connect_changed( LINK( this, RTSPaperPage, SelectHdl ) ); + m_xOrientBox->connect_changed( LINK( this, RTSPaperPage, SelectHdl ) ); + m_xDuplexBox->connect_changed( LINK( this, RTSPaperPage, SelectHdl ) ); + m_xSlotBox->connect_changed( LINK( this, RTSPaperPage, SelectHdl ) ); + m_xCbFromSetup->connect_toggled( LINK( this, RTSPaperPage, CheckBoxHdl ) ); + + update(); +} + +RTSPaperPage::~RTSPaperPage() +{ +} + +void RTSPaperPage::update() +{ + const PPDKey* pKey = nullptr; + + // orientation + m_xOrientBox->set_active(m_pParent->m_aJobData.m_eOrientation == orientation::Portrait ? 0 : 1); + + // duplex + if( m_pParent->m_aJobData.m_pParser && + (pKey = m_pParent->m_aJobData.m_pParser->getKey( "Duplex" )) ) + { + m_pParent->insertAllPPDValues( *m_xDuplexBox, m_pParent->m_aJobData.m_pParser, pKey ); + } + else + { + m_xDuplexText->set_sensitive( false ); + m_xDuplexBox->set_sensitive( false ); + } + + // paper + if( m_pParent->m_aJobData.m_pParser && + (pKey = m_pParent->m_aJobData.m_pParser->getKey( "PageSize" )) ) + { + m_pParent->insertAllPPDValues( *m_xPaperBox, m_pParent->m_aJobData.m_pParser, pKey ); + } + else + { + m_xPaperText->set_sensitive( false ); + m_xPaperBox->set_sensitive( false ); + } + + // input slots + if( m_pParent->m_aJobData.m_pParser && + (pKey = m_pParent->m_aJobData.m_pParser->getKey( "InputSlot" )) ) + { + m_pParent->insertAllPPDValues( *m_xSlotBox, m_pParent->m_aJobData.m_pParser, pKey ); + } + else + { + m_xSlotText->set_sensitive( false ); + m_xSlotBox->set_sensitive( false ); + } + + if ( m_pParent->m_aJobData.meSetupMode != PrinterSetupMode::SingleJob ) + return; + + m_xCbFromSetup->show(); + + if ( m_pParent->m_aJobData.m_bPapersizeFromSetup ) + m_xCbFromSetup->set_active(m_pParent->m_aJobData.m_bPapersizeFromSetup); + // disable those, unless user wants to use papersize from printer prefs + // as they have no influence on what's going to be printed anyway + else + { + m_xPaperText->set_sensitive( false ); + m_xPaperBox->set_sensitive( false ); + m_xOrientText->set_sensitive( false ); + m_xOrientBox->set_sensitive( false ); + } +} + +IMPL_LINK( RTSPaperPage, SelectHdl, weld::ComboBox&, rBox, void ) +{ + const PPDKey* pKey = nullptr; + if( &rBox == m_xPaperBox.get() ) + { + if( m_pParent->m_aJobData.m_pParser ) + pKey = m_pParent->m_aJobData.m_pParser->getKey( "PageSize" ); + } + else if( &rBox == m_xDuplexBox.get() ) + { + if( m_pParent->m_aJobData.m_pParser ) + pKey = m_pParent->m_aJobData.m_pParser->getKey( "Duplex" ); + } + else if( &rBox == m_xSlotBox.get() ) + { + if( m_pParent->m_aJobData.m_pParser ) + pKey = m_pParent->m_aJobData.m_pParser->getKey( "InputSlot" ); + } + else if( &rBox == m_xOrientBox.get() ) + { + m_pParent->m_aJobData.m_eOrientation = m_xOrientBox->get_active() == 0 ? orientation::Portrait : orientation::Landscape; + } + if( pKey ) + { + PPDValue* pValue = weld::fromId(rBox.get_active_id()); + m_pParent->m_aJobData.m_aContext.setValue( pKey, pValue ); + update(); + } + + m_pParent->SetDataModified( true ); +} + +IMPL_LINK_NOARG(RTSPaperPage, CheckBoxHdl, weld::Toggleable&, void) +{ + bool bFromSetup = m_xCbFromSetup->get_active(); + m_pParent->m_aJobData.m_bPapersizeFromSetup = bFromSetup; + m_xPaperText->set_sensitive(bFromSetup); + m_xPaperBox->set_sensitive(bFromSetup); + m_xOrientText->set_sensitive(bFromSetup); + m_xOrientBox->set_sensitive(bFromSetup); + m_pParent->SetDataModified(true); +} + +/* + * RTSDevicePage + */ +RTSDevicePage::RTSDevicePage(weld::Widget* pPage, RTSDialog* pParent) + : m_xBuilder(Application::CreateBuilder(pPage, "vcl/ui/printerdevicepage.ui")) + , m_pCustomValue(nullptr) + , m_pParent(pParent) + , m_xContainer(m_xBuilder->weld_widget("PrinterDevicePage")) + , m_xPPDKeyBox(m_xBuilder->weld_tree_view("options")) + , m_xPPDValueBox(m_xBuilder->weld_tree_view("values")) + , m_xCustomEdit(m_xBuilder->weld_entry("custom")) + , m_xSpaceBox(m_xBuilder->weld_combo_box("colorspace")) + , m_xDepthBox(m_xBuilder->weld_combo_box("colordepth")) + , m_aReselectCustomIdle("RTSDevicePage m_aReselectCustomIdle") +{ + m_aReselectCustomIdle.SetInvokeHandler(LINK(this, RTSDevicePage, ImplHandleReselectHdl)); + + m_xPPDKeyBox->set_size_request(m_xPPDKeyBox->get_approximate_digit_width() * 32, + m_xPPDKeyBox->get_height_rows(12)); + + m_xCustomEdit->connect_changed(LINK(this, RTSDevicePage, ModifyHdl)); + + m_xPPDKeyBox->connect_changed( LINK( this, RTSDevicePage, SelectHdl ) ); + m_xPPDValueBox->connect_changed( LINK( this, RTSDevicePage, SelectHdl ) ); + + m_xSpaceBox->connect_changed(LINK(this, RTSDevicePage, ComboChangedHdl)); + m_xDepthBox->connect_changed(LINK(this, RTSDevicePage, ComboChangedHdl)); + + switch( m_pParent->m_aJobData.m_nColorDevice ) + { + case 0: + m_xSpaceBox->set_active(0); + break; + case 1: + m_xSpaceBox->set_active(1); + break; + case -1: + m_xSpaceBox->set_active(2); + break; + } + + if (m_pParent->m_aJobData.m_nColorDepth == 8) + m_xDepthBox->set_active(0); + else if (m_pParent->m_aJobData.m_nColorDepth == 24) + m_xDepthBox->set_active(1); + + // fill ppd boxes + if( !m_pParent->m_aJobData.m_pParser ) + return; + + for( int i = 0; i < m_pParent->m_aJobData.m_pParser->getKeys(); i++ ) + { + const PPDKey* pKey = m_pParent->m_aJobData.m_pParser->getKey( i ); + + // skip options already shown somewhere else + // also skip options from the "InstallableOptions" PPD group + // Options in that group define hardware features that are not + // job-specific and should better be handled in the system-wide + // printer configuration. Keyword is defined in PPD specification + // (version 4.3), section 5.4. + if( pKey->isUIKey() && + pKey->getKey() != "PageSize" && + pKey->getKey() != "InputSlot" && + pKey->getKey() != "PageRegion" && + pKey->getKey() != "Duplex" && + pKey->getGroup() != "InstallableOptions") + { + OUString aEntry( m_pParent->m_aJobData.m_pParser->translateKey( pKey->getKey() ) ); + m_xPPDKeyBox->append(weld::toId(pKey), aEntry); + } + } +} + +RTSDevicePage::~RTSDevicePage() +{ +} + +sal_uLong RTSDevicePage::getDepth() const +{ + sal_uInt16 nSelectPos = m_xDepthBox->get_active(); + if (nSelectPos == 0) + return 8; + else + return 24; +} + +sal_uLong RTSDevicePage::getColorDevice() const +{ + sal_uInt16 nSelectPos = m_xSpaceBox->get_active(); + switch (nSelectPos) + { + case 0: + return 0; + case 1: + return 1; + case 2: + return -1; + } + return 0; +} + +IMPL_LINK(RTSDevicePage, ModifyHdl, weld::Entry&, rEdit, void) +{ + if (m_pCustomValue) + { + // tdf#123734 Custom PPD option values are a CUPS extension to PPDs and the user-set value + // needs to be prefixed with "Custom." in order to be processed properly + m_pCustomValue->m_aCustomOption = "Custom." + rEdit.get_text(); + m_pCustomValue->m_bCustomOptionSetViaApp = true; + } +} + +IMPL_LINK( RTSDevicePage, SelectHdl, weld::TreeView&, rBox, void ) +{ + if (&rBox == m_xPPDKeyBox.get()) + { + const PPDKey* pKey = weld::fromId(m_xPPDKeyBox->get_selected_id()); + FillValueBox( pKey ); + } + else if (&rBox == m_xPPDValueBox.get()) + { + const PPDKey* pKey = weld::fromId(m_xPPDKeyBox->get_selected_id()); + const PPDValue* pValue = weld::fromId(m_xPPDValueBox->get_selected_id()); + if (pKey && pValue) + { + m_pParent->m_aJobData.m_aContext.setValue( pKey, pValue ); + ValueBoxChanged(pKey); + } + } + m_pParent->SetDataModified( true ); +} + +IMPL_LINK_NOARG( RTSDevicePage, ComboChangedHdl, weld::ComboBox&, void ) +{ + m_pParent->SetDataModified( true ); +} + +void RTSDevicePage::FillValueBox( const PPDKey* pKey ) +{ + m_xPPDValueBox->clear(); + m_xCustomEdit->hide(); + + if( ! pKey ) + return; + + const PPDValue* pValue = nullptr; + for( int i = 0; i < pKey->countValues(); i++ ) + { + pValue = pKey->getValue( i ); + if( m_pParent->m_aJobData.m_aContext.checkConstraints( pKey, pValue ) && + m_pParent->m_aJobData.m_pParser ) + { + OUString aEntry; + if (pValue->m_bCustomOption) + aEntry = VclResId(SV_PRINT_CUSTOM_TXT); + else + aEntry = m_pParent->m_aJobData.m_pParser->translateOption( pKey->getKey(), pValue->m_aOption); + m_xPPDValueBox->append(weld::toId(pValue), aEntry); + } + } + pValue = m_pParent->m_aJobData.m_aContext.getValue( pKey ); + m_xPPDValueBox->select_id(weld::toId(pValue)); + + ValueBoxChanged(pKey); +} + +IMPL_LINK_NOARG(RTSDevicePage, ImplHandleReselectHdl, Timer*, void) +{ + //in case selected entry is now not visible select it again to scroll it into view + m_xPPDValueBox->select(m_xPPDValueBox->get_selected_index()); +} + +void RTSDevicePage::ValueBoxChanged( const PPDKey* pKey ) +{ + const PPDValue* pValue = m_pParent->m_aJobData.m_aContext.getValue(pKey); + if (pValue->m_bCustomOption) + { + m_pCustomValue = pValue; + m_pParent->m_aJobData.m_aContext.setValue(pKey, pValue); + // don't show the "Custom." prefix in the UI, s.a. comment in ModifyHdl + m_xCustomEdit->set_text(m_pCustomValue->m_aCustomOption.replaceFirst("Custom.", "")); + m_xCustomEdit->show(); + m_aReselectCustomIdle.Start(); + } + else + m_xCustomEdit->hide(); +} + +int SetupPrinterDriver(weld::Window* pParent, ::psp::PrinterInfo& rJobData) +{ + int nRet = 0; + RTSDialog aDialog(rJobData, pParent); + + // return 0 if cancel was pressed or if the data + // weren't modified, 1 otherwise + if (aDialog.run() != RET_CANCEL) + { + rJobData = aDialog.getSetup(); + nRet = aDialog.GetDataModified() ? 1 : 0; + } + + return nRet; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/print/prtsetup.hxx b/vcl/unx/generic/print/prtsetup.hxx new file mode 100644 index 0000000000..1a32eb3318 --- /dev/null +++ b/vcl/unx/generic/print/prtsetup.hxx @@ -0,0 +1,135 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include +#include +#include +#include + +class RTSPaperPage; +class RTSDevicePage; + +class RTSDialog : public weld::GenericDialogController +{ + friend class RTSPaperPage; + friend class RTSDevicePage; + + ::psp::PrinterInfo m_aJobData; + + bool m_bDataModified; + + // controls + std::unique_ptr m_xTabControl; + std::unique_ptr m_xOKButton; + std::unique_ptr m_xCancelButton; + + // pages + std::unique_ptr m_xPaperPage; + std::unique_ptr m_xDevicePage; + + DECL_LINK(ActivatePage, const OUString&, void); + DECL_LINK(ClickButton, weld::Button&, void); + + // helper functions + void insertAllPPDValues(weld::ComboBox&, const psp::PPDParser*, const psp::PPDKey*); + +public: + RTSDialog(const ::psp::PrinterInfo& rJobData, weld::Window* pParent); + virtual ~RTSDialog() override; + + const ::psp::PrinterInfo& getSetup() const { return m_aJobData; } + + void SetDataModified(bool bModified) { m_bDataModified = bModified; } + bool GetDataModified() const { return m_bDataModified; } +}; + +class RTSPaperPage +{ +private: + std::unique_ptr m_xBuilder; + + RTSDialog* m_pParent; + + std::unique_ptr m_xContainer; + + std::unique_ptr m_xCbFromSetup; + + std::unique_ptr m_xPaperText; + std::unique_ptr m_xPaperBox; + + std::unique_ptr m_xOrientText; + std::unique_ptr m_xOrientBox; + + std::unique_ptr m_xDuplexText; + std::unique_ptr m_xDuplexBox; + + std::unique_ptr m_xSlotText; + std::unique_ptr m_xSlotBox; + + DECL_LINK(SelectHdl, weld::ComboBox&, void); + DECL_LINK(CheckBoxHdl, weld::Toggleable&, void); + +public: + RTSPaperPage(weld::Widget* pPage, RTSDialog* pDialog); + ~RTSPaperPage(); + + void update(); + + sal_Int32 getOrientation() const { return m_xOrientBox->get_active(); } +}; + +class RTSDevicePage +{ +private: + std::unique_ptr m_xBuilder; + + const psp::PPDValue* m_pCustomValue; + RTSDialog* m_pParent; + + std::unique_ptr m_xContainer; + std::unique_ptr m_xPPDKeyBox; + std::unique_ptr m_xPPDValueBox; + std::unique_ptr m_xCustomEdit; + + std::unique_ptr m_xSpaceBox; + std::unique_ptr m_xDepthBox; + + void FillValueBox(const ::psp::PPDKey*); + void ValueBoxChanged(const ::psp::PPDKey*); + + Idle m_aReselectCustomIdle; + + DECL_LINK(SelectHdl, weld::TreeView&, void); + DECL_LINK(ModifyHdl, weld::Entry&, void); + DECL_LINK(ComboChangedHdl, weld::ComboBox&, void); + DECL_LINK(ImplHandleReselectHdl, Timer*, void); + +public: + RTSDevicePage(weld::Widget* pPage, RTSDialog* pDialog); + ~RTSDevicePage(); + + sal_uLong getDepth() const; + sal_uLong getColorDevice() const; +}; + +int SetupPrinterDriver(weld::Window* pParent, ::psp::PrinterInfo& rJobData); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/print/psheader.ps b/vcl/unx/generic/print/psheader.ps new file mode 100644 index 0000000000..49f0f51011 --- /dev/null +++ b/vcl/unx/generic/print/psheader.ps @@ -0,0 +1,363 @@ +% +% This file is part of the LibreOffice project. +% +% This Source Code Form is subject to the terms of the Mozilla Public +% License, v. 2.0. If a copy of the MPL was not distributed with this +% file, You can obtain one at http://mozilla.org/MPL/2.0/. +% +% This file incorporates work covered by the following license notice: +% +% Licensed to the Apache Software Foundation (ASF) under one or more +% contributor license agreements. See the NOTICE file distributed +% with this work for additional information regarding copyright +% ownership. The ASF licenses this file to you under the Apache +% License, Version 2.0 (the "License"); you may not use this file +% except in compliance with the License. You may obtain a copy of +% the License at http://www.apache.org/licenses/LICENSE-2.0 . +% + +% This is an "unobsfucated version of postscript header" in printerjob.cxx. It +% was probably kept separate for the comments, but it is not used in itself +% and probably was not kept in sync with the actual header. + +% +% +% readpath +% +% The intention of readpath is to save disk space since the vcl clip region routines +% produce a huge amount of lineto/moveto commands +% +% The principal idea is to maintain the current point on stack and to provide only deltas +% in the command. These deltas are added to the current point. The new point is used for +% the lineto and moveto command and saved on stack for the next command. +% +% pathdict implements binary/hex representation of lineto and moveto commands. +% The command consists of a 1byte opcode to switch between lineto and moveto and the size +% of the following delta-x and delta-y values. The opcode is read with /rcmd, the two +% coordinates are read with /rhex. The whole command is executed with /xcmd +% +% + +/pathdict dup 8 dict def load +begin + + % the command is of the bit format cxxyy + % with c=0 meaning lineto + % c=1 meaning moveto + % xx is a 2bit value for the number of bytes for x position + % yy is the same for y, values are off by one: 00 means 1; 11 means 4 ! + % the command has been added to 'A' to be always in the ascii character + % range. the command is followed by 2*xx + 2*yy hexchars. + % '~' denotes the special case of EOD + /rcmd { + { + currentfile 1 string readstring % s bool + pop % s + 0 get % s[0] + % --- check whether s[0] is CR, LF ... + dup 32 gt % s > ' ' ? then read on + { exit } + { pop } + ifelse + } + loop + + dup 126 eq { pop exit } if % -- Exit loop if cmd is '~' + 65 sub % cmd=s[0]-'A' + % -- Separate yy bits + dup 16#3 and 1 add % cmd yy + % -- Separate xx bits + exch % yy cmd + dup 16#C and -2 bitshift + 16#3 and 1 add exch % yy xx cmd + % -- Separate command bit + 16#10 and 16#10 eq % yy xx bool + 3 1 roll exch % bool xx yy + } def + + % length rhex -- reads a signed hex value of given length + % the left most bit of char 0 is considered as the sign (0 means '+', 1 means '-') + % the rest of the bits is considered to be the abs value. Please note that this + % does not match the C binary representation of integers + /rhex { + dup 1 sub exch % l-1 l + currentfile exch string readhexstring % l-1 substring[l] bool + pop + dup 0 get dup % l-1 s s[0] s[0] + % -- Extract the sign + 16#80 and 16#80 eq dup % l-1 s s[0] sign=- sign=- + % -- Mask out the sign bit and put value back + 3 1 roll % l-1 s sign=- s[0] sign=- + { 16#7f and } if % l-1 s sign=- +s[0] + 2 index 0 % l-1 s sign=- +s[0] s 0 + 3 -1 roll put % l-1 s sign=- s 0 +s[0] + % -- Read loop: add to prev sum, mul with 256 + 3 1 roll 0 % sign=- l-1 s Sum=0 + 0 1 5 -1 roll % sign=- s Sum=0 0 1 l-1 + { % sign=- s Sum idx + 2 index exch % sign=- s Sum s idx + get % sign=- s Sum s[idx] + add 256 mul % sign=- s Sum=(s[idx]+Sum)*256 + } + for + % -- mul was once too often, weave in the sign + 256 div % sign=- s Sum/256 + exch pop % sign=- Sum/256 + exch { neg } if % (sign=- ? -Sum : Sum) + } def + + % execute a single command, the former x and y position is already on stack + % only offsets are read from cmdstring + /xcmd { % x y + rcmd % x y bool wx wy + exch rhex % x y bool wy Dx + exch rhex % x y bool Dx Dy + exch 5 -1 roll % y bool Dy Dx x + add exch % y bool X Dy + 4 -1 roll add % bool X Y + 1 index 1 index % bool X Y X Y + 5 -1 roll % X Y X Y bool + { moveto } + { lineto } + ifelse % X Y + } def +end + +/readpath +{ + 0 0 % push initial-x initial-y + pathdict begin + { xcmd } loop + end + pop pop % pop final-x final-y +} def + +% +% +% if languagelevel is not in the systemdict then its level 1 interpreter: +% provide compatibility routines +% +% + +systemdict /languagelevel known not +{ + % string numarray xxshow - + % does only work for single byte fonts + /xshow { + exch dup % a s s + length 0 1 % a s l(s) 1 1 + 3 -1 roll 1 sub % a s 0 1 l(s)-1 + { % a s idx + dup % a s idx idx + % -- extract the delta offset + 3 index exch get % a s idx a[idx] + % -- extract the character + exch % a s a[idx] idx + 2 index exch get % a s a[idx] s[idx] + % -- create a tmp string for show + 1 string dup 0 % a s a[idx] s[idx] s1 s1 0 + 4 -1 roll % a s a[idx] s1 s1 0 s[idx] + put % a s a[idx] s1 + % -- store the current point + currentpoint 3 -1 roll % a s a[idx] x y s1 + % -- draw the character + show % a s a[idx] x y + % -- move to the offset + moveto 0 rmoveto % a s + } + for + pop pop % - + } def + + % x y width height rectfill + % x y width height rectshow + % in contrast to the languagelevel 2 operator + % they use and change the currentpath + /rectangle { + 4 -2 roll % width height x y + moveto % width height + 1 index 0 rlineto % width height % rmoveto(width, 0) + 0 exch rlineto % width % rmoveto(0, height) + neg 0 rlineto % - % rmoveto(-width, 0) + closepath + } def + + /rectfill { rectangle fill } def + /rectstroke { rectangle stroke } def +} +if + +% -- small test program +% 75 75 moveto /Times-Roman findfont 12 scalefont setfont +% <292a2b2c2d2e2f30313233343536373839> +% [5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 5] xshow <21>[0] xshow +% showpage + +% +% +% shortcuts for image header with compression +% +% + +/psp_lzwfilter { + currentfile /ASCII85Decode filter /LZWDecode filter +} def +/psp_ascii85filter { + currentfile /ASCII85Decode filter +} def +/psp_lzwstring { + psp_lzwfilter 1024 string readstring +} def +/psp_ascii85string { + psp_ascii85filter 1024 string readstring +} def +/psp_imagedict { + /psp_bitspercomponent { + 3 eq + { 1 } + { 8 } + ifelse + } def + /psp_decodearray { + [ [0 1 0 1 0 1] [0 255] [0 1] [0 255] ] exch get + } def + + 7 dict dup + /ImageType 1 put dup + /Width 7 -1 roll put dup + /Height 5 index put dup + /BitsPerComponent 4 index + psp_bitspercomponent put dup + /Decode 5 -1 roll + psp_decodearray put dup + /ImageMatrix [1 0 0 1 0 0] dup + 5 8 -1 roll put put dup + /DataSource 4 -1 roll + 1 eq + { psp_lzwfilter } + { psp_ascii85filter } + ifelse put +} def + + +% +% +% font encoding and reencoding +% +% + +/ISO1252Encoding [ + /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef + /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef + /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef + /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef + /space /exclam /quotedbl /numbersign /dollar /percent /ampersand /quotesingle + /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash + /zero /one /two /three /four /five /six /seven + /eight /nine /colon /semicolon /less /equal /greater /question + /at /A /B /C /D /E /F /G + /H /I /J /K /L /M /N /O + /P /Q /R /S /T /U /V /W + /X /Y /Z /bracketleft /backslash /bracketright /asciicircum /underscore + /grave /a /b /c /d /e /f /g + /h /i /j /k /l /m /n /o + /p /q /r /s /t /u /v /w + /x /y /z /braceleft /bar /braceright /asciitilde /unused + /Euro /unused /quotesinglbase /florin /quotedblbase /ellipsis /dagger /daggerdbl + /circumflex /perthousand /Scaron /guilsinglleft /OE /unused /Zcaron /unused + /unused /quoteleft /quoteright /quotedblleft /quotedblright /bullet /endash /emdash + /tilde /trademark /scaron /guilsinglright /oe /unused /zcaron /Ydieresis + /space /exclamdown /cent /sterling /currency /yen /brokenbar /section + /dieresis /copyright /ordfeminine /guillemotleft /logicalnot /hyphen /registered /macron + /degree /plusminus /twosuperior /threesuperior /acute /mu /paragraph /periodcentered + /cedilla /onesuperior /ordmasculine /guillemotright /onequarter /onehalf /threequarters /questiondown + /Agrave /Aacute /Acircumflex /Atilde /Adieresis /Aring /AE /Ccedilla + /Egrave /Eacute /Ecircumflex /Edieresis /Igrave /Iacute /Icircumflex /Idieresis + /Eth /Ntilde /Ograve /Oacute /Ocircumflex /Otilde /Odieresis /multiply + /Oslash /Ugrave /Uacute /Ucircumflex /Udieresis /Yacute /Thorn /germandbls + /agrave /aacute /acircumflex /atilde /adieresis /aring /ae /ccedilla + /egrave /eacute /ecircumflex /edieresis /igrave /iacute /icircumflex /idieresis + /eth /ntilde /ograve /oacute /ocircumflex /otilde /odieresis /divide + /oslash /ugrave /uacute /ucircumflex /udieresis /yacute /thorn /ydieresis +] def + +% /fontname /encoding psp_findfont +/psp_findfont { + exch dup % encoding fontname fontname + findfont % encoding fontname + dup length dict + begin + { + 1 index /FID ne + { def } + { pop pop } + ifelse + } forall + /Encoding 3 -1 roll def + currentdict + end + /psp_reencodedfont exch definefont +} def + +% bshow shows a text in artificial bold +% this is achieved by first showing the text +% then stroking its outline over it with +% the linewidth set to the second parameter +% usage: (string) num bshow + +/bshow { + currentlinewidth % save current linewidth + 3 1 roll % move it to the last stack position + currentpoint % save the current point + 3 index % copy the string to show + show % show it + moveto % move to the original coordinates again + setlinewidth % set the linewidth + false charpath % create the outline path of the shown string + stroke % and stroke it + setlinewidth % reset the stored linewidth +} def + +% bxshow shows a text with a delta array in artificial bold +% that is it does what bshow does for show +% usage: (string) [deltaarray] num bxshow + +/bxshow { + currentlinewidth % save linewidth + 4 1 roll % move it to the last stack position + setlinewidth % set the new linewidth + exch % exchange string and delta array + dup + length % get length of string + 1 sub % prepare parameters for {} for + 0 1 + 3 -1 roll + { + 1 string % create a string object length 1 + 2 index % get the text + 2 index % get charpos (for index variable) + get % have char value at charpos + 1 index % prepare string for put + exch + 0 + exch + put % put into string of length 1 + dup % duplicate the it + currentpoint % save current position + 3 -1 roll % prepare show + show % show the character + moveto % move back to beginning + currentpoint % save current position + 3 -1 roll % prepare outline path of character + false charpath + stroke % stroke it + moveto % move back + % now move to next point + 2 index % get advance array + exch % get charpos + get % get advance element + 0 rmoveto % advance current position + } for + pop pop % remove string and delta array + setlinewidth % restore linewidth +} def diff --git a/vcl/unx/generic/printer/configuration/README b/vcl/unx/generic/printer/configuration/README new file mode 100644 index 0000000000..c39237a53f --- /dev/null +++ b/vcl/unx/generic/printer/configuration/README @@ -0,0 +1,5 @@ +Contains ppds for use by vcl when not using CUPS + +This is used for the print-to-file functionality. These two PPDs +describe the range of paper sizes and postscript options necessary for +printing to postscript without a configured printer. diff --git a/vcl/unx/generic/printer/configuration/ppds/SGENPRT.PS b/vcl/unx/generic/printer/configuration/ppds/SGENPRT.PS new file mode 100644 index 0000000000..ed5882a593 --- /dev/null +++ b/vcl/unx/generic/printer/configuration/ppds/SGENPRT.PS @@ -0,0 +1,582 @@ +*PPD-Adobe: "4.0" +*% +*% This file is part of the LibreOffice project. +*% +*% This Source Code Form is subject to the terms of the Mozilla Public +*% License, v. 2.0. If a copy of the MPL was not distributed with this +*% file, You can obtain one at http://mozilla.org/MPL/2.0/. +*% +*% This file incorporates work covered by the following license notice: +*% +*% Licensed to the Apache Software Foundation (ASF) under one or more +*% contributor license agreements. See the NOTICE file distributed +*% with this work for additional information regarding copyright +*% ownership. The ASF licenses this file to you under the Apache +*% License, Version 2.0 (the "License")*% you may not use this file +*% except in compliance with the License. You may obtain a copy of +*% the License at http://www.apache.org/licenses/LICENSE-2.0 . +*% + +*% The user must print with a PostScript(R) emulator to non PostScript(R) +*% printers if the system has no specific printer support. This file +*% allows the user to print to most printers without any modification. +*% Standard paper sizes and resolutions are defined. There are some +*% additional definitions for screen or online documents in this file. +*% To print to a PostScript(R) printer, use the specific PPD file. + +*% ===== General ===== + +*FormatVersion: "4.0" +*FileVersion: "1.0" +*LanguageEncoding: ISOLatin1 +*LanguageVersion: English +*PSVersion: "(1) 1" +*Product: "(Generic Printer)" +*ModelName: "Generic Printer" +*NickName: "Generic Printer" +*PCFileName: "SGENPRT.PPD" + + +*% ===== Basic Capabilities and Defaults ===== + +*ColorDevice: True +*DefaultColorSpace: RGB +*LanguageLevel: "2" +*TTRasterizer: Type42 + +*% --- For None Color or old PostScript(R) printers use following lines --- +*% *ColorDevice: False +*% *DefaultColorSpace: Gray +*% *LanguageLevel: "1" + +*FreeVM: "8388608" +*VariablePaperSize: True +*FileSystem: False +*Throughput: "8" +*Password: "0" +*ExitServer: " + count 0 eq % is the password on the stack? + { true } + { dup % potential password + statusdict /checkpassword get exec not + } ifelse + { % if no password or not valid + (WARNING : Cannot perform the exitserver command.) = + (Password supplied is not valid.) = + (Please contact the author of this software.) = flush + quit + } if + serverdict /exitserver get exec +" +*End +*Reset: " + count 0 eq % is the password on the stack? + { true } + { dup % potential password + statusdict /checkpassword get exec not + } ifelse + { % if no password or not valid + (WARNING : Cannot reset printer.) = + (Password supplied is not valid.) = + (Please contact the author of this software.) = flush + quit + } if + serverdict /exitserver get exec + systemdict /quit get exec + (WARNING : Printer Reset Failed.) = flush +" +*End + + +*DefaultResolution: 300dpi + +*ResScreenFreq 72dpi: "60.0" +*ResScreenFreq 144dpi: "60.0" +*ResScreenFreq 300dpi: "60.0" +*ResScreenFreq 360dpi: "60.0" +*ResScreenFreq 600dpi: "60.0" +*ResScreenFreq 720dpi: "60.0" +*ResScreenFreq 1200dpi: "60.0" +*ResScreenFreq 1440dpi: "60.0" +*ResScreenFreq 2400dpi: "60.0" +*ResScreenAngle 72dpi: "45.0" +*ResScreenAngle 144dpi: "45.0" +*ResScreenAngle 300dpi: "45.0" +*ResScreenAngle 360dpi: "45.0" +*ResScreenAngle 600dpi: "45.0" +*ResScreenAngle 720dpi: "45.0" +*ResScreenAngle 1200dpi: "45.0" +*ResScreenAngle 1440dpi: "45.0" +*ResScreenAngle 2400dpi: "45.0" + + +*% ===== Halftone ===== + +*ContoneOnly: False +*DefaultHalftoneType: 1 +*ScreenFreq: "60.0" +*ScreenAngle: "45.0" +*DefaultScreenProc: Dot +*ScreenProc Dot: " + { abs exch abs 2 copy add 1 gt {1 sub dup mul exch 1 sub + dup mul add 1 sub } { dup mul exch dup mul add 1 exch sub } + ifelse } bind +" +*End +*ScreenProc Line: "{ exch pop abs neg } bind" +*ScreenProc Ellipse: " + { abs exch abs 2 copy mul exch 4 mul add 3 sub dup 0 + lt { pop dup mul exch .75 div dup mul add 4 div 1 exch sub } + { dup 1 gt { pop 1 exch sub dup mul exch 1 exch sub .75 div + dup mul add 4 div 1 sub } + { .5 exch sub exch pop exch pop } ifelse } ifelse } bind +" +*End +*ScreenProc Cross: "{ abs exch abs 2 copy gt { exch } if pop neg } bind" + +*DefaultTransfer: Null +*Transfer Null: "{ } bind" +*Transfer Null.Inverse: "{ 1 exch sub } bind" + + +*% ===== Paper ===== + +*OpenUI *PageSize: PickOne +*OrderDependency: 30 AnySetup *PageSize +*DefaultPageSize: Letter +*PageSize A0: "<> setpagedevice" +*PageSize A1: "<> setpagedevice" +*PageSize A2: "<> setpagedevice" +*PageSize A3: "<> setpagedevice" +*PageSize A4: "<> setpagedevice" +*PageSize A5: "<> setpagedevice" +*PageSize A6: "<> setpagedevice" +*PageSize B4: "<> setpagedevice" +*PageSize B5: "<> setpagedevice" +*PageSize B6: "<> setpagedevice" +*PageSize Legal/US Legal: "<> setpagedevice" +*PageSize Letter/US Letter: "<> setpagedevice" +*PageSize Executive: "<> setpagedevice" +*PageSize Statement: "<> setpagedevice" +*PageSize Tabloid/US Tabloid: "<> setpagedevice" +*PageSize Ledger/Ledger Landscape: "<> setpagedevice" +*PageSize AnsiC/US C: "<> setpagedevice" +*PageSize AnsiD/US D: "<> setpagedevice" +*PageSize AnsiE/US E: "<> setpagedevice" +*PageSize ARCHA/ARCH A: "<> setpagedevice" +*PageSize ARCHB/ARCH B: "<> setpagedevice" +*PageSize ARCHC/ARCH C: "<> setpagedevice" +*PageSize ARCHD/ARCH D: "<> setpagedevice" +*PageSize ARCHE/ARCH E: "<> setpagedevice" +*PageSize EnvMonarch/Monarch Envelope: "<> setpagedevice" +*PageSize EnvDL/DL Envelope: "<> setpagedevice" +*PageSize EnvC4/C4 Envelope: "<> setpagedevice" +*PageSize EnvC5/C5 Envelope: "<> setpagedevice" +*PageSize EnvC6/C6 Envelope: "<> setpagedevice" +*PageSize Env10/C10 Envelope: "<> setpagedevice" +*PageSize EnvC65/C65 Envelope: "<> setpagedevice" +*PageSize Folio: "<> setpagedevice" +*?PageSize: " + save + currentpagedevice /PageSize get aload pop + 2 copy gt {exch} if + (Unknown) + 32 dict + dup [2384 3370] (A0) put + dup [1684 2384] (A1) put + dup [1191 1684] (A2) put + dup [842 1191] (A3) put + dup [595 842] (A4) put + dup [420 595] (A5) put + dup [297 420] (A6) put + dup [728 1032] (B4) put + dup [516 729] (B5) put + dup [363 516] (B6) put + dup [612 1008] (Legal) put + dup [612 792] (Letter) put + dup [522 756] (Executive) put + dup [396 612] (Statement) put + dup [792 1224] (Tabloid) put + dup [1224 792] (Ledger) put + dup [1224 1584] (AnsiC) put + dup [1584 2448] (AnsiD) put + dup [2448 3168] (AnsiE) put + dup [648 864] (ARCHA) put + dup [864 1296] (ARCHB) put + dup [1296 1728] (ARCHC) put + dup [1728 2592] (ARCHD) put + dup [2592 3456] (ARCHE) put + dup [279 540] (EnvMonarch) put + dup [312 624] (EnvDL) put + dup [649 918] (EnvC4) put + dup [459 649] (EnvC5) put + dup [323 459] (EnvC6) put + dup [297 684] (Env10) put + dup [323 649] (EnvC65) put + dup [595 935] (Folio) put + { exch aload pop 4 index sub abs 5 le exch + 5 index sub abs 5 le and + { exch pop exit } { pop } ifelse + } bind forall + = flush pop pop + restore +" +*End +*CloseUI: *PageSize + +*OpenUI *PageRegion: PickOne +*OrderDependency: 40 AnySetup *PageRegion +*DefaultPageRegion: Letter +*PageRegion A0: "<> setpagedevice" +*PageRegion A1: "<> setpagedevice" +*PageRegion A2: "<> setpagedevice" +*PageRegion A3: "<> setpagedevice" +*PageRegion A4: "<> setpagedevice" +*PageRegion A5: "<> setpagedevice" +*PageRegion A6: "<> setpagedevice" +*PageRegion B4: "<> setpagedevice" +*PageRegion B5: "<> setpagedevice" +*PageRegion B6: "<> setpagedevice" +*PageRegion Legal/US Legal: "<> setpagedevice" +*PageRegion Letter/US Letter: "<> setpagedevice" +*PageRegion Executive: "<> setpagedevice" +*PageRegion Statement: "<> setpagedevice" +*PageRegion Tabloid/US Tabloid: "<> setpagedevice" +*PageRegion Ledger/Ledger Landscape: "<> setpagedevice" +*PageRegion AnsiC/US C: "<> setpagedevice" +*PageRegion AnsiD/US D: "<> setpagedevice" +*PageRegion AnsiE/US E: "<> setpagedevice" +*PageRegion ARCHA/ARCH A: "<> setpagedevice" +*PageRegion ARCHB/ARCH B: "<> setpagedevice" +*PageRegion ARCHC/ARCH C: "<> setpagedevice" +*PageRegion ARCHD/ARCH D: "<> setpagedevice" +*PageRegion ARCHE/ARCH E: "<> setpagedevice" +*PageRegion EnvMonarch/Monarch Envelope: "<> setpagedevice" +*PageRegion EnvDL/DL Envelope: "<> setpagedevice" +*PageRegion EnvC4/C4 Envelope: "<> setpagedevice" +*PageRegion EnvC5/C5 Envelope: "<> setpagedevice" +*PageRegion EnvC6/C6 Envelope: "<> setpagedevice" +*PageRegion Env10/C10 Envelope: "<> setpagedevice" +*PageRegion EnvC65/C65 Envelope: "<> setpagedevice" +*PageRegion Folio: "<> setpagedevice" +*CloseUI: *PageRegion + +*DefaultImageableArea: Letter +*ImageableArea A0: "0 0 2384 3370" +*ImageableArea A1: "0 0 1684 2384" +*ImageableArea A2: "0 0 1191 1684" +*ImageableArea A3: "18 18 824 1173" +*ImageableArea A4: "18 18 577 824" +*ImageableArea A5: "18 18 402 577" +*ImageableArea A6: "18 18 279 402" +*ImageableArea B4: "18 18 710 1014" +*ImageableArea B5: "18 18 498 711" +*ImageableArea B6: "18 18 345 498" +*ImageableArea Legal: "18 18 594 990" +*ImageableArea Letter: "18 18 594 774" +*ImageableArea Executive: "18 18 504 738" +*ImageableArea Statement: "18 18 378 594" +*ImageableArea Tabloid: "18 18 774 1206" +*ImageableArea Ledger: "18 18 1206 774" +*ImageableArea AnsiC: "0 0 1224 1584" +*ImageableArea AnsiD: "0 0 1584 2448" +*ImageableArea AnsiE: "0 0 2448 3168" +*ImageableArea ARCHA: "0 0 648 864" +*ImageableArea ARCHB: "0 0 864 1296" +*ImageableArea ARCHC: "0 0 1296 1728" +*ImageableArea ARCHD: "0 0 1728 2592" +*ImageableArea ARCHE: "0 0 2592 3456" +*ImageableArea EnvMonarch: "0 0 279 540" +*ImageableArea EnvDL: "0 0 312 624" +*ImageableArea EnvC4: "0 0 649 918" +*ImageableArea EnvC5: "0 0 459 649" +*ImageableArea EnvC6: "0 0 323 459" +*ImageableArea Env10: "0 0 297 684" +*ImageableArea EnvC65: "0 0 323 649" +*ImageableArea Folio: "0 0 595 935" + +*DefaultPaperDimension: Letter +*PaperDimension A0: "2384 3370" +*PaperDimension A1: "1684 2384" +*PaperDimension A2: "1191 1684" +*PaperDimension A3: "842 1191" +*PaperDimension A4: "595 842" +*PaperDimension A5: "420 595" +*PaperDimension A6: "297 420" +*PaperDimension B4: "728 1032" +*PaperDimension B5: "516 729" +*PaperDimension B6: "363 516" +*PaperDimension Legal: "612 1008" +*PaperDimension Letter: "612 792" +*PaperDimension Executive: "522 756" +*PaperDimension Statement: "396 612" +*PaperDimension Tabloid: "792 1224" +*PaperDimension Ledger: "1224 792" +*PaperDimension AnsiC: "1224 1584" +*PaperDimension AnsiD: "1584 2448" +*PaperDimension AnsiE: "2448 3168" +*PaperDimension ARCHA: "648 864" +*PaperDimension ARCHB: "864 1296" +*PaperDimension ARCHC: "1296 1728" +*PaperDimension ARCHD: "1728 2592" +*PaperDimension ARCHE: "2592 3456" +*PaperDimension EnvMonarch: "279 540" +*PaperDimension EnvDL: "312 624" +*PaperDimension EnvC4: "649 918" +*PaperDimension EnvC5: "459 649" +*PaperDimension EnvC6: "323 459" +*PaperDimension Env10: "297 684" +*PaperDimension EnvC65: "323 649" +*PaperDimension Folio: "595 935" + +*% ===== Duplex ===== +*OpenUI *Duplex/Duplex: PickOne +*OrderDependency: 30 AnySetup *Duplex +*DefaultDuplex: Simplex +*Duplex Simplex: "" +*Duplex None/Off: " +<> +>> setpagedevice" +*Duplex DuplexNoTumble/Long edge:" +<> +>> setpagedevice" +*Duplex DuplexTumble/Short edge:" +<> +>> setpagedevice" +*End +*CloseUI: *Duplex + +*% ===== ManualFeed === +*OpenUI *ManualFeed/Manual Feed: Boolean +*OrderDependency: 15 AnySetup *ManualFeed +*DefaultManualFeed: False +*ManualFeed False: " +<< /ManualFeed false /Policies << /ManualFeed 1 >> >> setpagedevice" +*ManualFeed True: " +<< /ManualFeed true /Policies << /ManualFeed 1 >> >> setpagedevice" +*End +*CloseUI: *ManualFeed + +*% ===== Fonts ===== + +*DefaultFont: Courier +*Font AvantGarde-Book: Standard "(001.002)" Standard ROM +*Font AvantGarde-BookOblique: Standard "(001.000)" Standard ROM +*Font AvantGarde-Demi: Standard "(001.000)" Standard ROM +*Font AvantGarde-DemiOblique: Standard "(001.000)" Standard ROM +*Font Bookman-Demi: Standard "(001.000)" Standard ROM +*Font Bookman-DemiItalic: Standard "(001.000)" Standard ROM +*Font Bookman-Light: Standard "(001.000)" Standard ROM +*Font Bookman-LightItalic: Standard "(001.000)" Standard ROM +*Font Courier: Standard "(001.000)" Standard ROM +*Font Courier-Bold: Standard "(001.000)" Standard ROM +*Font Courier-BoldOblique: Standard "(001.000)" Standard ROM +*Font Courier-Oblique: Standard "(001.000)" Standard ROM +*Font Helvetica: Standard "(001.000)" Standard ROM +*Font Helvetica-Bold: Standard "(001.000)" Standard ROM +*Font Helvetica-BoldOblique: Standard "(001.000)" Standard ROM +*Font Helvetica-Narrow: Standard "(001.000)" Standard ROM +*Font Helvetica-Narrow-Bold: Standard "(001.000)" Standard ROM +*Font Helvetica-Narrow-BoldOblique: Standard "(001.000)" Standard ROM +*Font Helvetica-Narrow-Oblique: Standard "(001.000)" Standard ROM +*Font Helvetica-Oblique: Standard "(001.000)" Standard ROM +*Font NewCenturySchlbk-Bold: Standard "(001.000)" Standard ROM +*Font NewCenturySchlbk-BoldItalic: Standard "(001.000)" Standard ROM +*Font NewCenturySchlbk-Italic: Standard "(001.000)" Standard ROM +*Font NewCenturySchlbk-Roman: Standard "(001.000)" Standard ROM +*Font Palatino-Bold: Standard "(001.000)" Standard ROM +*Font Palatino-BoldItalic: Standard "(001.000)" Standard ROM +*Font Palatino-Italic: Standard "(001.000)" Standard ROM +*Font Palatino-Roman: Standard "(001.000)" Standard ROM +*Font Symbol: Special "(001.001)" Special ROM +*Font Times-Bold: Standard "(001.000)" Standard ROM +*Font Times-BoldItalic: Standard "(001.000)" Standard ROM +*Font Times-Italic: Standard "(001.000)" Standard ROM +*Font Times-Roman: Standard "(001.000)" Standard ROM +*Font ZapfChancery-MediumItalic: Standard "(001.000)" Standard ROM +*Font ZapfDingbats: Special "(001.000)" Special ROM +*?FontQuery: " + save + { + count 1 gt + { + exch dup 127 string cvs (/) print print (:) print + /Font resourcestatus {pop pop (Yes)} {(No)} ifelse = + } + { exit } ifelse + } bind loop + (*) = flush + restore +" +*End + +*?FontList: " + save + (*) {cvn ==} 128 string /Font resourceforall + (*) = flush + restore +" +*End + + +*% === Printer Messages === + +*Message: "%%[ exitserver: permanent state may be changed ]%%" +*Message: "%%[ Flushing: rest of job (to end-of-file) will be ignored ]%%" +*Message: "\FontName\ not found, using Courier" + +*% Status (format: %%[ status: %%] ) +*Status: "idle" +*Status: "busy" +*Status: "waiting" +*Status: "printing" +*Status: "PrinterError: timeout, clearing printer" +*Status: "PrinterError: paper entry misfeed" +*Status: "PrinterError: warming up" +*Status: "PrinterError: service call" +*Status: "PrinterError: no toner cartridge" +*Status: "PrinterError: no paper tray" +*Status: "PrinterError: cover open" +*Status: "PrinterError: resetting printer" +*Status: "PrinterError: out of paper" +*Status: "PrinterError: timeout" +*Status: "PrinterError: manual feed timeout" + +*% Input Sources (format: %%[ status: ; source: ]%% ) + +*% Printer Error (format: %%[ PrinterError: ]%%) +*PrinterError: "timeout, clearing printer" +*PrinterError: "paper entry misfeed" +*PrinterError: "warming up" +*PrinterError: "service call" +*PrinterError: "no toner cartridge" +*PrinterError: "no paper tray" +*PrinterError: "cover open" +*PrinterError: "resetting printer" +*PrinterError: "out of paper" +*PrinterError: "timeout" +*PrinterError: "manual feed timeout" + + +*% ===== Color Separation ===== + +*DefaultColorSep: ProcessBlack.60lpi.300dpi/60 lpi / 300 dpi +*InkName: ProcessBlack/Process Black +*InkName: CustomColor/Custom Color +*InkName: ProcessCyan/Process Cyan +*InkName: ProcessMagenta/Process Magenta +*InkName: ProcessYellow/Process Yellow + +*% --- For 60 lpi / 72 dpi --- +*ColorSepScreenAngle ProcessBlack.60lpi.72dpi/60 lpi / 72 dpi: "45" +*ColorSepScreenAngle CustomColor.60lpi.72dpi/60 lpi / 72 dpi: "45" +*ColorSepScreenAngle ProcessCyan.60lpi.72dpi/60 lpi / 72 dpi: "15" +*ColorSepScreenAngle ProcessMagenta.60lpi.72dpi/60 lpi / 72 dpi: "75" +*ColorSepScreenAngle ProcessYellow.60lpi.72dpi/60 lpi / 72 dpi: "0" +*ColorSepScreenFreq ProcessBlack.60lpi.72dpi/60 lpi / 72 dpi: "60" +*ColorSepScreenFreq CustomColor.60lpi.72dpi/60 lpi / 72 dpi: "60" +*ColorSepScreenFreq ProcessCyan.60lpi.72dpi/60 lpi / 72 dpi: "60" +*ColorSepScreenFreq ProcessMagenta.60lpi.72dpi/60 lpi / 72 dpi: "60" +*ColorSepScreenFreq ProcessYellow.60lpi.72dpi/60 lpi / 72 dpi: "60" + +*% --- For 60 lpi / 144 dpi --- +*ColorSepScreenAngle ProcessBlack.60lpi.144dpi/60 lpi / 144 dpi: "45" +*ColorSepScreenAngle CustomColor.60lpi.144dpi/60 lpi / 144 dpi: "45" +*ColorSepScreenAngle ProcessCyan.60lpi.144dpi/60 lpi / 144 dpi: "15" +*ColorSepScreenAngle ProcessMagenta.60lpi.144dpi/60 lpi / 144 dpi: "75" +*ColorSepScreenAngle ProcessYellow.60lpi.144dpi/60 lpi / 144 dpi: "0" +*ColorSepScreenFreq ProcessBlack.60lpi.144dpi/60 lpi / 144 dpi: "60" +*ColorSepScreenFreq CustomColor.60lpi.144dpi/60 lpi / 144 dpi: "60" +*ColorSepScreenFreq ProcessCyan.60lpi.144dpi/60 lpi / 144 dpi: "60" +*ColorSepScreenFreq ProcessMagenta.60lpi.144dpi/60 lpi / 144 dpi: "60" +*ColorSepScreenFreq ProcessYellow.60lpi.144dpi/60 lpi / 144 dpi: "60" + +*% --- For 60 lpi / 300 dpi --- +*ColorSepScreenAngle ProcessBlack.60lpi.300dpi/60 lpi / 300 dpi: "45" +*ColorSepScreenAngle CustomColor.60lpi.300dpi/60 lpi / 300 dpi: "45" +*ColorSepScreenAngle ProcessCyan.60lpi.300dpi/60 lpi / 300 dpi: "15" +*ColorSepScreenAngle ProcessMagenta.60lpi.300dpi/60 lpi / 300 dpi: "75" +*ColorSepScreenAngle ProcessYellow.60lpi.300dpi/60 lpi / 300 dpi: "0" +*ColorSepScreenFreq ProcessBlack.60lpi.300dpi/60 lpi / 300 dpi: "60" +*ColorSepScreenFreq CustomColor.60lpi.300dpi/60 lpi / 300 dpi: "60" +*ColorSepScreenFreq ProcessCyan.60lpi.300dpi/60 lpi / 300 dpi: "60" +*ColorSepScreenFreq ProcessMagenta.60lpi.300dpi/60 lpi / 300 dpi: "60" +*ColorSepScreenFreq ProcessYellow.60lpi.300dpi/60 lpi / 300 dpi: "60" + +*% --- For 60 lpi / 360 dpi --- +*ColorSepScreenAngle ProcessBlack.60lpi.360dpi/60 lpi / 360 dpi: "45" +*ColorSepScreenAngle CustomColor.60lpi.360dpi/60 lpi / 360 dpi: "45" +*ColorSepScreenAngle ProcessCyan.60lpi.360dpi/60 lpi / 360 dpi: "15" +*ColorSepScreenAngle ProcessMagenta.60lpi.360dpi/60 lpi / 360 dpi: "75" +*ColorSepScreenAngle ProcessYellow.60lpi.360dpi/60 lpi / 360 dpi: "0" +*ColorSepScreenFreq ProcessBlack.60lpi.360dpi/60 lpi / 360 dpi: "60" +*ColorSepScreenFreq CustomColor.60lpi.360dpi/60 lpi / 360 dpi: "60" +*ColorSepScreenFreq ProcessCyan.60lpi.360dpi/60 lpi / 360 dpi: "60" +*ColorSepScreenFreq ProcessMagenta.60lpi.360dpi/60 lpi / 360 dpi: "60" +*ColorSepScreenFreq ProcessYellow.60lpi.360dpi/60 lpi / 360 dpi: "60" + +*% --- For 71 lpi / 600 dpi --- +*ColorSepScreenAngle ProcessBlack.71lpi.600dpi/71 lpi / 600 dpi: "45.0" +*ColorSepScreenAngle CustomColor.71lpi.600dpi/71 lpi / 600 dpi: "45.0" +*ColorSepScreenAngle ProcessCyan.71lpi.600dpi/71 lpi / 600 dpi: "71.5651" +*ColorSepScreenAngle ProcessMagenta.71lpi.600dpi/71 lpi / 600 dpi: "18.4349" +*ColorSepScreenAngle ProcessYellow.71lpi.600dpi/71 lpi / 600 dpi: "0.0" +*ColorSepScreenFreq ProcessBlack.71lpi.600dpi/71 lpi / 600 dpi: "70.7107" +*ColorSepScreenFreq CustomColor.71lpi.600dpi/71 lpi / 600 dpi: "70.7107" +*ColorSepScreenFreq ProcessCyan.71lpi.600dpi/71 lpi / 600 dpi: "63.2456" +*ColorSepScreenFreq ProcessMagenta.71lpi.600dpi/71 lpi / 600 dpi: "63.2456" +*ColorSepScreenFreq ProcessYellow.71lpi.600dpi/71 lpi / 600 dpi: "66.6667" + +*% --- For 71 lpi / 720 dpi --- +*ColorSepScreenAngle ProcessBlack.71lpi.720dpi/71 lpi / 720 dpi: "45.0" +*ColorSepScreenAngle CustomColor.71lpi.720dpi/71 lpi / 720 dpi: "45.0" +*ColorSepScreenAngle ProcessCyan.71lpi.720dpi/71 lpi / 720 dpi: "71.5651" +*ColorSepScreenAngle ProcessMagenta.71lpi.720dpi/71 lpi / 720 dpi: "18.4349" +*ColorSepScreenAngle ProcessYellow.71lpi.720dpi/71 lpi / 720 dpi: "0.0" +*ColorSepScreenFreq ProcessBlack.71lpi.720dpi/71 lpi / 720 dpi: "70.7107" +*ColorSepScreenFreq CustomColor.71lpi.720dpi/71 lpi / 720 dpi: "70.7107" +*ColorSepScreenFreq ProcessCyan.71lpi.720dpi/71 lpi / 720 dpi: "63.2456" +*ColorSepScreenFreq ProcessMagenta.71lpi.720dpi/71 lpi / 720 dpi: "63.2456" +*ColorSepScreenFreq ProcessYellow.71lpi.720dpi/71 lpi / 720 dpi: "66.6667" + +*% --- For 100 lpi / 1200 dpi --- +*ColorSepScreenAngle ProcessBlack.100lpi.1200dpi/100 lpi / 1200 dpi: "45.0" +*ColorSepScreenAngle CustomColor.100lpi.1200dpi/100 lpi / 1200 dpi: "45.0" +*ColorSepScreenAngle ProcessCyan.100lpi.1200dpi/100 lpi / 1200 dpi: "15.0" +*ColorSepScreenAngle ProcessMagenta.100lpi.1200dpi/100 lpi / 1200 dpi: "75.0" +*ColorSepScreenAngle ProcessYellow.100lpi.1200dpi/100 lpi / 1200 dpi: "0.0" +*ColorSepScreenFreq ProcessBlack.100lpi.1200dpi/100 lpi / 1200 dpi: "100.0" +*ColorSepScreenFreq CustomColor.100lpi.1200dpi/100 lpi / 1200 dpi: "100.0" +*ColorSepScreenFreq ProcessCyan.100lpi.1200dpi/100 lpi / 1200 dpi: "100.0" +*ColorSepScreenFreq ProcessMagenta.100lpi.1200dpi/100 lpi / 1200 dpi: "100.0" +*ColorSepScreenFreq ProcessYellow.100lpi.1200dpi/100 lpi / 1200 dpi: "100.0" + +*% --- For 100 lpi / 1440 dpi --- +*ColorSepScreenAngle ProcessBlack.100lpi.1440dpi/100 lpi / 1440 dpi: "45.0" +*ColorSepScreenAngle CustomColor.100lpi.1440dpi/100 lpi / 1440 dpi: "45.0" +*ColorSepScreenAngle ProcessCyan.100lpi.1440dpi/100 lpi / 1440 dpi: "15.0" +*ColorSepScreenAngle ProcessMagenta.100lpi.1440dpi/100 lpi / 1440 dpi: "75.0" +*ColorSepScreenAngle ProcessYellow.100lpi.1440dpi/100 lpi / 1440 dpi: "0.0" +*ColorSepScreenFreq ProcessBlack.100lpi.1440dpi/100 lpi / 1440 dpi: "100.0" +*ColorSepScreenFreq CustomColor.100lpi.1440dpi/100 lpi / 1440 dpi: "100.0" +*ColorSepScreenFreq ProcessCyan.100lpi.1440dpi/100 lpi / 1440 dpi: "100.0" +*ColorSepScreenFreq ProcessMagenta.100lpi.1440dpi/100 lpi / 1440 dpi: "100.0" +*ColorSepScreenFreq ProcessYellow.100lpi.1440dpi/100 lpi / 1440 dpi: "100.0" + +*% --- For 175 lpi / 2400 dpi --- +*ColorSepScreenAngle ProcessBlack.175lpi.2400dpi/175 lpi / 2400 dpi: "45.0" +*ColorSepScreenAngle CustomColor.175lpi.2400dpi/175 lpi / 2400 dpi: "45.0" +*ColorSepScreenAngle ProcessCyan.175lpi.2400dpi/175 lpi / 2400 dpi: "15.0" +*ColorSepScreenAngle ProcessMagenta.175lpi.2400dpi/175 lpi / 2400 dpi: "75.0" +*ColorSepScreenAngle ProcessYellow.175lpi.2400dpi/175 lpi / 2400 dpi: "0.0" +*ColorSepScreenFreq ProcessBlack.175lpi.2400dpi/175 lpi / 2400 dpi: "175.0" +*ColorSepScreenFreq CustomColor.175lpi.2400dpi/175 lpi / 2400 dpi: "175.0" +*ColorSepScreenFreq ProcessCyan.175lpi.2400dpi/175 lpi / 2400 dpi: "175.0" +*ColorSepScreenFreq ProcessMagenta.175lpi.2400dpi/175 lpi / 2400 dpi: "175.0" +*ColorSepScreenFreq ProcessYellow.175lpi.2400dpi/175 lpi / 2400 dpi: "175.0" + +*% Last Edit Date: March 24 2000 +*% end of PPD file diff --git a/vcl/unx/generic/printer/configuration/psprint.conf b/vcl/unx/generic/printer/configuration/psprint.conf new file mode 100644 index 0000000000..015a310d62 --- /dev/null +++ b/vcl/unx/generic/printer/configuration/psprint.conf @@ -0,0 +1,94 @@ +; +; This file is part of the LibreOffice project. +; +; This Source Code Form is subject to the terms of the Mozilla Public +; License, v. 2.0. If a copy of the MPL was not distributed with this +; file, You can obtain one at http://mozilla.org/MPL/2.0/. +; +; This file incorporates work covered by the following license notice: +; +; Licensed to the Apache Software Foundation (ASF) under one or more +; contributor license agreements. See the NOTICE file distributed +; with this work for additional information regarding copyright +; ownership. The ASF licenses this file to you under the Apache +; License, Version 2.0 (the "License"); you may not use this file +; except in compliance with the License. You may obtain a copy of +; the License at http://www.apache.org/licenses/LICENSE-2.0 . +; +[__Global_Printer_Defaults__] +; Copies: the default number of copies produced +; if key is absent the default is 1 +; Copies=1 + +; Orientation: the default orientation of pages +; possible Values: Portrait, Landscape +; if key is absent the default is Portrait +; Orientation=Portrait + +; Scale: the default scaling of output in percent +; if key is absent the default is 100 +; Scale=100 + +; MarginAdjust: the default adjustment to driver margins in 1/100 mm +; MarginAdjust contains corrections for the driver defined margins +; the values are comma separated +; the order is: left,right,top,bottom +; if key is absent the default is 0,0,0,0 +; MarginAdjust=0,0,0,0 + +; ColorDepth: the default colordepth of the device in bits +; possible values: 1, 8, 24 +; if key is absent the default is 24 +; ColorDepth=24 + +; ColorDevice: the default setting whether the device is color capable +; possible values: 0: driver setting, -1: grey scale, 1: color +; if key is absent the default is 0 +; ColorDepth=0 + +; PPD_PageSize: the default page size to use. If a specific printer does +; not support this page size its default is used instead. +; possible values: A0, A1, A2, A3, A4, A5, A6, B4, B5, B6, +; Legal, Letter, Executive, Statement, Tabloid, +; Ledger, AnsiC, AnsiD, ARCHA, ARCHB, ARCHC, +; ARCHD, ARCHE, EnvMonarch, EnvC4, EnvC5, EnvC6, +; Env10, EnvC65, Folio +; if key is absent the default value is driver specific +; PPD_PageSize=A4 + + +[Generic Printer] +; for every printer a group with at least the keys +; "Printer" and "Command" is required + +; Printer: contains the base name of the PPD and the Printer name separated by / +Printer=SGENPRT/Generic Printer + +; DefaultPrinter: marks the default printer +DefaultPrinter=1 + +; Location: a user readable string that will be shown in the print dialog +Location= + +; Comment: a user readable string that will be shown in the print dialog +Comment= + +; Command: a command line that accepts PostScript as standard input (pipe) +; note: a shell will be started for the command +Command= + +; QuickCommand: a command line that accepts PostScript as standard input (pipe) +; this command line will be used instead of the command line given in the +; "Command" key, if the user presses the direct print button. In this case +; no print dialog should be shown, neither from the printing application nor +; from the command line (example "kprinter --nodialog --stdin") +; note: a shell will be started for the command +;QuickCommand= + +; Features: a string containing additional comma separated properties of a printer +; currently valid properties: +; fax for a Fax printer queue +; pdf= for a PDF printer where is the base directory for output files +; external_dialog to notify that the print command of a printer will show a dialog +; and therefore the application should not show its own dialog. +;Features= diff --git a/vcl/unx/generic/printer/cpdmgr.cxx b/vcl/unx/generic/printer/cpdmgr.cxx new file mode 100644 index 0000000000..834c1383ef --- /dev/null +++ b/vcl/unx/generic/printer/cpdmgr.cxx @@ -0,0 +1,756 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include + +#include + +#include +#include + +#include +#include + +#include +#include + +using namespace psp; +using namespace osl; + +#if ENABLE_DBUS && ENABLE_GIO +// Function to execute when name is acquired on the bus +void CPDManager::onNameAcquired (GDBusConnection *connection, + const gchar *, + gpointer user_data) +{ + gchar* contents; + // Get Interface for introspection + if (!g_file_get_contents (FRONTEND_INTERFACE, &contents, nullptr, nullptr)) + return; + + GDBusNodeInfo *introspection_data = g_dbus_node_info_new_for_xml (contents, nullptr); + + g_dbus_connection_register_object (connection, + "/org/libreoffice/PrintDialog", + introspection_data->interfaces[0], + nullptr, + nullptr, /* user_data */ + nullptr, /* user_data_free_func */ + nullptr); /* GError** */ + g_free(contents); + g_dbus_node_info_unref(introspection_data); + + CPDManager* current = static_cast(user_data); + std::vector> backends = current->getTempBackends(); + for (auto const& backend : backends) + { + // Get Interface for introspection + if (g_file_get_contents(BACKEND_INTERFACE, &contents, nullptr, nullptr)) + { + introspection_data = g_dbus_node_info_new_for_xml (contents, nullptr); + GDBusProxy *proxy = g_dbus_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + introspection_data->interfaces[0], + backend.first.c_str(), + backend.second, + "org.openprinting.PrintBackend", + nullptr, + nullptr); + g_assert (proxy != nullptr); + g_dbus_proxy_call(proxy, "ActivateBackend", + nullptr, + G_DBUS_CALL_FLAGS_NONE, + -1, nullptr, nullptr, nullptr); + + g_free(contents); + g_object_unref(proxy); + g_dbus_node_info_unref(introspection_data); + } + g_free(backend.second); + } +} + +void CPDManager::onNameLost (GDBusConnection *, + const gchar *name, + gpointer) +{ + g_message("Name Lost: %s", name); +} + +void CPDManager::printerAdded (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *, + GVariant *parameters, + gpointer user_data) +{ + CPDManager* current = static_cast(user_data); + GDBusProxy *proxy; + proxy = current->getProxy(sender_name); + if (proxy == nullptr) { + gchar* contents; + + // Get Interface for introspection + if (g_file_get_contents ("/usr/share/dbus-1/interfaces/org.openprinting.Backend.xml", &contents, nullptr, nullptr)) { + GDBusNodeInfo *introspection_data = g_dbus_node_info_new_for_xml (contents, nullptr); + proxy = g_dbus_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS, + introspection_data->interfaces[0], + sender_name, + object_path, + interface_name, + nullptr, + nullptr); + + g_free(contents); + g_dbus_node_info_unref(introspection_data); + std::pair new_backend (sender_name, proxy); + current->addBackend(std::move(new_backend)); + } + } + CPDPrinter *pDest = static_cast(malloc(sizeof(CPDPrinter))); + pDest->backend = proxy; + g_variant_get (parameters, "(sssssbss)", &(pDest->id), &(pDest->name), &(pDest->info), &(pDest->location), &(pDest->make_and_model), &(pDest->is_accepting_jobs), &(pDest->printer_state), &(pDest->backend_name)); + std::stringstream printerName; + printerName << pDest->name << ", " << pDest->backend_name; + std::stringstream uniqueName; + uniqueName << pDest->id << ", " << pDest->backend_name; + rtl_TextEncoding aEncoding = osl_getThreadTextEncoding(); + OUString aPrinterName = OStringToOUString( printerName.str(), aEncoding ); + OUString aUniqueName = OStringToOUString( uniqueName.str(), aEncoding ); + current->addNewPrinter(aPrinterName, aUniqueName, pDest); +} + +void CPDManager::printerRemoved (GDBusConnection *, + const gchar *, + const gchar *, + const gchar *, + const gchar *, + GVariant *parameters, + gpointer user_data) +{ + // TODO: Remove every data linked to this particular printer. + CPDManager* pManager = static_cast(user_data); + char* id; + char* backend_name; + g_variant_get (parameters, "(ss)", &id, &backend_name); + std::stringstream uniqueName; + uniqueName << id << ", " << backend_name; + rtl_TextEncoding aEncoding = osl_getThreadTextEncoding(); + OUString aUniqueName = OStringToOUString( uniqueName.str(), aEncoding ); + std::unordered_map::iterator it = pManager->m_aCPDDestMap.find( aUniqueName ); + if (it == pManager->m_aCPDDestMap.end()) { + SAL_WARN("vcl.unx.print", "CPD trying to remove non-existent printer from list"); + return; + } + pManager->m_aCPDDestMap.erase(it); + std::unordered_map::iterator printersIt = pManager->m_aPrinters.find( aUniqueName ); + if (printersIt == pManager->m_aPrinters.end()) { + SAL_WARN("vcl.unx.print", "CPD trying to remove non-existent printer from m_aPrinters"); + return; + } + pManager->m_aPrinters.erase(printersIt); +} + +GDBusProxy* CPDManager::getProxy(const std::string& target) +{ + std::unordered_map::const_iterator it = m_pBackends.find(target); + if (it == m_pBackends.end()) { + return nullptr; + } + return it->second; +} + +void CPDManager::addBackend(std::pair pair) { + m_pBackends.insert(pair); +} + +void CPDManager::addTempBackend(const std::pair& pair) +{ + m_tBackends.push_back(pair); +} + +std::vector> const & CPDManager::getTempBackends() const { + return m_tBackends; +} + +void CPDManager::addNewPrinter(const OUString& aPrinterName, const OUString& aUniqueName, CPDPrinter *pDest) { + m_aCPDDestMap[aUniqueName] = pDest; + bool bSetToGlobalDefaults = m_aPrinters.find( aUniqueName ) == m_aPrinters.end(); + Printer aPrinter = m_aPrinters[ aUniqueName ]; + if( bSetToGlobalDefaults ) + aPrinter.m_aInfo = m_aGlobalDefaults; + aPrinter.m_aInfo.m_aPrinterName = aPrinterName; + + // TODO: I don't know how this should work when we have multiple + // sources with multiple possible defaults for each + // if( pDest->is_default ) + // m_aDefaultPrinter = aPrinterName; + + rtl_TextEncoding aEncoding = osl_getThreadTextEncoding(); + aPrinter.m_aInfo.m_aComment = OStringToOUString(pDest->info, aEncoding); + aPrinter.m_aInfo.m_aLocation = OStringToOUString(pDest->location, aEncoding); + // note: the parser that goes with the PrinterInfo + // is created implicitly by the JobData::operator=() + // when it detects the NULL ptr m_pParser. + // if we wanted to fill in the parser here this + // would mean we'd have to send a dbus message for each and + // every printer - which would be really bad runtime + // behaviour + aPrinter.m_aInfo.m_pParser = nullptr; + aPrinter.m_aInfo.m_aContext.setParser( nullptr ); + std::unordered_map< OUString, PPDContext >::const_iterator c_it = m_aDefaultContexts.find( aUniqueName ); + if( c_it != m_aDefaultContexts.end() ) + { + aPrinter.m_aInfo.m_pParser = c_it->second.getParser(); + aPrinter.m_aInfo.m_aContext = c_it->second; + } + aPrinter.m_aInfo.m_aDriverName = "CPD:" + aUniqueName; + m_aPrinters[ aUniqueName ] = aPrinter; +} +#endif + +/* + * CPDManager class + */ + +CPDManager* CPDManager::tryLoadCPD() +{ + CPDManager* pManager = nullptr; +#if ENABLE_DBUS && ENABLE_GIO + static const char* pEnv = getenv("SAL_DISABLE_CPD"); + + if (!pEnv || !*pEnv) { + // interface description XML files are needed in 'onNameAcquired()' + if (!g_file_test(FRONTEND_INTERFACE, G_FILE_TEST_IS_REGULAR) || + !g_file_test(BACKEND_INTERFACE, G_FILE_TEST_IS_REGULAR)) { + return nullptr; + } + + GDir *dir; + const gchar *filename; + dir = g_dir_open(BACKEND_DIR, 0, nullptr); + if (dir != nullptr) { + while ((filename = g_dir_read_name(dir))) { + if (pManager == nullptr) { + pManager = new CPDManager(); + } + gchar* contents; + std::stringstream filepath; + filepath << BACKEND_DIR << '/' << filename; + if (g_file_get_contents(filepath.str().c_str(), &contents, nullptr, nullptr)) + { + std::pair new_tbackend (filename, contents); + pManager->addTempBackend(new_tbackend); + } + } + g_dir_close(dir); + } + } +#endif + return pManager; +} + +CPDManager::CPDManager() : + PrinterInfoManager( PrinterInfoManager::Type::CPD ) +{ +#if ENABLE_DBUS && ENABLE_GIO + // Get Destinations number and pointers + GError *error = nullptr; + m_pConnection = g_bus_get_sync (G_BUS_TYPE_SESSION, nullptr, &error); + g_assert_no_error (error); +#endif +} + +CPDManager::~CPDManager() +{ +#if ENABLE_DBUS && ENABLE_GIO + g_dbus_connection_emit_signal (m_pConnection, + nullptr, + "/org/libreoffice/PrintDialog", + "org.openprinting.PrintFrontend", + "StopListing", + nullptr, + nullptr); + g_dbus_connection_flush_sync (m_pConnection, + nullptr, + nullptr); + g_dbus_connection_close_sync (m_pConnection, + nullptr, + nullptr); + for (auto const& backend : m_pBackends) + { + g_object_unref(backend.second); + } + for (auto const& backend : m_aCPDDestMap) + { + free(backend.second); + } +#endif +} + + +const PPDParser* CPDManager::createCPDParser( const OUString& rPrinter ) +{ + const PPDParser* pNewParser = nullptr; +#if ENABLE_DBUS && ENABLE_GIO + OUString aPrinter; + + if( rPrinter.startsWith("CPD:") ) + aPrinter = rPrinter.copy( 4 ); + else + aPrinter = rPrinter; + + std::unordered_map< OUString, CPDPrinter * >::iterator dest_it = + m_aCPDDestMap.find( aPrinter ); + + if( dest_it != m_aCPDDestMap.end() ) + { + CPDPrinter* pDest = dest_it->second; + GVariant* ret = nullptr; + GError* error = nullptr; + ret = g_dbus_proxy_call_sync (pDest->backend, "GetAllOptions", + g_variant_new("(s)", (pDest->id)), + G_DBUS_CALL_FLAGS_NONE, + -1, nullptr, &error); + if (ret != nullptr && error == nullptr) + { + // TODO: These keys need to be redefined to preserve usage across libreoffice + // InputSlot - media-col.media-source? + // Font - not needed now as it is required only for ps and we are using pdf + // Dial? - for FAX (need to look up PWG spec) + + int num_attribute; + GVariantIter *iter_attr, *iter_supported_values; + g_variant_get (ret, "(ia(ssia(s)))", &num_attribute, &iter_attr); + rtl_TextEncoding aEncoding = osl_getThreadTextEncoding(); + PPDKey *pKey = nullptr; + OUString aValueName; + PPDValue* pValue; + std::vector keys; + std::vector default_values; + for (int i = 0; i < num_attribute; i++) { + char *name, *default_value; + int num_supported_values; + g_variant_iter_loop(iter_attr, "(ssia(s))", + &name, &default_value, + &num_supported_values, &iter_supported_values); + OUString aOptionName = OStringToOUString( name, aEncoding ); + OUString aDefaultValue = OStringToOUString( default_value, aEncoding ); + if (aOptionName == "sides") { + // Duplex key is used throughout for checking Duplex Support + aOptionName = OUString("Duplex"); + } else if (aOptionName == "printer-resolution") { + // Resolution key is used in places + aOptionName = OUString("Resolution"); + } else if (aOptionName == "media") { + // PageSize key is used in many places + aOptionName = OUString("PageSize"); + } + default_values.push_back(aDefaultValue); + pKey = new PPDKey( aOptionName ); + + // If number of values are 0, this is not settable via UI + if (num_supported_values > 0 && aDefaultValue != "NA") + pKey->m_bUIOption = true; + + bool bDefaultFound = false; + + for (int j = 0; j < num_supported_values; j++) { + char* value; + g_variant_iter_loop(iter_supported_values, "(s)", &value); + aValueName = OStringToOUString( value, aEncoding ); + if (aOptionName == "Duplex") { + // Duplex key matches against very specific Values + if (aValueName == "one-sided") { + aValueName = OUString("None"); + } else if (aValueName == "two-sided-long-edge") { + aValueName = OUString("DuplexNoTumble"); + } else if (aValueName == "two-sided-short-edge") { + aValueName = OUString("DuplexTumble"); + } + } + + pValue = pKey->insertValue( aValueName, eQuoted ); + if( ! pValue ) + continue; + pValue->m_aValue = aValueName; + + if (aValueName.equals(aDefaultValue)) { + pKey->m_pDefaultValue = pValue; + bDefaultFound = true; + } + + } + // This could be done to ensure default values also appear as options: + if (!bDefaultFound && pKey->m_bUIOption) { + // pValue = pKey->insertValue( aDefaultValue, eQuoted ); + // if( pValue ) + // pValue->m_aValue = aDefaultValue; + } + keys.emplace_back(pKey); + } + + pKey = new PPDKey("ModelName"); + aValueName = OStringToOUString( "", aEncoding ); + pValue = pKey->insertValue( aValueName, eQuoted ); + if( pValue ) + pValue->m_aValue = aValueName; + pKey->m_pDefaultValue = pValue; + keys.emplace_back(pKey); + + pKey = new PPDKey("NickName"); + aValueName = OStringToOUString( pDest->name, aEncoding ); + pValue = pKey->insertValue( aValueName, eQuoted ); + if( pValue ) + pValue->m_aValue = aValueName; + pKey->m_pDefaultValue = pValue; + keys.emplace_back(pKey); + + pNewParser = new PPDParser(aPrinter, keys); + PrinterInfo& rInfo = m_aPrinters[ aPrinter ].m_aInfo; + PPDContext& rContext = m_aDefaultContexts[ aPrinter ]; + rContext.setParser( pNewParser ); + setDefaultPaper( rContext ); + std::vector::iterator defit = default_values.begin(); + for (auto const& key : keys) + { + const PPDValue* p1Value = key->getValue( *defit ); + if( p1Value ) + { + if( p1Value != key->getDefaultValue() ) + { + rContext.setValue( key, p1Value, true ); + SAL_INFO("vcl.unx.print", "key " << pKey->getKey() << " is set to " << *defit); + } + else + SAL_INFO("vcl.unx.print", "key " << pKey->getKey() << " is defaulted to " << *defit); + } + ++defit; + } + + rInfo.m_pParser = pNewParser; + rInfo.m_aContext = rContext; + g_variant_unref(ret); + } + else + { + g_clear_error(&error); + SAL_INFO("vcl.unx.print", "CPD GetAllOptions failed, falling back to generic driver"); + } + } + else + SAL_INFO("vcl.unx.print", "no dest found for printer " << aPrinter); + + if( ! pNewParser ) + { + // get the default PPD + pNewParser = PPDParser::getParser( "SGENPRT" ); + SAL_WARN("vcl.unx.print", "Parsing default SGENPRT PPD" ); + + PrinterInfo& rInfo = m_aPrinters[ aPrinter ].m_aInfo; + + rInfo.m_pParser = pNewParser; + rInfo.m_aContext.setParser( pNewParser ); + } +#else + (void)rPrinter; +#endif + return pNewParser; +} + + +void CPDManager::initialize() +{ + // get normal printers, clear printer list + PrinterInfoManager::initialize(); +#if ENABLE_DBUS && ENABLE_GIO + g_bus_own_name_on_connection (m_pConnection, + "org.libreoffice.print-dialog", + G_BUS_NAME_OWNER_FLAGS_NONE, + onNameAcquired, + onNameLost, + this, + nullptr); + + g_dbus_connection_signal_subscribe (m_pConnection, // DBus Connection + nullptr, // Sender Name + "org.openprinting.PrintBackend", // Sender Interface + "PrinterAdded", // Signal Name + nullptr, // Object Path + nullptr, // arg0 behaviour + G_DBUS_SIGNAL_FLAGS_NONE, // Signal Flags + printerAdded, // Callback Function + this, + nullptr); + g_dbus_connection_signal_subscribe (m_pConnection, // DBus Connection + nullptr, // Sender Name + "org.openprinting.PrintBackend", // Sender Interface + "PrinterRemoved", // Signal Name + nullptr, // Object Path + nullptr, // arg0 behaviour + G_DBUS_SIGNAL_FLAGS_NONE, // Signal Flags + printerRemoved, // Callback Function + this, + nullptr); + + // remove everything that is not a CUPS printer and not + // a special purpose printer (PDF, Fax) + std::unordered_map< OUString, Printer >::iterator it = m_aPrinters.begin(); + while (it != m_aPrinters.end()) + { + if( m_aCPDDestMap.find( it->first ) != m_aCPDDestMap.end() ) + { + ++it; + continue; + } + + if( !it->second.m_aInfo.m_aFeatures.isEmpty() ) + { + ++it; + continue; + } + it = m_aPrinters.erase(it); + } +#endif +} + +void CPDManager::setupJobContextData( JobData& rData ) +{ +#if ENABLE_DBUS && ENABLE_GIO + std::unordered_map::iterator dest_it = + m_aCPDDestMap.find( rData.m_aPrinterName ); + + if( dest_it == m_aCPDDestMap.end() ) + return PrinterInfoManager::setupJobContextData( rData ); + + std::unordered_map< OUString, Printer >::iterator p_it = + m_aPrinters.find( rData.m_aPrinterName ); + if( p_it == m_aPrinters.end() ) // huh ? + { + SAL_WARN("vcl.unx.print", "CPD printer list in disorder, " + "no dest for printer " << rData.m_aPrinterName); + return; + } + + if( p_it->second.m_aInfo.m_pParser == nullptr ) + { + // in turn calls createCPDParser + // which updates the printer info + p_it->second.m_aInfo.m_pParser = PPDParser::getParser( p_it->second.m_aInfo.m_aDriverName ); + } + if( p_it->second.m_aInfo.m_aContext.getParser() == nullptr ) + { + OUString aPrinter; + if( p_it->second.m_aInfo.m_aDriverName.startsWith("CPD:") ) + aPrinter = p_it->second.m_aInfo.m_aDriverName.copy( 4 ); + else + aPrinter = p_it->second.m_aInfo.m_aDriverName; + + p_it->second.m_aInfo.m_aContext = m_aDefaultContexts[ aPrinter ]; + } + + rData.m_pParser = p_it->second.m_aInfo.m_pParser; + rData.m_aContext = p_it->second.m_aInfo.m_aContext; +#else + (void)rData; +#endif +} + +FILE* CPDManager::startSpool( const OUString& rPrintername, bool bQuickCommand ) +{ +#if ENABLE_DBUS && ENABLE_GIO + SAL_INFO( "vcl.unx.print", "startSpool: " << rPrintername << " " << (bQuickCommand ? "true" : "false") ); + if( m_aCPDDestMap.find( rPrintername ) == m_aCPDDestMap.end() ) + { + SAL_INFO( "vcl.unx.print", "defer to PrinterInfoManager::startSpool" ); + return PrinterInfoManager::startSpool( rPrintername, bQuickCommand ); + } + OUString aTmpURL, aTmpFile; + osl_createTempFile( nullptr, nullptr, &aTmpURL.pData ); + osl_getSystemPathFromFileURL( aTmpURL.pData, &aTmpFile.pData ); + OString aSysFile = OUStringToOString( aTmpFile, osl_getThreadTextEncoding() ); + FILE* fp = fopen( aSysFile.getStr(), "w" ); + if( fp ) + m_aSpoolFiles[fp] = aSysFile; + + return fp; +#else + (void)rPrintername; + (void)bQuickCommand; + return nullptr; +#endif +} + +#if ENABLE_DBUS && ENABLE_GIO +void CPDManager::getOptionsFromDocumentSetup( const JobData& rJob, bool bBanner, const OString& rJobName, int& rNumOptions, GVariant **arr ) +{ + GVariantBuilder *builder; + builder = g_variant_builder_new(G_VARIANT_TYPE("a(ss)")); + g_variant_builder_add(builder, "(ss)", "job-name", rJobName.getStr()); + if( rJob.m_pParser == rJob.m_aContext.getParser() && rJob.m_pParser ) { + std::size_t i; + std::size_t nKeys = rJob.m_aContext.countValuesModified(); + ::std::vector< const PPDKey* > aKeys( nKeys ); + for( i = 0; i < nKeys; i++ ) + aKeys[i] = rJob.m_aContext.getModifiedKey( i ); + for( i = 0; i < nKeys; i++ ) { + const PPDKey* pKey = aKeys[i]; + const PPDValue* pValue = rJob.m_aContext.getValue( pKey ); + OUString sPayLoad; + if (pValue) { + sPayLoad = pValue->m_bCustomOption ? pValue->m_aCustomOption : pValue->m_aOption; + } + if (!sPayLoad.isEmpty()) { + OString aKey = OUStringToOString( pKey->getKey(), RTL_TEXTENCODING_ASCII_US ); + OString aValue = OUStringToOString( sPayLoad, RTL_TEXTENCODING_ASCII_US ); + if (aKey.equals("Duplex"_ostr)) { + aKey = "sides"_ostr; + } else if (aKey.equals("Resolution"_ostr)) { + aKey = "printer-resolution"_ostr; + } else if (aKey.equals("PageSize"_ostr)) { + aKey = "media"_ostr; + } + if (aKey.equals("sides"_ostr)) { + if (aValue.equals("None"_ostr)) { + aValue = "one-sided"_ostr; + } else if (aValue.equals("DuplexNoTumble"_ostr)) { + aValue = "two-sided-long-edge"_ostr; + } else if (aValue.equals("DuplexTumble"_ostr)) { + aValue = "two-sided-short-edge"_ostr; + } + } + g_variant_builder_add(builder, "(ss)", aKey.getStr(), aValue.getStr()); + } + } + } + if( rJob.m_nCopies > 1 ) + { + OString aVal( OString::number( rJob.m_nCopies ) ); + g_variant_builder_add(builder, "(ss)", "copies", aVal.getStr()); + rNumOptions++; + // TODO: something for collate + // Maybe this is the equivalent ipp attribute: + if (rJob.m_bCollate) { + g_variant_builder_add(builder, "(ss)", "multiple-document-handling", "separate-documents-collated-copies"); + } else { + g_variant_builder_add(builder, "(ss)", "multiple-document-handling", "separate-documents-uncollated-copies"); + } + rNumOptions++; + } + if( ! bBanner ) + { + g_variant_builder_add(builder, "(ss)", "job-sheets", "none"); + rNumOptions++; + } + if (rJob.m_eOrientation == orientation::Portrait) { + g_variant_builder_add(builder, "(ss)", "orientation-requested", "portrait"); + rNumOptions++; + } else if (rJob.m_eOrientation == orientation::Landscape) { + g_variant_builder_add(builder, "(ss)", "orientation-requested", "landscape"); + rNumOptions++; + } + (*arr) = g_variant_new("a(ss)", builder); + g_variant_builder_unref(builder); +} +#endif + +bool CPDManager::endSpool( const OUString& rPrintername, const OUString& rJobTitle, FILE* pFile, const JobData& rDocumentJobData, bool bBanner, const OUString& rFaxNumber ) +{ + bool success = false; +#if ENABLE_DBUS && ENABLE_GIO + SAL_INFO( "vcl.unx.print", "endSpool: " << rPrintername << "," << rJobTitle << " copy count = " << rDocumentJobData.m_nCopies ); + std::unordered_map< OUString, CPDPrinter * >::iterator dest_it = + m_aCPDDestMap.find( rPrintername ); + if( dest_it == m_aCPDDestMap.end() ) + { + SAL_INFO( "vcl.unx.print", "defer to PrinterInfoManager::endSpool" ); + return PrinterInfoManager::endSpool( rPrintername, rJobTitle, pFile, rDocumentJobData, bBanner, rFaxNumber ); + } + + std::unordered_map< FILE*, OString, FPtrHash >::const_iterator it = m_aSpoolFiles.find( pFile ); + if( it != m_aSpoolFiles.end() ) + { + fclose( pFile ); + rtl_TextEncoding aEnc = osl_getThreadTextEncoding(); + OString sJobName(OUStringToOString(rJobTitle, aEnc)); + if (!rFaxNumber.isEmpty()) + { + sJobName = OUStringToOString(rFaxNumber, aEnc); + } + OString aSysFile = it->second; + CPDPrinter* pDest = dest_it->second; + GVariant* ret; + gint job_id; + int nNumOptions = 0; + GVariant *pArr = nullptr; + getOptionsFromDocumentSetup( rDocumentJobData, bBanner, sJobName, nNumOptions, &pArr ); + ret = g_dbus_proxy_call_sync (pDest->backend, "printFile", + g_variant_new( + "(ssi@a(ss))", + (pDest->id), + aSysFile.getStr(), + nNumOptions, + pArr + ), + G_DBUS_CALL_FLAGS_NONE, + -1, nullptr, nullptr); + g_variant_get (ret, "(i)", &job_id); + if (job_id != -1) { + success = true; + } + g_variant_unref(ret); + unlink( it->second.getStr() ); + m_aSpoolFiles.erase(it); + } +#else + (void)rPrintername; + (void)rJobTitle; + (void)pFile; + (void)rDocumentJobData; + (void)bBanner; + (void)rFaxNumber; +#endif + return success; +} + +bool CPDManager::checkPrintersChanged( bool ) +{ +#if ENABLE_DBUS && ENABLE_GIO + bool bChanged = m_aPrintersChanged; + m_aPrintersChanged = false; + g_dbus_connection_emit_signal (m_pConnection, + nullptr, + "/org/libreoffice/PrintDialog", + "org.openprinting.PrintFrontend", + "RefreshBackend", + nullptr, + nullptr); + return bChanged; +#else + return false; +#endif +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ + diff --git a/vcl/unx/generic/printer/cupsmgr.cxx b/vcl/unx/generic/printer/cupsmgr.cxx new file mode 100644 index 0000000000..1fab7a1723 --- /dev/null +++ b/vcl/unx/generic/printer/cupsmgr.cxx @@ -0,0 +1,992 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include + +using namespace psp; +using namespace osl; + +namespace { + +struct GetPPDAttribs +{ + osl::Condition m_aCondition; + OString m_aParameter; + OString m_aResult; + int m_nRefs; + bool* m_pResetRunning; + osl::Mutex* m_pSyncMutex; + + GetPPDAttribs( const char * m_pParameter, + bool* pResetRunning, osl::Mutex* pSyncMutex ) + : m_aParameter( m_pParameter ), + m_pResetRunning( pResetRunning ), + m_pSyncMutex( pSyncMutex ) + { + m_nRefs = 2; + m_aCondition.reset(); + } + + ~GetPPDAttribs() + { + if( !m_aResult.isEmpty() ) + unlink( m_aResult.getStr() ); + } + + void unref() + { + if( --m_nRefs == 0 ) + { + *m_pResetRunning = false; + delete this; + } + } + + void executeCall() + { + // This CUPS method is not at all thread-safe we need + // to dup the pointer to a static buffer it returns ASAP +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + const char* pResult = cupsGetPPD(m_aParameter.getStr()); + OString aResult = pResult ? OString(pResult) : OString(); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + MutexGuard aGuard( *m_pSyncMutex ); + m_aResult = aResult; + m_aCondition.set(); + unref(); + } + + OString waitResult( TimeValue const *pDelay ) + { + m_pSyncMutex->release(); + + if (m_aCondition.wait( pDelay ) != Condition::result_ok + ) + { + SAL_WARN("vcl.unx.print", + "cupsGetPPD " << m_aParameter << " timed out"); + } + m_pSyncMutex->acquire(); + + OString aRetval = m_aResult; + m_aResult.clear(); + unref(); + + return aRetval; + } +}; + +} + +extern "C" { + static void getPPDWorker(void* pData) + { + osl_setThreadName("CUPSManager getPPDWorker"); + GetPPDAttribs* pAttribs = static_cast(pData); + pAttribs->executeCall(); + } +} + +OString CUPSManager::threadedCupsGetPPD( const char* pPrinter ) +{ + OString aResult; + + m_aGetPPDMutex.acquire(); + // if one thread hangs in cupsGetPPD already, don't start another + if( ! m_bPPDThreadRunning ) + { + m_bPPDThreadRunning = true; + GetPPDAttribs* pAttribs = new GetPPDAttribs( pPrinter, + &m_bPPDThreadRunning, + &m_aGetPPDMutex ); + + oslThread aThread = osl_createThread( getPPDWorker, pAttribs ); + + TimeValue aValue; + aValue.Seconds = 5; + aValue.Nanosec = 0; + + // NOTE: waitResult release and acquires the GetPPD mutex + aResult = pAttribs->waitResult( &aValue ); + osl_destroyThread( aThread ); + } + m_aGetPPDMutex.release(); + + return aResult; +} + +static const char* setPasswordCallback( const char* /*pIn*/ ) +{ + const char* pRet = nullptr; + + PrinterInfoManager& rMgr = PrinterInfoManager::get(); + if( rMgr.getType() == PrinterInfoManager::Type::CUPS ) // sanity check + pRet = static_cast(rMgr).authenticateUser(); + return pRet; +} + +/* + * CUPSManager class + */ + +CUPSManager* CUPSManager::tryLoadCUPS() +{ + CUPSManager* pManager = nullptr; + static const char* pEnv = getenv("SAL_DISABLE_CUPS"); + + if (!pEnv || !*pEnv) + pManager = new CUPSManager(); + return pManager; +} + +extern "C" +{ +static void run_dest_thread_stub( void* pThis ) +{ + osl_setThreadName("CUPSManager cupsGetDests"); + CUPSManager::runDestThread( pThis ); +} +} + +CUPSManager::CUPSManager() : + PrinterInfoManager( PrinterInfoManager::Type::CUPS ), + m_nDests( 0 ), + m_pDests( nullptr ), + m_bNewDests( false ), + m_bPPDThreadRunning( false ) +{ + m_aDestThread = osl_createThread( run_dest_thread_stub, this ); +} + +CUPSManager::~CUPSManager() +{ + if( m_aDestThread ) + { + osl_joinWithThread( m_aDestThread ); + osl_destroyThread( m_aDestThread ); + } + + if (m_nDests && m_pDests) + cupsFreeDests( m_nDests, m_pDests ); +} + +void CUPSManager::runDestThread( void* pThis ) +{ + static_cast(pThis)->runDests(); +} + +void CUPSManager::runDests() +{ + SAL_INFO("vcl.unx.print", "starting cupsGetDests"); + cups_dest_t* pDests = nullptr; + + // n#722902 - do a fast-failing check for cups working *at all* first + http_t* p_http; +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + if( (p_http=httpConnectEncrypt( + cupsServer(), + ippPort(), + cupsEncryption())) == nullptr ) + return; + + int nDests = cupsGetDests2(p_http, &pDests); + SAL_INFO("vcl.unx.print", "came out of cupsGetDests"); + + osl::MutexGuard aGuard( m_aCUPSMutex ); + m_nDests = nDests; + m_pDests = pDests; + m_bNewDests = true; + SAL_INFO("vcl.unx.print", "finished cupsGetDests"); + + httpClose(p_http); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif +} + +static void SetIfCustomOption(PPDContext& rContext, const cups_option_t& rOption, rtl_TextEncoding aEncoding) +{ + if (strncmp(rOption.value, RTL_CONSTASCII_STRINGPARAM("Custom.")) == 0) + { + const PPDParser* pParser = rContext.getParser(); + if (!pParser) + { + // normal for first sight of this printer + return; + } + + const PPDKey* pKey = pParser->getKey(OStringToOUString(rOption.name, aEncoding)); + if (!pKey) + { + SAL_WARN("vcl.unx.print", "Custom key " << rOption.name << " not found"); + return; + } + + const PPDValue* pCustomValue = rContext.getValue(pKey); + if (!pCustomValue) + { + SAL_WARN("vcl.unx.print", "Value for " << rOption.name << " not found"); + return; + } + + if (!pCustomValue->m_bCustomOption) + { + SAL_WARN("vcl.unx.print", "Value for " << rOption.name << " not set to custom option"); + return; + } + + // seems sensible to keep a value the user explicitly set even if lpoptions was used to set + // another default + if (pCustomValue->m_bCustomOptionSetViaApp) + return; + pCustomValue->m_aCustomOption = OStringToOUString(rOption.value, aEncoding); + } +} + +void CUPSManager::initialize() +{ + // get normal printers, clear printer list + PrinterInfoManager::initialize(); + + // check whether thread has completed + // if not behave like old printing system + osl::MutexGuard aGuard( m_aCUPSMutex ); + + if( ! m_bNewDests ) + return; + + // dest thread has run, clean up + if( m_aDestThread ) + { + osl_joinWithThread( m_aDestThread ); + osl_destroyThread( m_aDestThread ); + m_aDestThread = nullptr; + } + m_bNewDests = false; + + // clear old stuff + m_aCUPSDestMap.clear(); + + if( ! (m_nDests && m_pDests ) ) + return; + + // check for CUPS server(?) > 1.2 + // since there is no API to query, check for options that were + // introduced in dests with 1.2 + // this is needed to check for %%IncludeFeature support + // (#i65684#, #i65491#) + cups_dest_t* pDest = m_pDests; + + rtl_TextEncoding aEncoding = osl_getThreadTextEncoding(); + int nPrinter = m_nDests; + + // reset global default PPD options; these are queried on demand from CUPS + m_aGlobalDefaults.m_pParser = nullptr; + m_aGlobalDefaults.m_aContext = PPDContext(); + + // add CUPS printers, should there be a printer + // with the same name as a CUPS printer, overwrite it + while( nPrinter-- ) + { + pDest = m_pDests+nPrinter; + OUString aPrinterName = OStringToOUString( pDest->name, aEncoding ); + if( pDest->instance && *pDest->instance ) + { + aPrinterName += "/" + + OStringToOUString( pDest->instance, aEncoding ); + } + + // initialize printer with possible configuration from psprint.conf + bool bSetToGlobalDefaults = m_aPrinters.find( aPrinterName ) == m_aPrinters.end(); + Printer aPrinter = m_aPrinters[ aPrinterName ]; + if( bSetToGlobalDefaults ) + aPrinter.m_aInfo = m_aGlobalDefaults; + aPrinter.m_aInfo.m_aPrinterName = aPrinterName; + if( pDest->is_default ) + m_aDefaultPrinter = aPrinterName; + + // note: the parser that goes with the PrinterInfo + // is created implicitly by the JobData::operator=() + // when it detects the NULL ptr m_pParser. + // if we wanted to fill in the parser here this + // would mean we'd have to download PPDs for each and + // every printer - which would be really bad runtime + // behaviour + aPrinter.m_aInfo.m_pParser = nullptr; + aPrinter.m_aInfo.m_aContext.setParser( nullptr ); + std::unordered_map< OUString, PPDContext >::const_iterator c_it = m_aDefaultContexts.find( aPrinterName ); + if( c_it != m_aDefaultContexts.end() ) + { + aPrinter.m_aInfo.m_pParser = c_it->second.getParser(); + aPrinter.m_aInfo.m_aContext = c_it->second; + } + aPrinter.m_aInfo.m_aDriverName = "CUPS:" + aPrinterName; + + for( int k = 0; k < pDest->num_options; k++ ) + { + if(!strcmp(pDest->options[k].name, "printer-info")) + aPrinter.m_aInfo.m_aComment=OStringToOUString(pDest->options[k].value, aEncoding); + if(!strcmp(pDest->options[k].name, "printer-location")) + aPrinter.m_aInfo.m_aLocation=OStringToOUString(pDest->options[k].value, aEncoding); + if(!strcmp(pDest->options[k].name, "auth-info-required")) + aPrinter.m_aInfo.m_aAuthInfoRequired=OStringToOUString(pDest->options[k].value, aEncoding); + // tdf#149439 Update Custom values that may have changed if this is not a newly discovered printer + SetIfCustomOption(aPrinter.m_aInfo.m_aContext, pDest->options[k], aEncoding); + } + + m_aPrinters[ aPrinter.m_aInfo.m_aPrinterName ] = aPrinter; + m_aCUPSDestMap[ aPrinter.m_aInfo.m_aPrinterName ] = nPrinter; + } + + // remove everything that is not a CUPS printer and not + // a special purpose printer (PDF, Fax) + std::unordered_map< OUString, Printer >::iterator it = m_aPrinters.begin(); + while(it != m_aPrinters.end()) + { + if( m_aCUPSDestMap.find( it->first ) != m_aCUPSDestMap.end() ) + { + ++it; + continue; + } + + if( !it->second.m_aInfo.m_aFeatures.isEmpty() ) + { + ++it; + continue; + } + it = m_aPrinters.erase(it); + } + + cupsSetPasswordCB( setPasswordCallback ); +} + +static void updatePrinterContextInfo( ppd_group_t* pPPDGroup, PPDContext& rContext ) +{ + rtl_TextEncoding aEncoding = osl_getThreadTextEncoding(); + for( int i = 0; i < pPPDGroup->num_options; i++ ) + { + ppd_option_t* pOption = pPPDGroup->options + i; + for( int n = 0; n < pOption->num_choices; n++ ) + { + ppd_choice_t* pChoice = pOption->choices + n; + if( pChoice->marked ) + { + const PPDKey* pKey = rContext.getParser()->getKey( OStringToOUString( pOption->keyword, aEncoding ) ); + if( pKey ) + { + const PPDValue* pValue = pKey->getValue( OStringToOUString( pChoice->choice, aEncoding ) ); + if( pValue ) + { + if( pValue != pKey->getDefaultValue() ) + { + rContext.setValue( pKey, pValue, true ); + SAL_INFO("vcl.unx.print", "key " << pOption->keyword << " is set to " << pChoice->choice); + + } + else + SAL_INFO("vcl.unx.print", "key " << pOption->keyword << " is defaulted to " << pChoice->choice); + } + else + SAL_INFO("vcl.unx.print", "caution: value " << pChoice->choice << " not found in key " << pOption->keyword); + } + else + SAL_INFO("vcl.unx.print", "caution: key " << pOption->keyword << " not found in parser"); + } + } + } + + // recurse through subgroups + for( int g = 0; g < pPPDGroup->num_subgroups; g++ ) + { + updatePrinterContextInfo( pPPDGroup->subgroups + g, rContext ); + } +} + +const PPDParser* CUPSManager::createCUPSParser( const OUString& rPrinter ) +{ + const PPDParser* pNewParser = nullptr; + OUString aPrinter; + + if( rPrinter.startsWith("CUPS:") ) + aPrinter = rPrinter.copy( 5 ); + else + aPrinter = rPrinter; + + if( m_aCUPSMutex.tryToAcquire() ) + { + if (m_nDests && m_pDests) + { + std::unordered_map< OUString, int >::iterator dest_it = + m_aCUPSDestMap.find( aPrinter ); + if( dest_it != m_aCUPSDestMap.end() ) + { + cups_dest_t* pDest = m_pDests + dest_it->second; + OString aPPDFile = threadedCupsGetPPD( pDest->name ); + SAL_INFO("vcl.unx.print", + "PPD for " << aPrinter << " is " << aPPDFile); + if( !aPPDFile.isEmpty() ) + { + rtl_TextEncoding aEncoding = osl_getThreadTextEncoding(); + OUString aFileName( OStringToOUString( aPPDFile, aEncoding ) ); + // update the printer info with context information +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + ppd_file_t* pPPD = ppdOpenFile( aPPDFile.getStr() ); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + if( pPPD ) + { + // create the new parser + PPDParser* pCUPSParser = new PPDParser( aFileName ); + pCUPSParser->m_aFile = rPrinter; + pNewParser = pCUPSParser; + +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + /*int nConflicts =*/ cupsMarkOptions( pPPD, pDest->num_options, pDest->options ); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + SAL_INFO("vcl.unx.print", "processing the following options for printer " << pDest->name << " (instance " << (pDest->instance == nullptr ? "null" : pDest->instance) << "):"); + for( int k = 0; k < pDest->num_options; k++ ) + SAL_INFO("vcl.unx.print", + " \"" << pDest->options[k].name << + "\" = \"" << pDest->options[k].value << "\""); + PrinterInfo& rInfo = m_aPrinters[ aPrinter ].m_aInfo; + + // remember the default context for later use + PPDContext& rContext = m_aDefaultContexts[ aPrinter ]; + rContext.setParser( pNewParser ); + // set system default paper; printer CUPS PPD options + // may overwrite it + setDefaultPaper( rContext ); + for( int i = 0; i < pPPD->num_groups; i++ ) + updatePrinterContextInfo( pPPD->groups + i, rContext ); + + // tdf#149439 Set Custom values. + for (int k = 0; k < pDest->num_options; ++k) + SetIfCustomOption(rContext, pDest->options[k], aEncoding); + + rInfo.m_pParser = pNewParser; + rInfo.m_aContext = rContext; + + // clean up the mess +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + ppdClose( pPPD ); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + + } + else + SAL_INFO("vcl.unx.print", "ppdOpenFile failed, falling back to generic driver"); + + // remove temporary PPD file + if (!getenv("SAL_CUPS_PPD_RETAIN_TMP")) + unlink( aPPDFile.getStr() ); + } + else + SAL_INFO("vcl.unx.print", "cupsGetPPD failed, falling back to generic driver"); + } + else + SAL_INFO("vcl.unx.print", "no dest found for printer " << aPrinter); + } + m_aCUPSMutex.release(); + } + else + SAL_WARN("vcl.unx.print", "could not acquire CUPS mutex !!!" ); + + if( ! pNewParser ) + { + // get the default PPD + pNewParser = PPDParser::getParser( "SGENPRT" ); + SAL_INFO("vcl.unx.print", "Parsing default SGENPRT PPD" ); + + PrinterInfo& rInfo = m_aPrinters[ aPrinter ].m_aInfo; + + rInfo.m_pParser = pNewParser; + rInfo.m_aContext.setParser( pNewParser ); + } + + return pNewParser; +} + +void CUPSManager::setupJobContextData( JobData& rData ) +{ + std::unordered_map< OUString, int >::iterator dest_it = + m_aCUPSDestMap.find( rData.m_aPrinterName ); + + if( dest_it == m_aCUPSDestMap.end() ) + return PrinterInfoManager::setupJobContextData( rData ); + + std::unordered_map< OUString, Printer >::iterator p_it = + m_aPrinters.find( rData.m_aPrinterName ); + if( p_it == m_aPrinters.end() ) // huh ? + { + SAL_WARN("vcl.unx.print", "CUPS printer list in disorder, " + "no dest for printer " << rData.m_aPrinterName); + return; + } + + if( p_it->second.m_aInfo.m_pParser == nullptr ) + { + // in turn calls createCUPSParser + // which updates the printer info + p_it->second.m_aInfo.m_pParser = PPDParser::getParser( p_it->second.m_aInfo.m_aDriverName ); + } + if( p_it->second.m_aInfo.m_aContext.getParser() == nullptr ) + { + OUString aPrinter; + if( p_it->second.m_aInfo.m_aDriverName.startsWith("CUPS:") ) + aPrinter = p_it->second.m_aInfo.m_aDriverName.copy( 5 ); + else + aPrinter = p_it->second.m_aInfo.m_aDriverName; + + p_it->second.m_aInfo.m_aContext = m_aDefaultContexts[ aPrinter ]; + } + + rData.m_pParser = p_it->second.m_aInfo.m_pParser; + rData.m_aContext = p_it->second.m_aInfo.m_aContext; +} + +FILE* CUPSManager::startSpool( const OUString& rPrintername, bool bQuickCommand ) +{ + SAL_INFO( "vcl.unx.print", "startSpool: " << rPrintername << " " << (bQuickCommand ? "true" : "false") ); + + if( m_aCUPSDestMap.find( rPrintername ) == m_aCUPSDestMap.end() ) + { + SAL_INFO( "vcl.unx.print", "defer to PrinterInfoManager::startSpool" ); + return PrinterInfoManager::startSpool( rPrintername, bQuickCommand ); + } + + OUString aTmpURL, aTmpFile; + osl_createTempFile( nullptr, nullptr, &aTmpURL.pData ); + osl_getSystemPathFromFileURL( aTmpURL.pData, &aTmpFile.pData ); + OString aSysFile = OUStringToOString( aTmpFile, osl_getThreadTextEncoding() ); + FILE* fp = fopen( aSysFile.getStr(), "w" ); + if( fp ) + m_aSpoolFiles[fp] = aSysFile; + + return fp; +} + +namespace { + +struct less_ppd_key +{ + bool operator()(const PPDKey* left, const PPDKey* right) + { return left->getOrderDependency() < right->getOrderDependency(); } +}; + +} + +void CUPSManager::getOptionsFromDocumentSetup( const JobData& rJob, bool bBanner, int& rNumOptions, void** rOptions ) +{ + rNumOptions = 0; + *rOptions = nullptr; + + // emit features ordered to OrderDependency + // ignore features that are set to default + + // sanity check + if( rJob.m_pParser == rJob.m_aContext.getParser() && rJob.m_pParser ) + { + std::size_t i; + std::size_t nKeys = rJob.m_aContext.countValuesModified(); + ::std::vector< const PPDKey* > aKeys( nKeys ); + for( i = 0; i < nKeys; i++ ) + aKeys[i] = rJob.m_aContext.getModifiedKey( i ); + ::std::sort( aKeys.begin(), aKeys.end(), less_ppd_key() ); + + for( i = 0; i < nKeys; i++ ) + { + const PPDKey* pKey = aKeys[i]; + const PPDValue* pValue = rJob.m_aContext.getValue( pKey ); + OUString sPayLoad; + if (pValue && pValue->m_eType == eInvocation) + { + sPayLoad = pValue->m_bCustomOption ? pValue->m_aCustomOption : pValue->m_aOption; + } + + if (!sPayLoad.isEmpty()) + { + OString aKey = OUStringToOString( pKey->getKey(), RTL_TEXTENCODING_ASCII_US ); + OString aValue = OUStringToOString( sPayLoad, RTL_TEXTENCODING_ASCII_US ); + rNumOptions = cupsAddOption( aKey.getStr(), aValue.getStr(), rNumOptions, reinterpret_cast(rOptions) ); + } + } + } + + if( rJob.m_nCopies > 1 ) + { + OString aVal( OString::number( rJob.m_nCopies ) ); + rNumOptions = cupsAddOption( "copies", aVal.getStr(), rNumOptions, reinterpret_cast(rOptions) ); + aVal = OString::boolean(rJob.m_bCollate); + rNumOptions = cupsAddOption( "collate", aVal.getStr(), rNumOptions, reinterpret_cast(rOptions) ); + } + if( ! bBanner ) + { + rNumOptions = cupsAddOption( "job-sheets", "none", rNumOptions, reinterpret_cast(rOptions) ); + } +} + +namespace +{ + class RTSPWDialog : public weld::GenericDialogController + { + std::unique_ptr m_xText; + std::unique_ptr m_xDomainLabel; + std::unique_ptr m_xDomainEdit; + std::unique_ptr m_xUserLabel; + std::unique_ptr m_xUserEdit; + std::unique_ptr m_xPassLabel; + std::unique_ptr m_xPassEdit; + + public: + RTSPWDialog(weld::Window* pParent, std::string_view rServer, std::string_view rUserName); + + OString getDomain() const + { + return OUStringToOString( m_xDomainEdit->get_text(), osl_getThreadTextEncoding() ); + } + + OString getUserName() const + { + return OUStringToOString( m_xUserEdit->get_text(), osl_getThreadTextEncoding() ); + } + + OString getPassword() const + { + return OUStringToOString( m_xPassEdit->get_text(), osl_getThreadTextEncoding() ); + } + + void SetDomainVisible(bool bShow) + { + m_xDomainLabel->set_visible(bShow); + m_xDomainEdit->set_visible(bShow); + } + + void SetUserVisible(bool bShow) + { + m_xUserLabel->set_visible(bShow); + m_xUserEdit->set_visible(bShow); + } + + void SetPassVisible(bool bShow) + { + m_xPassLabel->set_visible(bShow); + m_xPassEdit->set_visible(bShow); + } + }; + + RTSPWDialog::RTSPWDialog(weld::Window* pParent, std::string_view rServer, std::string_view rUserName) + : GenericDialogController(pParent, "vcl/ui/cupspassworddialog.ui", "CUPSPasswordDialog") + , m_xText(m_xBuilder->weld_label("text")) + , m_xDomainLabel(m_xBuilder->weld_label("label3")) + , m_xDomainEdit(m_xBuilder->weld_entry("domain")) + , m_xUserLabel(m_xBuilder->weld_label("label1")) + , m_xUserEdit(m_xBuilder->weld_entry("user")) + , m_xPassLabel(m_xBuilder->weld_label("label2")) + , m_xPassEdit(m_xBuilder->weld_entry("pass")) + { + OUString aText(m_xText->get_label()); + aText = aText.replaceFirst("%s", OStringToOUString(rServer, osl_getThreadTextEncoding())); + m_xText->set_label(aText); + m_xDomainEdit->set_text("WORKGROUP"); + if (rUserName.empty()) + m_xUserEdit->grab_focus(); + else + { + m_xUserEdit->set_text(OStringToOUString(rUserName, osl_getThreadTextEncoding())); + m_xPassEdit->grab_focus(); + } + } + + bool AuthenticateQuery(std::string_view rServer, OString& rUserName, OString& rPassword) + { + bool bRet = false; + + RTSPWDialog aDialog(Application::GetDefDialogParent(), rServer, rUserName); + if (aDialog.run() == RET_OK) + { + rUserName = aDialog.getUserName(); + rPassword = aDialog.getPassword(); + bRet = true; + } + + return bRet; + } +} + +namespace +{ + OString EscapeCupsOption(const OString& rIn) + { + OStringBuffer sRet; + sal_Int32 nLen = rIn.getLength(); + for (sal_Int32 i = 0; i < nLen; ++i) + { + switch(rIn[i]) + { + case '\\': + case '\'': + case '\"': + case ',': + case ' ': + case '\f': + case '\n': + case '\r': + case '\t': + case '\v': + sRet.append('\\'); + break; + } + sRet.append(rIn[i]); + } + return sRet.makeStringAndClear(); + } +} + +bool CUPSManager::endSpool( const OUString& rPrintername, const OUString& rJobTitle, FILE* pFile, const JobData& rDocumentJobData, bool bBanner, const OUString& rFaxNumber ) +{ + SAL_INFO( "vcl.unx.print", "endSpool: " << rPrintername << "," << rJobTitle << " copy count = " << rDocumentJobData.m_nCopies ); + + int nJobID = 0; + + osl::MutexGuard aGuard( m_aCUPSMutex ); + + std::unordered_map< OUString, int >::iterator dest_it = + m_aCUPSDestMap.find( rPrintername ); + if( dest_it == m_aCUPSDestMap.end() ) + { + SAL_INFO( "vcl.unx.print", "defer to PrinterInfoManager::endSpool" ); + return PrinterInfoManager::endSpool( rPrintername, rJobTitle, pFile, rDocumentJobData, bBanner, rFaxNumber ); + } + + std::unordered_map< FILE*, OString, FPtrHash >::const_iterator it = m_aSpoolFiles.find( pFile ); + if( it != m_aSpoolFiles.end() ) + { + fclose( pFile ); + rtl_TextEncoding aEnc = osl_getThreadTextEncoding(); + + // setup cups options + int nNumOptions = 0; + cups_option_t* pOptions = nullptr; + auto ppOptions = reinterpret_cast(&pOptions); + getOptionsFromDocumentSetup( rDocumentJobData, bBanner, nNumOptions, ppOptions ); + + PrinterInfo aInfo(getPrinterInfo(rPrintername)); + if (!aInfo.m_aAuthInfoRequired.isEmpty()) + { + bool bDomain(false), bUser(false), bPass(false); + sal_Int32 nIndex = 0; + do + { + std::u16string_view aToken = o3tl::getToken(aInfo.m_aAuthInfoRequired, 0, ',', nIndex); + if (aToken == u"domain") + bDomain = true; + else if (aToken == u"username") + bUser = true; + else if (aToken == u"password") + bPass = true; + } + while (nIndex >= 0); + + if (bDomain || bUser || bPass) + { + OString sPrinterName(OUStringToOString(rPrintername, RTL_TEXTENCODING_UTF8)); + OString sUser = cupsUser(); + RTSPWDialog aDialog(Application::GetDefDialogParent(), sPrinterName, sUser); + aDialog.SetDomainVisible(bDomain); + aDialog.SetUserVisible(bUser); + aDialog.SetPassVisible(bPass); + + if (aDialog.run() == RET_OK) + { + OString sAuth; + if (bDomain) + sAuth = EscapeCupsOption(aDialog.getDomain()); + if (bUser) + { + if (bDomain) + sAuth += ","; + sAuth += EscapeCupsOption(aDialog.getUserName()); + } + if (bPass) + { + if (bUser || bDomain) + sAuth += ","; + sAuth += EscapeCupsOption(aDialog.getPassword()); + } + nNumOptions = cupsAddOption("auth-info", sAuth.getStr(), nNumOptions, &pOptions); + } + } + } + + OString sJobName(OUStringToOString(rJobTitle, aEnc)); + + //fax4CUPS, "the job name will be dialled for you" + //so override the jobname with the desired number + if (!rFaxNumber.isEmpty()) + { + sJobName = OUStringToOString(rFaxNumber, aEnc); + } + + cups_dest_t* pDest = m_pDests + dest_it->second; + nJobID = cupsPrintFile(pDest->name, + it->second.getStr(), + sJobName.getStr(), + nNumOptions, pOptions); + SAL_INFO("vcl.unx.print", "cupsPrintFile( " << pDest->name << ", " + << it->second << ", " << rJobTitle << ", " << nNumOptions + << ", " << pOptions << " ) returns " << nJobID); + for( int n = 0; n < nNumOptions; n++ ) + SAL_INFO("vcl.unx.print", + " option " << pOptions[n].name << "=" << pOptions[n].value); +#if OSL_DEBUG_LEVEL > 1 + OString aCmd( "cp " ); + aCmd += it->second.getStr(); + aCmd += OString( " $HOME/cupsprint.ps" ); + system( aCmd.getStr() ); +#endif + + unlink( it->second.getStr() ); + m_aSpoolFiles.erase(it); + if( pOptions ) + cupsFreeOptions( nNumOptions, pOptions ); + } + + return nJobID != 0; +} + +bool CUPSManager::checkPrintersChanged( bool bWait ) +{ + bool bChanged = false; + if( bWait ) + { + if( m_aDestThread ) + { + // initial asynchronous detection still running + SAL_INFO("vcl.unx.print", "syncing cups discovery thread"); + osl_joinWithThread( m_aDestThread ); + osl_destroyThread( m_aDestThread ); + m_aDestThread = nullptr; + SAL_INFO("vcl.unx.print", "done: syncing cups discovery thread"); + } + else + { + // #i82321# check for cups printer updates + // with this change the whole asynchronous detection in a thread is + // almost useless. The only relevance left is for some stalled systems + // where the user can set SAL_DISABLE_SYNCHRONOUS_PRINTER_DETECTION + // (see vcl/unx/source/gdi/salprnpsp.cxx) + // so that checkPrintersChanged( true ) will never be called + + // there is no way to query CUPS whether the printer list has changed + // so get the dest list anew + if( m_nDests && m_pDests ) + cupsFreeDests( m_nDests, m_pDests ); + m_nDests = 0; + m_pDests = nullptr; + runDests(); + } + } + if( m_aCUPSMutex.tryToAcquire() ) + { + bChanged = m_bNewDests; + m_aCUPSMutex.release(); + } + + if( ! bChanged ) + { + bChanged = PrinterInfoManager::checkPrintersChanged( bWait ); + // #i54375# ensure new merging with CUPS list in :initialize + if( bChanged ) + m_bNewDests = true; + } + + if( bChanged ) + initialize(); + + return bChanged; +} + +const char* CUPSManager::authenticateUser() +{ + const char* pRet = nullptr; + + osl::MutexGuard aGuard( m_aCUPSMutex ); + + OString aUser = cupsUser(); + OString aServer = cupsServer(); + OString aPassword; + if (AuthenticateQuery(aServer, aUser, aPassword)) + { + m_aPassword = aPassword; + m_aUser = aUser; + cupsSetUser( m_aUser.getStr() ); + pRet = m_aPassword.getStr(); + } + + return pRet; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/printer/jobdata.cxx b/vcl/unx/generic/printer/jobdata.cxx new file mode 100644 index 0000000000..5fc6a78700 --- /dev/null +++ b/vcl/unx/generic/printer/jobdata.cxx @@ -0,0 +1,238 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include +#include +#include + +#include +#include + +using namespace psp; + +JobData& JobData::operator=(const JobData& rRight) +{ + if(this == &rRight) + return *this; + + m_nCopies = rRight.m_nCopies; + m_bCollate = rRight.m_bCollate; + m_nLeftMarginAdjust = rRight.m_nLeftMarginAdjust; + m_nRightMarginAdjust = rRight.m_nRightMarginAdjust; + m_nTopMarginAdjust = rRight.m_nTopMarginAdjust; + m_nBottomMarginAdjust = rRight.m_nBottomMarginAdjust; + m_nColorDepth = rRight.m_nColorDepth; + m_eOrientation = rRight.m_eOrientation; + m_aPrinterName = rRight.m_aPrinterName; + m_bPapersizeFromSetup = rRight.m_bPapersizeFromSetup; + m_pParser = rRight.m_pParser; + m_aContext = rRight.m_aContext; + m_nColorDevice = rRight.m_nColorDevice; + + if( !m_pParser && !m_aPrinterName.isEmpty() ) + { + PrinterInfoManager& rMgr = PrinterInfoManager::get(); + rMgr.setupJobContextData( *this ); + } + return *this; +} + +void JobData::setCollate( bool bCollate ) +{ + m_bCollate = bCollate; + return; +} + +void JobData::setPaper( int i_nWidth, int i_nHeight ) +{ + if( m_pParser ) + { + OUString aPaper( m_pParser->matchPaper( i_nWidth, i_nHeight ) ); + + const PPDKey* pKey = m_pParser->getKey( "PageSize" ); + const PPDValue* pValue = pKey ? pKey->getValueCaseInsensitive( aPaper ) : nullptr; + + if (pKey && pValue) + m_aContext.setValue( pKey, pValue ); + } +} + +void JobData::setPaperBin( int i_nPaperBin ) +{ + if( m_pParser ) + { + const PPDKey* pKey = m_pParser->getKey( "InputSlot" ); + const PPDValue* pValue = pKey ? pKey->getValue( i_nPaperBin ) : nullptr; + + if (pKey && pValue) + m_aContext.setValue( pKey, pValue ); + } +} + +bool JobData::getStreamBuffer( std::unique_ptr& pData, sal_uInt32& bytes ) +{ + // consistency checks + if( ! m_pParser ) + m_pParser = m_aContext.getParser(); + if( m_pParser != m_aContext.getParser() || + ! m_pParser ) + return false; + + SvMemoryStream aStream; + + // write header job data + aStream.WriteLine("JobData 1"); + + OStringBuffer aLine("printer=" + + OUStringToOString(m_aPrinterName, RTL_TEXTENCODING_UTF8)); + aStream.WriteLine(aLine); + aLine.setLength(0); + + aLine.append("orientation="); + if (m_eOrientation == orientation::Landscape) + aLine.append("Landscape"); + else + aLine.append("Portrait"); + aStream.WriteLine(aLine); + aLine.setLength(0); + + aStream.WriteLine(Concat2View("copies=" + OString::number(static_cast(m_nCopies)))); + aStream.WriteLine(Concat2View("collate=" + OString::boolean(m_bCollate))); + + aStream.WriteLine(Concat2View( + "marginadjustment=" + + OString::number(static_cast(m_nLeftMarginAdjust)) + + "," + + OString::number(static_cast(m_nRightMarginAdjust)) + + ",'" + + OString::number(static_cast(m_nTopMarginAdjust)) + + "," + + OString::number(static_cast(m_nBottomMarginAdjust)))); + + aStream.WriteLine(Concat2View("colordepth=" + OString::number(static_cast(m_nColorDepth)))); + + aStream.WriteLine(Concat2View("colordevice=" + OString::number(static_cast(m_nColorDevice)))); + + // now append the PPDContext stream buffer + aStream.WriteLine( "PPDContextData" ); + sal_uLong nBytes; + std::unique_ptr pContextBuffer(m_aContext.getStreamableBuffer( nBytes )); + if( nBytes ) + aStream.WriteBytes( pContextBuffer.get(), nBytes ); + pContextBuffer.reset(); + + // success + bytes = static_cast(aStream.Tell()); + pData = std::make_unique( bytes ); + memcpy( pData.get(), aStream.GetData(), bytes ); + return true; +} + +bool JobData::constructFromStreamBuffer( const void* pData, sal_uInt32 bytes, JobData& rJobData ) +{ + SvMemoryStream aStream( const_cast(pData), bytes, StreamMode::READ ); + OString aLine; + bool bVersion = false; + bool bPrinter = false; + bool bOrientation = false; + bool bCopies = false; + bool bContext = false; + bool bMargin = false; + bool bColorDepth = false; + bool bColorDevice = false; + + const char printerEquals[] = "printer="; + const char orientatationEquals[] = "orientation="; + const char copiesEquals[] = "copies="; + const char collateEquals[] = "collate="; + const char marginadjustmentEquals[] = "marginadjustment="; + const char colordepthEquals[] = "colordepth="; + const char colordeviceEquals[] = "colordevice="; + + while( ! aStream.eof() ) + { + aStream.ReadLine( aLine ); + if (aLine.startsWith("JobData")) + bVersion = true; + else if (aLine.startsWith(printerEquals)) + { + bPrinter = true; + rJobData.m_aPrinterName = OStringToOUString(aLine.subView(RTL_CONSTASCII_LENGTH(printerEquals)), RTL_TEXTENCODING_UTF8); + } + else if (aLine.startsWith(orientatationEquals)) + { + bOrientation = true; + rJobData.m_eOrientation = o3tl::equalsIgnoreAsciiCase(aLine.subView(RTL_CONSTASCII_LENGTH(orientatationEquals)), "landscape") ? orientation::Landscape : orientation::Portrait; + } + else if (aLine.startsWith(copiesEquals)) + { + bCopies = true; + rJobData.m_nCopies = o3tl::toInt32(aLine.subView(RTL_CONSTASCII_LENGTH(copiesEquals))); + } + else if (aLine.startsWith(collateEquals)) + { + rJobData.m_bCollate = aLine.copy(RTL_CONSTASCII_LENGTH(collateEquals)).toBoolean(); + } + else if (aLine.startsWith(marginadjustmentEquals)) + { + bMargin = true; + sal_Int32 nIdx {RTL_CONSTASCII_LENGTH(marginadjustmentEquals)}; + rJobData.m_nLeftMarginAdjust = o3tl::toInt32(o3tl::getToken(aLine, 0, ',', nIdx)); + rJobData.m_nRightMarginAdjust = o3tl::toInt32(o3tl::getToken(aLine, 0, ',', nIdx)); + rJobData.m_nTopMarginAdjust = o3tl::toInt32(o3tl::getToken(aLine, 0, ',', nIdx)); + rJobData.m_nBottomMarginAdjust = o3tl::toInt32(o3tl::getToken(aLine, 0, ',', nIdx)); + } + else if (aLine.startsWith(colordepthEquals)) + { + bColorDepth = true; + rJobData.m_nColorDepth = o3tl::toInt32(aLine.subView(RTL_CONSTASCII_LENGTH(colordepthEquals))); + } + else if (aLine.startsWith(colordeviceEquals)) + { + bColorDevice = true; + rJobData.m_nColorDevice = o3tl::toInt32(aLine.subView(RTL_CONSTASCII_LENGTH(colordeviceEquals))); + } + else if (aLine == "PPDContextData" && bPrinter) + { + PrinterInfoManager& rManager = PrinterInfoManager::get(); + const PrinterInfo& rInfo = rManager.getPrinterInfo( rJobData.m_aPrinterName ); + rJobData.m_pParser = PPDParser::getParser( rInfo.m_aDriverName ); + if( rJobData.m_pParser ) + { + rJobData.m_aContext.setParser( rJobData.m_pParser ); + sal_uInt64 nBytes = bytes - aStream.Tell(); + std::vector aRemain(nBytes+1); + nBytes = aStream.ReadBytes(aRemain.data(), nBytes); + if (nBytes) + { + aRemain.resize(nBytes+1); + aRemain[nBytes] = 0; + rJobData.m_aContext.rebuildFromStreamBuffer(aRemain); + bContext = true; + } + } + } + } + + return bVersion && bPrinter && bOrientation && bCopies && bContext && bMargin && bColorDevice && bColorDepth; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/printer/ppdparser.cxx b/vcl/unx/generic/printer/ppdparser.cxx new file mode 100644 index 0000000000..2474da3895 --- /dev/null +++ b/vcl/unx/generic/printer/ppdparser.cxx @@ -0,0 +1,1951 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#ifdef ENABLE_CUPS +#include +#endif + +#include +#include +#include + +namespace psp +{ + class PPDTranslator + { + struct LocaleEqual + { + bool operator()(const css::lang::Locale& i_rLeft, + const css::lang::Locale& i_rRight) const + { + return i_rLeft.Language == i_rRight.Language && + i_rLeft.Country == i_rRight.Country && + i_rLeft.Variant == i_rRight.Variant; + } + }; + + struct LocaleHash + { + size_t operator()(const css::lang::Locale& rLocale) const + { + std::size_t seed = 0; + o3tl::hash_combine(seed, rLocale.Language.hashCode()); + o3tl::hash_combine(seed, rLocale.Country.hashCode()); + o3tl::hash_combine(seed, rLocale.Variant.hashCode()); + return seed; + } + }; + + typedef std::unordered_map< css::lang::Locale, OUString, LocaleHash, LocaleEqual > translation_map; + typedef std::unordered_map< OUString, translation_map > key_translation_map; + + key_translation_map m_aTranslations; + public: + PPDTranslator() {} + + void insertValue( + std::u16string_view i_rKey, + std::u16string_view i_rOption, + std::u16string_view i_rValue, + const OUString& i_rTranslation, + const css::lang::Locale& i_rLocale + ); + + void insertOption( std::u16string_view i_rKey, + std::u16string_view i_rOption, + const OUString& i_rTranslation, + const css::lang::Locale& i_rLocale ) + { + insertValue( i_rKey, i_rOption, u"", i_rTranslation, i_rLocale ); + } + + void insertKey( std::u16string_view i_rKey, + const OUString& i_rTranslation, + const css::lang::Locale& i_rLocale = css::lang::Locale() ) + { + insertValue( i_rKey, u"", u"", i_rTranslation, i_rLocale ); + } + + OUString translateValue( + std::u16string_view i_rKey, + std::u16string_view i_rOption + ) const; + + OUString translateOption( std::u16string_view i_rKey, + std::u16string_view i_rOption ) const + { + return translateValue( i_rKey, i_rOption ); + } + + OUString translateKey( std::u16string_view i_rKey ) const + { + return translateValue( i_rKey, u"" ); + } + }; + + static css::lang::Locale normalizeInputLocale( + const css::lang::Locale& i_rLocale + ) + { + css::lang::Locale aLoc( i_rLocale ); + if( aLoc.Language.isEmpty() ) + { + // empty locale requested, fill in application UI locale + aLoc = Application::GetSettings().GetUILanguageTag().getLocale(); + + #if OSL_DEBUG_LEVEL > 1 + static const char* pEnvLocale = getenv( "SAL_PPDPARSER_LOCALE" ); + if( pEnvLocale && *pEnvLocale ) + { + OString aStr( pEnvLocale ); + sal_Int32 nLen = aStr.getLength(); + aLoc.Language = OStringToOUString( aStr.copy( 0, std::min(nLen, 2) ), RTL_TEXTENCODING_MS_1252 ); + if( nLen >=5 && aStr[2] == '_' ) + aLoc.Country = OStringToOUString( aStr.copy( 3, 2 ), RTL_TEXTENCODING_MS_1252 ); + else + aLoc.Country.clear(); + aLoc.Variant.clear(); + } + #endif + } + /* FIXME-BCP47: using Variant, uppercase? */ + aLoc.Language = aLoc.Language.toAsciiLowerCase(); + aLoc.Country = aLoc.Country.toAsciiUpperCase(); + aLoc.Variant = aLoc.Variant.toAsciiUpperCase(); + + return aLoc; + } + + void PPDTranslator::insertValue( + std::u16string_view i_rKey, + std::u16string_view i_rOption, + std::u16string_view i_rValue, + const OUString& i_rTranslation, + const css::lang::Locale& i_rLocale + ) + { + OUStringBuffer aKey( i_rKey.size() + i_rOption.size() + i_rValue.size() + 2 ); + aKey.append( i_rKey ); + if( !i_rOption.empty() || !i_rValue.empty() ) + { + aKey.append( OUString::Concat(":") + i_rOption ); + } + if( !i_rValue.empty() ) + { + aKey.append( OUString::Concat(":") + i_rValue ); + } + if( !aKey.isEmpty() && !i_rTranslation.isEmpty() ) + { + OUString aK( aKey.makeStringAndClear() ); + css::lang::Locale aLoc; + /* FIXME-BCP47: using Variant, uppercase? */ + aLoc.Language = i_rLocale.Language.toAsciiLowerCase(); + aLoc.Country = i_rLocale.Country.toAsciiUpperCase(); + aLoc.Variant = i_rLocale.Variant.toAsciiUpperCase(); + m_aTranslations[ aK ][ aLoc ] = i_rTranslation; + } + } + + OUString PPDTranslator::translateValue( + std::u16string_view i_rKey, + std::u16string_view i_rOption + ) const + { + OUString aResult; + + OUStringBuffer aKey( i_rKey.size() + i_rOption.size() + 2 ); + aKey.append( i_rKey ); + if( !i_rOption.empty() ) + { + aKey.append( OUString::Concat(":") + i_rOption ); + } + if( !aKey.isEmpty() ) + { + OUString aK( aKey.makeStringAndClear() ); + key_translation_map::const_iterator it = m_aTranslations.find( aK ); + if( it != m_aTranslations.end() ) + { + const translation_map& rMap( it->second ); + + css::lang::Locale aLoc( normalizeInputLocale( css::lang::Locale() ) ); + /* FIXME-BCP47: use LanguageTag::getFallbackStrings()? */ + for( int nTry = 0; nTry < 4; nTry++ ) + { + translation_map::const_iterator tr = rMap.find( aLoc ); + if( tr != rMap.end() ) + { + aResult = tr->second; + break; + } + switch( nTry ) + { + case 0: aLoc.Variant.clear();break; + case 1: aLoc.Country.clear();break; + case 2: aLoc.Language.clear();break; + } + } + } + } + return aResult; + } + + class PPDCache + { + public: + std::vector< std::unique_ptr > aAllParsers; + std::optional> xAllPPDFiles; + }; +} + +using namespace psp; + +namespace +{ + PPDCache& getPPDCache() + { + static PPDCache thePPDCache; + return thePPDCache; + } + +class PPDDecompressStream +{ +private: + PPDDecompressStream(const PPDDecompressStream&) = delete; + PPDDecompressStream& operator=(const PPDDecompressStream&) = delete; + + std::unique_ptr mpFileStream; + std::unique_ptr mpMemStream; + OUString maFileName; + +public: + explicit PPDDecompressStream( const OUString& rFile ); + ~PPDDecompressStream(); + + bool IsOpen() const; + bool eof() const; + OString ReadLine(); + void Open( const OUString& i_rFile ); + void Close(); + const OUString& GetFileName() const { return maFileName; } +}; + +} + +PPDDecompressStream::PPDDecompressStream( const OUString& i_rFile ) +{ + Open( i_rFile ); +} + +PPDDecompressStream::~PPDDecompressStream() +{ + Close(); +} + +void PPDDecompressStream::Open( const OUString& i_rFile ) +{ + Close(); + + mpFileStream.reset( new SvFileStream( i_rFile, StreamMode::READ ) ); + maFileName = mpFileStream->GetFileName(); + + if( ! mpFileStream->IsOpen() ) + { + Close(); + return; + } + + OString aLine; + mpFileStream->ReadLine( aLine ); + mpFileStream->Seek( 0 ); + + // check for compress'ed or gzip'ed file + if( aLine.getLength() <= 1 || + static_cast(aLine[0]) != 0x1f || + static_cast(aLine[1]) != 0x8b /* check for gzip */ ) + return; + + // so let's try to decompress the stream + mpMemStream.reset( new SvMemoryStream( 4096, 4096 ) ); + ZCodec aCodec; + aCodec.BeginCompression( ZCODEC_DEFAULT_COMPRESSION, /*gzLib*/true ); + tools::Long nComp = aCodec.Decompress( *mpFileStream, *mpMemStream ); + aCodec.EndCompression(); + if( nComp < 0 ) + { + // decompression failed, must be an uncompressed stream after all + mpMemStream.reset(); + mpFileStream->Seek( 0 ); + } + else + { + // compression successful, can get rid of file stream + mpFileStream.reset(); + mpMemStream->Seek( 0 ); + } +} + +void PPDDecompressStream::Close() +{ + mpMemStream.reset(); + mpFileStream.reset(); +} + +bool PPDDecompressStream::IsOpen() const +{ + return (mpMemStream || (mpFileStream && mpFileStream->IsOpen())); +} + +bool PPDDecompressStream::eof() const +{ + return ( mpMemStream ? mpMemStream->eof() : ( mpFileStream == nullptr || mpFileStream->eof() ) ); +} + +OString PPDDecompressStream::ReadLine() +{ + OString o_rLine; + if( mpMemStream ) + mpMemStream->ReadLine( o_rLine ); + else if( mpFileStream ) + mpFileStream->ReadLine( o_rLine ); + return o_rLine; +} + +static osl::FileBase::RC resolveLink( const OUString& i_rURL, OUString& o_rResolvedURL, OUString& o_rBaseName, osl::FileStatus::Type& o_rType) +{ + salhelper::LinkResolver aResolver(osl_FileStatus_Mask_FileName | + osl_FileStatus_Mask_Type | + osl_FileStatus_Mask_FileURL); + + osl::FileBase::RC aRet = aResolver.fetchFileStatus(i_rURL, 10/*nLinkLevel*/); + + if (aRet == osl::FileBase::E_None) + { + o_rResolvedURL = aResolver.m_aStatus.getFileURL(); + o_rBaseName = aResolver.m_aStatus.getFileName(); + o_rType = aResolver.m_aStatus.getFileType(); + } + + return aRet; +} + +void PPDParser::scanPPDDir( const OUString& rDir ) +{ + static struct suffix_t + { + const char* pSuffix; + const sal_Int32 nSuffixLen; + } const pSuffixes[] = + { { ".PS", 3 }, { ".PPD", 4 }, { ".PS.GZ", 6 }, { ".PPD.GZ", 7 } }; + + PPDCache &rPPDCache = getPPDCache(); + + osl::Directory aDir( rDir ); + if ( aDir.open() != osl::FileBase::E_None ) + return; + + osl::DirectoryItem aItem; + + INetURLObject aPPDDir(rDir); + while( aDir.getNextItem( aItem ) == osl::FileBase::E_None ) + { + osl::FileStatus aStatus( osl_FileStatus_Mask_FileName ); + if( aItem.getFileStatus( aStatus ) == osl::FileBase::E_None ) + { + OUString aFileURL, aFileName; + osl::FileStatus::Type eType = osl::FileStatus::Unknown; + OUString aURL = rDir + "/" + aStatus.getFileName(); + + if(resolveLink( aURL, aFileURL, aFileName, eType ) == osl::FileBase::E_None) + { + if( eType == osl::FileStatus::Regular ) + { + INetURLObject aPPDFile = aPPDDir; + aPPDFile.Append( aFileName ); + + // match extension + for(const suffix_t & rSuffix : pSuffixes) + { + if( aFileName.getLength() > rSuffix.nSuffixLen ) + { + if( aFileName.endsWithIgnoreAsciiCaseAsciiL( rSuffix.pSuffix, rSuffix.nSuffixLen ) ) + { + (*rPPDCache.xAllPPDFiles)[ aFileName.copy( 0, aFileName.getLength() - rSuffix.nSuffixLen ) ] = aPPDFile.PathToFileName(); + break; + } + } + } + } + else if( eType == osl::FileStatus::Directory ) + { + scanPPDDir( aFileURL ); + } + } + } + } + aDir.close(); +} + +void PPDParser::initPPDFiles(PPDCache &rPPDCache) +{ + if( rPPDCache.xAllPPDFiles ) + return; + + rPPDCache.xAllPPDFiles.emplace(); + + // check installation directories + std::vector< OUString > aPathList; + psp::getPrinterPathList( aPathList, PRINTER_PPDDIR ); + for (auto const& path : aPathList) + { + INetURLObject aPPDDir( path, INetProtocol::File, INetURLObject::EncodeMechanism::All ); + scanPPDDir( aPPDDir.GetMainURL( INetURLObject::DecodeMechanism::NONE ) ); + } + if( rPPDCache.xAllPPDFiles->find( OUString( "SGENPRT" ) ) != rPPDCache.xAllPPDFiles->end() ) + return; + + // last try: search in directory of executable (mainly for setup) + OUString aExe; + if( osl_getExecutableFile( &aExe.pData ) == osl_Process_E_None ) + { + INetURLObject aDir( aExe ); + aDir.removeSegment(); + SAL_INFO("vcl.unx.print", "scanning last chance dir: " + << aDir.GetMainURL(INetURLObject::DecodeMechanism::NONE)); + scanPPDDir( aDir.GetMainURL( INetURLObject::DecodeMechanism::NONE ) ); + SAL_INFO("vcl.unx.print", "SGENPRT " + << (rPPDCache.xAllPPDFiles->find("SGENPRT") == + rPPDCache.xAllPPDFiles->end() ? "not found" : "found")); + } +} + +OUString PPDParser::getPPDFile( const OUString& rFile ) +{ + INetURLObject aPPD( rFile, INetProtocol::File, INetURLObject::EncodeMechanism::All ); + // someone might enter a full qualified name here + PPDDecompressStream aStream( aPPD.PathToFileName() ); + if( ! aStream.IsOpen() ) + { + std::unordered_map< OUString, OUString >::const_iterator it; + PPDCache &rPPDCache = getPPDCache(); + + bool bRetry = true; + do + { + initPPDFiles(rPPDCache); + // some PPD files contain dots beside the extension, so try name first + // and cut of points after that + OUString aBase( rFile ); + sal_Int32 nLastIndex = aBase.lastIndexOf( '/' ); + if( nLastIndex >= 0 ) + aBase = aBase.copy( nLastIndex+1 ); + do + { + it = rPPDCache.xAllPPDFiles->find( aBase ); + nLastIndex = aBase.lastIndexOf( '.' ); + if( nLastIndex > 0 ) + aBase = aBase.copy( 0, nLastIndex ); + } while( it == rPPDCache.xAllPPDFiles->end() && nLastIndex > 0 ); + + if( it == rPPDCache.xAllPPDFiles->end() && bRetry ) + { + // a new file ? rehash + rPPDCache.xAllPPDFiles.reset(); + bRetry = false; + // note this is optimized for office start where + // no new files occur and initPPDFiles is called only once + } + } while( ! rPPDCache.xAllPPDFiles ); + + if( it != rPPDCache.xAllPPDFiles->end() ) + aStream.Open( it->second ); + } + + OUString aRet; + if( aStream.IsOpen() ) + { + OString aLine = aStream.ReadLine(); + if (aLine.startsWith("*PPD-Adobe")) + aRet = aStream.GetFileName(); + else + { + // our *Include hack does usually not begin + // with *PPD-Adobe, so try some lines for *Include + int nLines = 10; + while (aLine.indexOf("*Include") != 0 && --nLines) + aLine = aStream.ReadLine(); + if( nLines ) + aRet = aStream.GetFileName(); + } + } + + return aRet; +} + +const PPDParser* PPDParser::getParser( const OUString& rFile ) +{ + // Recursive because we can get re-entered via CUPSManager::createCUPSParser + static std::recursive_mutex aMutex; + std::scoped_lock aGuard( aMutex ); + + OUString aFile = rFile; + if( !rFile.startsWith( "CUPS:" ) && !rFile.startsWith( "CPD:" ) ) + aFile = getPPDFile( rFile ); + if( aFile.isEmpty() ) + { + SAL_INFO("vcl.unx.print", "Could not get printer PPD file \"" + << rFile << "\" !"); + return nullptr; + } + else + SAL_INFO("vcl.unx.print", "Parsing printer info from \"" + << rFile << "\" !"); + + + PPDCache &rPPDCache = getPPDCache(); + for( auto const & i : rPPDCache.aAllParsers ) + if( i->m_aFile == aFile ) + return i.get(); + + PPDParser* pNewParser = nullptr; + if( !aFile.startsWith( "CUPS:" ) && !aFile.startsWith( "CPD:" ) ) + pNewParser = new PPDParser( aFile ); + else + { + PrinterInfoManager& rMgr = PrinterInfoManager::get(); + if( rMgr.getType() == PrinterInfoManager::Type::CUPS ) + { +#ifdef ENABLE_CUPS + pNewParser = const_cast(static_cast(rMgr).createCUPSParser( aFile )); +#endif + } else if ( rMgr.getType() == PrinterInfoManager::Type::CPD ) + { +#if ENABLE_DBUS && ENABLE_GIO + pNewParser = const_cast(static_cast(rMgr).createCPDParser( aFile )); +#endif + } + } + if( pNewParser ) + { + // this may actually be the SGENPRT parser, + // so ensure uniqueness here (but don't remove last we delete us!) + if (std::none_of( + rPPDCache.aAllParsers.begin(), + rPPDCache.aAllParsers.end(), + [pNewParser] (std::unique_ptr const & x) { return x.get() == pNewParser; } )) + { + // insert new parser to vector + rPPDCache.aAllParsers.emplace_back(pNewParser); + } + } + return pNewParser; +} + +PPDParser::PPDParser(OUString aFile, const std::vector& keys) + : m_aFile(std::move(aFile)) + , m_aFileEncoding(RTL_TEXTENCODING_MS_1252) + , m_pImageableAreas(nullptr) + , m_pDefaultPaperDimension(nullptr) + , m_pPaperDimensions(nullptr) + , m_pDefaultInputSlot(nullptr) + , m_pDefaultResolution(nullptr) + , m_pTranslator(new PPDTranslator()) +{ + for (auto & key: keys) + { + insertKey( std::unique_ptr(key) ); + } + + // fill in shortcuts + const PPDKey* pKey; + + pKey = getKey( "PageSize" ); + + if ( pKey ) { + std::unique_ptr pImageableAreas(new PPDKey("ImageableArea")); + std::unique_ptr pPaperDimensions(new PPDKey("PaperDimension")); +#if defined(CUPS_VERSION_MAJOR) +#if (CUPS_VERSION_MAJOR == 1 && CUPS_VERSION_MINOR >= 7) || CUPS_VERSION_MAJOR > 1 + for (int i = 0; i < pKey->countValues(); i++) { + const PPDValue* pValue = pKey -> getValue(i); + OUString aValueName = pValue -> m_aOption; + PPDValue* pImageableAreaValue = pImageableAreas -> insertValue( aValueName, eQuoted ); + PPDValue* pPaperDimensionValue = pPaperDimensions -> insertValue( aValueName, eQuoted ); + rtl_TextEncoding aEncoding = osl_getThreadTextEncoding(); + OString o = OUStringToOString( aValueName, aEncoding ); + pwg_media_t *pPWGMedia = pwgMediaForPWG(o.pData->buffer); + if (pPWGMedia != nullptr) { + OUStringBuffer aBuf( 256 ); + aBuf = "0 0 " + + OUString::number(PWG_TO_POINTS(pPWGMedia -> width)) + + " " + + OUString::number(PWG_TO_POINTS(pPWGMedia -> length)); + if ( pImageableAreaValue ) + pImageableAreaValue->m_aValue = aBuf.makeStringAndClear(); + aBuf.append( OUString::number(PWG_TO_POINTS(pPWGMedia -> width)) + + " " + + OUString::number(PWG_TO_POINTS(pPWGMedia -> length) )); + if ( pPaperDimensionValue ) + pPaperDimensionValue->m_aValue = aBuf.makeStringAndClear(); + if (aValueName.equals(pKey -> getDefaultValue() -> m_aOption)) { + pImageableAreas -> m_pDefaultValue = pImageableAreaValue; + pPaperDimensions -> m_pDefaultValue = pPaperDimensionValue; + } + } + } +#endif // HAVE_CUPS_API_1_7 +#endif + insertKey(std::move(pImageableAreas)); + insertKey(std::move(pPaperDimensions)); + } + + m_pImageableAreas = getKey( "ImageableArea" ); + const PPDValue* pDefaultImageableArea = nullptr; + if( m_pImageableAreas ) + pDefaultImageableArea = m_pImageableAreas->getDefaultValue(); + if (m_pImageableAreas == nullptr) { + SAL_WARN( "vcl.unx.print", "no ImageableArea in " << m_aFile); + } + if (pDefaultImageableArea == nullptr) { + SAL_WARN( "vcl.unx.print", "no DefaultImageableArea in " << m_aFile); + } + + m_pPaperDimensions = getKey( "PaperDimension" ); + if( m_pPaperDimensions ) + m_pDefaultPaperDimension = m_pPaperDimensions->getDefaultValue(); + if (m_pPaperDimensions == nullptr) { + SAL_WARN( "vcl.unx.print", "no PaperDimensions in " << m_aFile); + } + if (m_pDefaultPaperDimension == nullptr) { + SAL_WARN( "vcl.unx.print", "no DefaultPaperDimensions in " << m_aFile); + } + + auto pResolutions = getKey( "Resolution" ); + if( pResolutions ) + m_pDefaultResolution = pResolutions->getDefaultValue(); + if (pResolutions == nullptr) { + SAL_INFO( "vcl.unx.print", "no Resolution in " << m_aFile); + } + SAL_INFO_IF(!m_pDefaultResolution, "vcl.unx.print", "no DefaultResolution in " + m_aFile); + + auto pInputSlots = getKey( "InputSlot" ); + if( pInputSlots ) + m_pDefaultInputSlot = pInputSlots->getDefaultValue(); + SAL_INFO_IF(!pInputSlots, "vcl.unx.print", "no InputSlot in " << m_aFile); + SAL_INFO_IF(!m_pDefaultInputSlot, "vcl.unx.print", "no DefaultInputSlot in " << m_aFile); +} + +PPDParser::PPDParser( OUString aFile ) : + m_aFile(std::move( aFile )), + m_aFileEncoding( RTL_TEXTENCODING_MS_1252 ), + m_pImageableAreas( nullptr ), + m_pDefaultPaperDimension( nullptr ), + m_pPaperDimensions( nullptr ), + m_pDefaultInputSlot( nullptr ), + m_pDefaultResolution( nullptr ), + m_pTranslator( new PPDTranslator() ) +{ + // read in the file + std::vector< OString > aLines; + PPDDecompressStream aStream( m_aFile ); + if( aStream.IsOpen() ) + { + bool bLanguageEncoding = false; + while( ! aStream.eof() ) + { + OString aCurLine = aStream.ReadLine(); + if( aCurLine.startsWith("*") ) + { + if (aCurLine.matchIgnoreAsciiCase("*include:")) + { + aCurLine = aCurLine.copy(9); + aCurLine = comphelper::string::strip(aCurLine, ' '); + aCurLine = comphelper::string::strip(aCurLine, '\t'); + aCurLine = comphelper::string::stripEnd(aCurLine, '\r'); + aCurLine = comphelper::string::stripEnd(aCurLine, '\n'); + aCurLine = comphelper::string::strip(aCurLine, '"'); + aStream.Close(); + aStream.Open(getPPDFile(OStringToOUString(aCurLine, m_aFileEncoding))); + continue; + } + else if( ! bLanguageEncoding && + aCurLine.matchIgnoreAsciiCase("*languageencoding") ) + { + bLanguageEncoding = true; // generally only the first one counts + OString aLower = aCurLine.toAsciiLowerCase(); + if( aLower.indexOf("isolatin1", 17 ) != -1 || + aLower.indexOf("windowsansi", 17 ) != -1 ) + m_aFileEncoding = RTL_TEXTENCODING_MS_1252; + else if( aLower.indexOf("isolatin2", 17 ) != -1 ) + m_aFileEncoding = RTL_TEXTENCODING_ISO_8859_2; + else if( aLower.indexOf("isolatin5", 17 ) != -1 ) + m_aFileEncoding = RTL_TEXTENCODING_ISO_8859_5; + else if( aLower.indexOf("jis83-rksj", 17 ) != -1 ) + m_aFileEncoding = RTL_TEXTENCODING_SHIFT_JIS; + else if( aLower.indexOf("macstandard", 17 ) != -1 ) + m_aFileEncoding = RTL_TEXTENCODING_APPLE_ROMAN; + else if( aLower.indexOf("utf-8", 17 ) != -1 ) + m_aFileEncoding = RTL_TEXTENCODING_UTF8; + } + } + aLines.push_back( aCurLine ); + } + } + aStream.Close(); + + // now get the Values + parse( aLines ); +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.print", "acquired " << m_aKeys.size() + << " Keys from PPD " << m_aFile << ":"); + for (auto const& key : m_aKeys) + { + const PPDKey* pKey = key.second.get(); + char const* pSetupType = ""; + switch( pKey->m_eSetupType ) + { + case PPDKey::SetupType::ExitServer: pSetupType = "ExitServer";break; + case PPDKey::SetupType::Prolog: pSetupType = "Prolog";break; + case PPDKey::SetupType::DocumentSetup: pSetupType = "DocumentSetup";break; + case PPDKey::SetupType::PageSetup: pSetupType = "PageSetup";break; + case PPDKey::SetupType::JCLSetup: pSetupType = "JCLSetup";break; + case PPDKey::SetupType::AnySetup: pSetupType = "AnySetup";break; + default: break; + } + SAL_INFO("vcl.unx.print", "\t\"" << pKey->getKey() << "\" (" + << pKey->countValues() << "values) OrderDependency: " + << pKey->m_nOrderDependency << pSetupType ); + for( int j = 0; j < pKey->countValues(); j++ ) + { + const PPDValue* pValue = pKey->getValue( j ); + char const* pVType = ""; + switch( pValue->m_eType ) + { + case eInvocation: pVType = "invocation";break; + case eQuoted: pVType = "quoted";break; + case eString: pVType = "string";break; + case eSymbol: pVType = "symbol";break; + case eNo: pVType = "no";break; + default: break; + } + SAL_INFO("vcl.unx.print", "\t\t" + << (pValue == pKey->m_pDefaultValue ? "(Default:) " : "") + << "option: \"" << pValue->m_aOption + << "\", value: type " << pVType << " \"" + << pValue->m_aValue << "\""); + } + } + SAL_INFO("vcl.unx.print", + "constraints: (" << m_aConstraints.size() << " found)"); + for (auto const& constraint : m_aConstraints) + { + SAL_INFO("vcl.unx.print", "*\"" << constraint.m_pKey1->getKey() << "\" \"" + << (constraint.m_pOption1 ? constraint.m_pOption1->m_aOption : "") + << "\" *\"" << constraint.m_pKey2->getKey() << "\" \"" + << (constraint.m_pOption2 ? constraint.m_pOption2->m_aOption : "") + << "\""); + } +#endif + + m_pImageableAreas = getKey( "ImageableArea" ); + const PPDValue * pDefaultImageableArea = nullptr; + if( m_pImageableAreas ) + pDefaultImageableArea = m_pImageableAreas->getDefaultValue(); + if (m_pImageableAreas == nullptr) { + SAL_WARN( "vcl.unx.print", "no ImageableArea in " << m_aFile); + } + if (pDefaultImageableArea == nullptr) { + SAL_WARN( "vcl.unx.print", "no DefaultImageableArea in " << m_aFile); + } + + m_pPaperDimensions = getKey( "PaperDimension" ); + if( m_pPaperDimensions ) + m_pDefaultPaperDimension = m_pPaperDimensions->getDefaultValue(); + if (m_pPaperDimensions == nullptr) { + SAL_WARN( "vcl.unx.print", "no PaperDimensions in " << m_aFile); + } + if (m_pDefaultPaperDimension == nullptr) { + SAL_WARN( "vcl.unx.print", "no DefaultPaperDimensions in " << m_aFile); + } + + auto pResolutions = getKey( "Resolution" ); + if( pResolutions ) + m_pDefaultResolution = pResolutions->getDefaultValue(); + if (pResolutions == nullptr) { + SAL_INFO( "vcl.unx.print", "no Resolution in " << m_aFile); + } + SAL_INFO_IF(!m_pDefaultResolution, "vcl.unx.print", "no DefaultResolution in " + m_aFile); + + auto pInputSlots = getKey( "InputSlot" ); + if( pInputSlots ) + m_pDefaultInputSlot = pInputSlots->getDefaultValue(); + SAL_INFO_IF(!pInputSlots, "vcl.unx.print", "no InputSlot in " << m_aFile); + SAL_INFO_IF(!m_pDefaultInputSlot, "vcl.unx.print", "no DefaultInputSlot in " << m_aFile); +} + +PPDParser::~PPDParser() +{ + m_pTranslator.reset(); +} + +void PPDParser::insertKey( std::unique_ptr pKey ) +{ + m_aOrderedKeys.push_back( pKey.get() ); + m_aKeys[ pKey->getKey() ] = std::move(pKey); +} + +const PPDKey* PPDParser::getKey( int n ) const +{ + return (n >= 0 && o3tl::make_unsigned(n) < m_aOrderedKeys.size()) ? m_aOrderedKeys[n] : nullptr; +} + +const PPDKey* PPDParser::getKey( const OUString& rKey ) const +{ + PPDParser::hash_type::const_iterator it = m_aKeys.find( rKey ); + return it != m_aKeys.end() ? it->second.get() : nullptr; +} + +bool PPDParser::hasKey( const PPDKey* pKey ) const +{ + return pKey && ( m_aKeys.find( pKey->getKey() ) != m_aKeys.end() ); +} + +static sal_uInt8 getNibble( char cChar ) +{ + sal_uInt8 nRet = 0; + if( cChar >= '0' && cChar <= '9' ) + nRet = sal_uInt8( cChar - '0' ); + else if( cChar >= 'A' && cChar <= 'F' ) + nRet = 10 + sal_uInt8( cChar - 'A' ); + else if( cChar >= 'a' && cChar <= 'f' ) + nRet = 10 + sal_uInt8( cChar - 'a' ); + return nRet; +} + +OUString PPDParser::handleTranslation(const OString& i_rString, bool bIsGlobalized) +{ + sal_Int32 nOrigLen = i_rString.getLength(); + OStringBuffer aTrans( nOrigLen ); + const char* pStr = i_rString.getStr(); + const char* pEnd = pStr + nOrigLen; + while( pStr < pEnd ) + { + if( *pStr == '<' ) + { + pStr++; + char cChar; + while( *pStr != '>' && pStr < pEnd-1 ) + { + cChar = getNibble( *pStr++ ) << 4; + cChar |= getNibble( *pStr++ ); + aTrans.append( cChar ); + } + pStr++; + } + else + aTrans.append( *pStr++ ); + } + return OStringToOUString( aTrans, bIsGlobalized ? RTL_TEXTENCODING_UTF8 : m_aFileEncoding ); +} + +namespace +{ + bool oddDoubleQuoteCount(OStringBuffer &rBuffer) + { + bool bHasOddCount = false; + for (sal_Int32 i = 0; i < rBuffer.getLength(); ++i) + { + if (rBuffer[i] == '"') + bHasOddCount = !bHasOddCount; + } + return bHasOddCount; + } +} + +void PPDParser::parse( ::std::vector< OString >& rLines ) +{ + // Name for PPD group into which all options are put for which the PPD + // does not explicitly define a group. + // This is similar to how CUPS handles it, + // s. Sweet, Michael R. (2001): Common UNIX Printing System, p. 251: + // "Each option in turn is associated with a group stored in the + // ppd_group_t structure. Groups can be specified in the PPD file; if an + // option is not associated with a group, it is put in a "General" or + // "Extra" group depending on the option. + static constexpr OString aDefaultPPDGroupName("General"_ostr); + + std::vector< OString >::iterator line = rLines.begin(); + PPDParser::hash_type::const_iterator keyit; + + // name of the PPD group that is currently being processed + OString aCurrentGroup = aDefaultPPDGroupName; + + while( line != rLines.end() ) + { + OString aCurrentLine( *line ); + ++line; + + SAL_INFO("vcl.unx.print", "Parse line '" << aCurrentLine << "'"); + + if (aCurrentLine.getLength() < 2 || aCurrentLine[0] != '*') + continue; + if( aCurrentLine[1] == '%' ) + continue; + + OString aKey = GetCommandLineToken( 0, aCurrentLine.getToken(0, ':') ); + sal_Int32 nPos = aKey.indexOf('/'); + if (nPos != -1) + aKey = aKey.copy(0, nPos); + if(!aKey.isEmpty()) + { + aKey = aKey.copy(1); // remove the '*' + } + if(aKey.isEmpty()) + { + continue; + } + + if (aKey == "CloseGroup") + { + aCurrentGroup = aDefaultPPDGroupName; + continue; + } + if (aKey == "OpenGroup") + { + OString aGroupName = aCurrentLine; + sal_Int32 nPosition = aGroupName.indexOf('/'); + if (nPosition != -1) + { + aGroupName = aGroupName.copy(0, nPosition); + } + + aCurrentGroup = GetCommandLineToken(1, aGroupName); + continue; + } + if ((aKey == "CloseUI") || + (aKey == "JCLCloseUI") || + (aKey == "End") || + (aKey == "JCLEnd") || + (aKey == "OpenSubGroup") || + (aKey == "CloseSubGroup")) + { + continue; + } + + if ((aKey == "OpenUI") || (aKey == "JCLOpenUI")) + { + parseOpenUI( aCurrentLine, aCurrentGroup); + continue; + } + else if (aKey == "OrderDependency") + { + parseOrderDependency( aCurrentLine ); + continue; + } + else if (aKey == "UIConstraints" || + aKey == "NonUIConstraints") + { + continue; // parsed in pass 2 + } + else if( aKey == "CustomPageSize" ) // currently not handled + continue; + else if (aKey.startsWith("Custom", &aKey) ) + { + //fdo#43049 very basic support for Custom entries, we ignore the + //validation params and types + OUString aUniKey(OStringToOUString(aKey, RTL_TEXTENCODING_MS_1252)); + keyit = m_aKeys.find( aUniKey ); + if(keyit != m_aKeys.end()) + { + PPDKey* pKey = keyit->second.get(); + pKey->insertValue("Custom", eInvocation, true); + } + continue; + } + + // default values are parsed in pass 2 + if (aKey.startsWith("Default")) + continue; + + bool bQuery = false; + if (aKey[0] == '?') + { + aKey = aKey.copy(1); + bQuery = true; + } + + OUString aUniKey(OStringToOUString(aKey, RTL_TEXTENCODING_MS_1252)); + // handle CUPS extension for globalized PPDs + /* FIXME-BCP47: really only ISO 639-1 two character language codes? + * goodnight... */ + bool bIsGlobalizedLine = false; + css::lang::Locale aTransLocale; + if( ( aUniKey.getLength() > 3 && aUniKey[ 2 ] == '.' ) || + ( aUniKey.getLength() > 5 && aUniKey[ 2 ] == '_' && aUniKey[ 5 ] == '.' ) ) + { + if( aUniKey[ 2 ] == '.' ) + { + aTransLocale.Language = aUniKey.copy( 0, 2 ); + aUniKey = aUniKey.copy( 3 ); + } + else + { + aTransLocale.Language = aUniKey.copy( 0, 2 ); + aTransLocale.Country = aUniKey.copy( 3, 2 ); + aUniKey = aUniKey.copy( 6 ); + } + bIsGlobalizedLine = true; + } + + OUString aOption; + nPos = aCurrentLine.indexOf(':'); + if( nPos != -1 ) + { + aOption = OStringToOUString( + aCurrentLine.subView( 1, nPos-1 ), RTL_TEXTENCODING_MS_1252 ); + aOption = GetCommandLineToken( 1, aOption ); + sal_Int32 nTransPos = aOption.indexOf( '/' ); + if( nTransPos != -1 ) + aOption = aOption.copy(0, nTransPos); + } + + PPDValueType eType = eNo; + OUString aValue; + OUString aOptionTranslation; + OUString aValueTranslation; + if( nPos != -1 ) + { + // found a colon, there may be an option + OString aLine = aCurrentLine.copy( 1, nPos-1 ); + aLine = WhitespaceToSpace( aLine ); + sal_Int32 nTransPos = aLine.indexOf('/'); + if (nTransPos != -1) + aOptionTranslation = handleTranslation( aLine.copy(nTransPos+1), bIsGlobalizedLine ); + + // read in more lines if necessary for multiline values + aLine = aCurrentLine.copy( nPos+1 ); + if (!aLine.isEmpty()) + { + OStringBuffer aBuffer(aLine); + while (line != rLines.end() && oddDoubleQuoteCount(aBuffer)) + { + // copy the newlines also + aBuffer.append("\n" + *line); + ++line; + } + aLine = aBuffer.makeStringAndClear(); + } + aLine = WhitespaceToSpace( aLine ); + + // #i100644# handle a missing value (actually a broken PPD) + if( aLine.isEmpty() ) + { + if( !aOption.isEmpty() && + !aUniKey.startsWith( "JCL" ) ) + eType = eInvocation; + else + eType = eQuoted; + } + // check for invocation or quoted value + else if(aLine[0] == '"') + { + aLine = aLine.copy(1); + nTransPos = aLine.indexOf('"'); + if (nTransPos == -1) + nTransPos = aLine.getLength(); + aValue = OStringToOUString(aLine.subView(0, nTransPos), RTL_TEXTENCODING_MS_1252); + // after the second doublequote can follow a / and a translation + if (nTransPos < aLine.getLength() - 2) + { + aValueTranslation = handleTranslation( aLine.copy( nTransPos+2 ), bIsGlobalizedLine ); + } + // check for quoted value + if( !aOption.isEmpty() && + !aUniKey.startsWith( "JCL" ) ) + eType = eInvocation; + else + eType = eQuoted; + } + // check for symbol value + else if(aLine[0] == '^') + { + aLine = aLine.copy(1); + aValue = OStringToOUString(aLine, RTL_TEXTENCODING_MS_1252); + eType = eSymbol; + } + else + { + // must be a string value then + // strictly this is false because string values + // can contain any whitespace which is reduced + // to one space by now + // who cares ... + nTransPos = aLine.indexOf('/'); + if (nTransPos == -1) + nTransPos = aLine.getLength(); + aValue = OStringToOUString(aLine.subView(0, nTransPos), RTL_TEXTENCODING_MS_1252); + if (nTransPos+1 < aLine.getLength()) + aValueTranslation = handleTranslation( aLine.copy( nTransPos+1 ), bIsGlobalizedLine ); + eType = eString; + } + } + + // handle globalized PPD entries + if( bIsGlobalizedLine ) + { + // handle main key translations of form: + // *ll_CC.Translation MainKeyword/translated text: "" + if( aUniKey == "Translation" ) + { + m_pTranslator->insertKey( aOption, aOptionTranslation, aTransLocale ); + } + // handle options translations of for: + // *ll_CC.MainKeyword OptionKeyword/translated text: "" + else + { + m_pTranslator->insertOption( aUniKey, aOption, aOptionTranslation, aTransLocale ); + } + continue; + } + + PPDKey* pKey = nullptr; + keyit = m_aKeys.find( aUniKey ); + if( keyit == m_aKeys.end() ) + { + pKey = new PPDKey( aUniKey ); + insertKey( std::unique_ptr(pKey) ); + } + else + pKey = keyit->second.get(); + + if( eType == eNo && bQuery ) + continue; + + PPDValue* pValue = pKey->insertValue( aOption, eType ); + if( ! pValue ) + continue; + pValue->m_aValue = aValue; + + if( !aOptionTranslation.isEmpty() ) + m_pTranslator->insertOption( aUniKey, aOption, aOptionTranslation, aTransLocale ); + if( !aValueTranslation.isEmpty() ) + m_pTranslator->insertValue( aUniKey, aOption, aValue, aValueTranslation, aTransLocale ); + + // eventually update query and remove from option list + if( bQuery && !pKey->m_bQueryValue ) + { + pKey->m_bQueryValue = true; + pKey->eraseValue( pValue->m_aOption ); + } + } + + // second pass: fill in defaults + for( const auto& aLine : rLines ) + { + if (aLine.startsWith("*Default")) + { + SAL_INFO("vcl.unx.print", "Found a default: '" << aLine << "'"); + OUString aKey(OStringToOUString(aLine.subView(8), RTL_TEXTENCODING_MS_1252)); + sal_Int32 nPos = aKey.indexOf( ':' ); + if( nPos != -1 ) + { + aKey = aKey.copy(0, nPos); + OUString aOption(OStringToOUString( + WhitespaceToSpace(aLine.subView(nPos+9)), + RTL_TEXTENCODING_MS_1252)); + keyit = m_aKeys.find( aKey ); + if( keyit != m_aKeys.end() ) + { + PPDKey* pKey = keyit->second.get(); + const PPDValue* pDefValue = pKey->getValue( aOption ); + if( pKey->m_pDefaultValue == nullptr ) + pKey->m_pDefaultValue = pDefValue; + } + else + { + // some PPDs contain defaults for keys that + // do not exist otherwise + // (example: DefaultResolution) + // so invent that key here and have a default value + std::unique_ptr pKey(new PPDKey( aKey )); + pKey->insertValue( aOption, eInvocation /*or what ?*/ ); + pKey->m_pDefaultValue = pKey->getValue( aOption ); + insertKey( std::move(pKey) ); + } + } + } + else if (aLine.startsWith("*UIConstraints") || + aLine.startsWith("*NonUIConstraints")) + { + parseConstraint( aLine ); + } + } +} + +void PPDParser::parseOpenUI(const OString& rLine, std::string_view rPPDGroup) +{ + OUString aTranslation; + OString aKey = rLine; + + sal_Int32 nPos = aKey.indexOf(':'); + if( nPos != -1 ) + aKey = aKey.copy(0, nPos); + nPos = aKey.indexOf('/'); + if( nPos != -1 ) + { + aTranslation = handleTranslation( aKey.copy( nPos + 1 ), false ); + aKey = aKey.copy(0, nPos); + } + aKey = GetCommandLineToken( 1, aKey ); + aKey = aKey.copy(1); + + OUString aUniKey(OStringToOUString(aKey, RTL_TEXTENCODING_MS_1252)); + PPDParser::hash_type::const_iterator keyit = m_aKeys.find( aUniKey ); + PPDKey* pKey; + if( keyit == m_aKeys.end() ) + { + pKey = new PPDKey( aUniKey ); + insertKey( std::unique_ptr(pKey) ); + } + else + pKey = keyit->second.get(); + + pKey->m_bUIOption = true; + m_pTranslator->insertKey( pKey->getKey(), aTranslation ); + + pKey->m_aGroup = OStringToOUString(rPPDGroup, RTL_TEXTENCODING_MS_1252); +} + +void PPDParser::parseOrderDependency(const OString& rLine) +{ + OString aLine(rLine); + sal_Int32 nPos = aLine.indexOf(':'); + if( nPos != -1 ) + aLine = aLine.copy( nPos+1 ); + + sal_Int32 nOrder = GetCommandLineToken( 0, aLine ).toInt32(); + OUString aKey(OStringToOUString(GetCommandLineToken(2, aLine), RTL_TEXTENCODING_MS_1252)); + if( aKey[ 0 ] != '*' ) + return; // invalid order dependency + aKey = aKey.replaceAt( 0, 1, u"" ); + + PPDKey* pKey; + PPDParser::hash_type::const_iterator keyit = m_aKeys.find( aKey ); + if( keyit == m_aKeys.end() ) + { + pKey = new PPDKey( aKey ); + insertKey( std::unique_ptr(pKey) ); + } + else + pKey = keyit->second.get(); + + pKey->m_nOrderDependency = nOrder; +} + +void PPDParser::parseConstraint( const OString& rLine ) +{ + bool bFailed = false; + + OUString aLine(OStringToOUString(rLine, RTL_TEXTENCODING_MS_1252)); + sal_Int32 nIdx = rLine.indexOf(':'); + if (nIdx != -1) + aLine = aLine.replaceAt(0, nIdx + 1, u""); + PPDConstraint aConstraint; + int nTokens = GetCommandLineTokenCount( aLine ); + for( int i = 0; i < nTokens; i++ ) + { + OUString aToken = GetCommandLineToken( i, aLine ); + if( !aToken.isEmpty() && aToken[ 0 ] == '*' ) + { + aToken = aToken.replaceAt( 0, 1, u"" ); + if( aConstraint.m_pKey1 ) + aConstraint.m_pKey2 = getKey( aToken ); + else + aConstraint.m_pKey1 = getKey( aToken ); + } + else + { + if( aConstraint.m_pKey2 ) + { + if( ! ( aConstraint.m_pOption2 = aConstraint.m_pKey2->getValue( aToken ) ) ) + bFailed = true; + } + else if( aConstraint.m_pKey1 ) + { + if( ! ( aConstraint.m_pOption1 = aConstraint.m_pKey1->getValue( aToken ) ) ) + bFailed = true; + } + else + // constraint for nonexistent keys; this happens + // e.g. in HP4PLUS3 + bFailed = true; + } + } + // there must be two keywords + if( ! aConstraint.m_pKey1 || ! aConstraint.m_pKey2 || bFailed ) + { + SAL_INFO("vcl.unx.print", + "Warning: constraint \"" << rLine << "\" is invalid"); + } + else + m_aConstraints.push_back( aConstraint ); +} + +OUString PPDParser::getDefaultPaperDimension() const +{ + if( m_pDefaultPaperDimension ) + return m_pDefaultPaperDimension->m_aOption; + + return OUString(); +} + +bool PPDParser::getMargins( + std::u16string_view rPaperName, + int& rLeft, int& rRight, + int& rUpper, int& rLower ) const +{ + if( ! m_pImageableAreas || ! m_pPaperDimensions ) + return false; + + int nPDim=-1, nImArea=-1, i; + for( i = 0; i < m_pImageableAreas->countValues(); i++ ) + if( rPaperName == m_pImageableAreas->getValue( i )->m_aOption ) + nImArea = i; + for( i = 0; i < m_pPaperDimensions->countValues(); i++ ) + if( rPaperName == m_pPaperDimensions->getValue( i )->m_aOption ) + nPDim = i; + if( nPDim == -1 || nImArea == -1 ) + return false; + + double ImLLx, ImLLy, ImURx, ImURy; + double PDWidth, PDHeight; + OUString aArea = m_pImageableAreas->getValue( nImArea )->m_aValue; + ImLLx = StringToDouble( GetCommandLineToken( 0, aArea ) ); + ImLLy = StringToDouble( GetCommandLineToken( 1, aArea ) ); + ImURx = StringToDouble( GetCommandLineToken( 2, aArea ) ); + ImURy = StringToDouble( GetCommandLineToken( 3, aArea ) ); + aArea = m_pPaperDimensions->getValue( nPDim )->m_aValue; + PDWidth = StringToDouble( GetCommandLineToken( 0, aArea ) ); + PDHeight = StringToDouble( GetCommandLineToken( 1, aArea ) ); + rLeft = static_cast(ImLLx + 0.5); + rLower = static_cast(ImLLy + 0.5); + rUpper = static_cast(PDHeight - ImURy + 0.5); + rRight = static_cast(PDWidth - ImURx + 0.5); + + return true; +} + +bool PPDParser::getPaperDimension( + std::u16string_view rPaperName, + int& rWidth, int& rHeight ) const +{ + if( ! m_pPaperDimensions ) + return false; + + int nPDim=-1; + for( int i = 0; i < m_pPaperDimensions->countValues(); i++ ) + if( rPaperName == m_pPaperDimensions->getValue( i )->m_aOption ) + nPDim = i; + if( nPDim == -1 ) + return false; + + double PDWidth, PDHeight; + OUString aArea = m_pPaperDimensions->getValue( nPDim )->m_aValue; + PDWidth = StringToDouble( GetCommandLineToken( 0, aArea ) ); + PDHeight = StringToDouble( GetCommandLineToken( 1, aArea ) ); + rHeight = static_cast(PDHeight + 0.5); + rWidth = static_cast(PDWidth + 0.5); + + return true; +} + +OUString PPDParser::matchPaperImpl(int nWidth, int nHeight, bool bSwaped, psp::orientation* pOrientation) const +{ + if( ! m_pPaperDimensions ) + return OUString(); + + int nPDim = -1; + double fSort = 2e36, fNewSort; + + for( int i = 0; i < m_pPaperDimensions->countValues(); i++ ) + { + OUString aArea = m_pPaperDimensions->getValue( i )->m_aValue; + double PDWidth = StringToDouble( GetCommandLineToken( 0, aArea ) ); + double PDHeight = StringToDouble( GetCommandLineToken( 1, aArea ) ); + PDWidth /= static_cast(nWidth); + PDHeight /= static_cast(nHeight); + if( PDWidth >= 0.9 && PDWidth <= 1.1 && + PDHeight >= 0.9 && PDHeight <= 1.1 ) + { + fNewSort = + (1.0-PDWidth)*(1.0-PDWidth) + (1.0-PDHeight)*(1.0-PDHeight); + if( fNewSort == 0.0 ) // perfect match + return m_pPaperDimensions->getValue( i )->m_aOption; + + if( fNewSort < fSort ) + { + fSort = fNewSort; + nPDim = i; + } + } + } + + if (nPDim == -1 && !bSwaped) + { + // swap portrait/landscape and try again + return matchPaperImpl(nHeight, nWidth, true, pOrientation); + } + + if (nPDim == -1) + return OUString(); + + if (bSwaped && pOrientation) + { + switch (*pOrientation) + { + case psp::orientation::Portrait: + *pOrientation = psp::orientation::Landscape; + break; + case psp::orientation::Landscape: + *pOrientation = psp::orientation::Portrait; + break; + } + } + + return m_pPaperDimensions->getValue( nPDim )->m_aOption; +} + +OUString PPDParser::matchPaper(int nWidth, int nHeight, psp::orientation* pOrientation) const +{ + return matchPaperImpl(nHeight, nWidth, true, pOrientation); +} + +OUString PPDParser::getDefaultInputSlot() const +{ + if( m_pDefaultInputSlot ) + return m_pDefaultInputSlot->m_aValue; + return OUString(); +} + +void PPDParser::getResolutionFromString(std::u16string_view rString, + int& rXRes, int& rYRes ) +{ + rXRes = rYRes = 300; + + const size_t nDPIPos {rString.find( u"dpi" )}; + if( nDPIPos != std::u16string_view::npos ) + { + const size_t nPos {rString.find( 'x' )}; + if( nPos != std::u16string_view::npos ) + { + rXRes = o3tl::toInt32(rString.substr( 0, nPos )); + rYRes = o3tl::toInt32(rString.substr(nPos+1, nDPIPos - nPos - 1)); + } + else + rXRes = rYRes = o3tl::toInt32(rString.substr( 0, nDPIPos )); + } +} + +void PPDParser::getDefaultResolution( int& rXRes, int& rYRes ) const +{ + if( m_pDefaultResolution ) + { + getResolutionFromString( m_pDefaultResolution->m_aValue, rXRes, rYRes ); + return; + } + + rXRes = 300; + rYRes = 300; +} + +OUString PPDParser::translateKey( const OUString& i_rKey ) const +{ + OUString aResult( m_pTranslator->translateKey( i_rKey ) ); + if( aResult.isEmpty() ) + aResult = i_rKey; + return aResult; +} + +OUString PPDParser::translateOption( std::u16string_view i_rKey, + const OUString& i_rOption ) const +{ + OUString aResult( m_pTranslator->translateOption( i_rKey, i_rOption ) ); + if( aResult.isEmpty() ) + aResult = i_rOption; + return aResult; +} + +/* + * PPDKey + */ + +PPDKey::PPDKey( OUString aKey ) : + m_aKey(std::move( aKey )), + m_pDefaultValue( nullptr ), + m_bQueryValue( false ), + m_bUIOption( false ), + m_nOrderDependency( 100 ) +{ +} + +PPDKey::~PPDKey() +{ +} + +const PPDValue* PPDKey::getValue( int n ) const +{ + return (n >= 0 && o3tl::make_unsigned(n) < m_aOrderedValues.size()) ? m_aOrderedValues[n] : nullptr; +} + +const PPDValue* PPDKey::getValue( const OUString& rOption ) const +{ + PPDKey::hash_type::const_iterator it = m_aValues.find( rOption ); + return it != m_aValues.end() ? &it->second : nullptr; +} + +const PPDValue* PPDKey::getValueCaseInsensitive( const OUString& rOption ) const +{ + const PPDValue* pValue = getValue( rOption ); + if( ! pValue ) + { + for( size_t n = 0; n < m_aOrderedValues.size() && ! pValue; n++ ) + if( m_aOrderedValues[n]->m_aOption.equalsIgnoreAsciiCase( rOption ) ) + pValue = m_aOrderedValues[n]; + } + + return pValue; +} + +void PPDKey::eraseValue( const OUString& rOption ) +{ + PPDKey::hash_type::iterator it = m_aValues.find( rOption ); + if( it == m_aValues.end() ) + return; + + auto vit = std::find(m_aOrderedValues.begin(), m_aOrderedValues.end(), &(it->second )); + if( vit != m_aOrderedValues.end() ) + m_aOrderedValues.erase( vit ); + + m_aValues.erase( it ); +} + +PPDValue* PPDKey::insertValue(const OUString& rOption, PPDValueType eType, bool bCustomOption) +{ + if( m_aValues.find( rOption ) != m_aValues.end() ) + return nullptr; + + PPDValue aValue; + aValue.m_aOption = rOption; + aValue.m_bCustomOption = bCustomOption; + aValue.m_bCustomOptionSetViaApp = false; + aValue.m_eType = eType; + m_aValues[ rOption ] = aValue; + PPDValue* pValue = &m_aValues[rOption]; + m_aOrderedValues.push_back( pValue ); + return pValue; +} + +/* + * PPDContext + */ + +PPDContext::PPDContext() : + m_pParser( nullptr ) +{ +} + +PPDContext& PPDContext::operator=( PPDContext&& rCopy ) +{ + std::swap(m_pParser, rCopy.m_pParser); + std::swap(m_aCurrentValues, rCopy.m_aCurrentValues); + return *this; +} + +const PPDKey* PPDContext::getModifiedKey( std::size_t n ) const +{ + if( m_aCurrentValues.size() <= n ) + return nullptr; + + hash_type::const_iterator it = m_aCurrentValues.begin(); + std::advance(it, n); + return it->first; +} + +void PPDContext::setParser( const PPDParser* pParser ) +{ + if( pParser != m_pParser ) + { + m_aCurrentValues.clear(); + m_pParser = pParser; + } +} + +const PPDValue* PPDContext::getValue( const PPDKey* pKey ) const +{ + if( ! m_pParser ) + return nullptr; + + hash_type::const_iterator it = m_aCurrentValues.find( pKey ); + if( it != m_aCurrentValues.end() ) + return it->second; + + if( ! m_pParser->hasKey( pKey ) ) + return nullptr; + + const PPDValue* pValue = pKey->getDefaultValue(); + if( ! pValue ) + pValue = pKey->getValue( 0 ); + + return pValue; +} + +const PPDValue* PPDContext::setValue( const PPDKey* pKey, const PPDValue* pValue, bool bDontCareForConstraints ) +{ + if( ! m_pParser || ! pKey ) + return nullptr; + + // pValue can be NULL - it means ignore this option + + if( ! m_pParser->hasKey( pKey ) ) + return nullptr; + + // check constraints + if( pValue ) + { + if( bDontCareForConstraints ) + { + m_aCurrentValues[ pKey ] = pValue; + } + else if( checkConstraints( pKey, pValue, true ) ) + { + m_aCurrentValues[ pKey ] = pValue; + + // after setting this value, check all constraints ! + hash_type::iterator it = m_aCurrentValues.begin(); + while( it != m_aCurrentValues.end() ) + { + if( it->first != pKey && + ! checkConstraints( it->first, it->second, false ) ) + { + SAL_INFO("vcl.unx.print", "PPDContext::setValue: option " + << it->first->getKey() + << " (" << it->second->m_aOption + << ") is constrained after setting " + << pKey->getKey() + << " to " << pValue->m_aOption); + resetValue( it->first, true ); + it = m_aCurrentValues.begin(); + } + else + ++it; + } + } + } + else + m_aCurrentValues[ pKey ] = nullptr; + + return pValue; +} + +bool PPDContext::checkConstraints( const PPDKey* pKey, const PPDValue* pValue ) +{ + if( ! m_pParser || ! pKey || ! pValue ) + return false; + + // ensure that this key is already in the list if it exists at all + if( m_aCurrentValues.find( pKey ) != m_aCurrentValues.end() ) + return checkConstraints( pKey, pValue, false ); + + // it is not in the list, insert it temporarily + bool bRet = false; + if( m_pParser->hasKey( pKey ) ) + { + const PPDValue* pDefValue = pKey->getDefaultValue(); + m_aCurrentValues[ pKey ] = pDefValue; + bRet = checkConstraints( pKey, pValue, false ); + m_aCurrentValues.erase( pKey ); + } + + return bRet; +} + +bool PPDContext::resetValue( const PPDKey* pKey, bool bDefaultable ) +{ + if( ! pKey || ! m_pParser || ! m_pParser->hasKey( pKey ) ) + return false; + + const PPDValue* pResetValue = pKey->getValue( "None" ); + if( ! pResetValue ) + pResetValue = pKey->getValue( "False" ); + if( ! pResetValue && bDefaultable ) + pResetValue = pKey->getDefaultValue(); + + bool bRet = pResetValue && ( setValue( pKey, pResetValue ) == pResetValue ); + + return bRet; +} + +bool PPDContext::checkConstraints( const PPDKey* pKey, const PPDValue* pNewValue, bool bDoReset ) +{ + if( ! pNewValue ) + return true; + + // sanity checks + if( ! m_pParser ) + return false; + + if( pKey->getValue( pNewValue->m_aOption ) != pNewValue ) + return false; + + // None / False and the default can always be set, but be careful ! + // setting them might influence constrained values + if( pNewValue->m_aOption == "None" || pNewValue->m_aOption == "False" || + pNewValue == pKey->getDefaultValue() ) + return true; + + const ::std::vector< PPDParser::PPDConstraint >& rConstraints( m_pParser->getConstraints() ); + for (auto const& constraint : rConstraints) + { + const PPDKey* pLeft = constraint.m_pKey1; + const PPDKey* pRight = constraint.m_pKey2; + if( ! pLeft || ! pRight || ( pKey != pLeft && pKey != pRight ) ) + continue; + + const PPDKey* pOtherKey = pKey == pLeft ? pRight : pLeft; + const PPDValue* pOtherKeyOption = pKey == pLeft ? constraint.m_pOption2 : constraint.m_pOption1; + const PPDValue* pKeyOption = pKey == pLeft ? constraint.m_pOption1 : constraint.m_pOption2; + + // syntax *Key1 option1 *Key2 option2 + if( pKeyOption && pOtherKeyOption ) + { + if( pNewValue != pKeyOption ) + continue; + if( pOtherKeyOption == getValue( pOtherKey ) ) + { + return false; + } + } + // syntax *Key1 option *Key2 or *Key1 *Key2 option + else if( pOtherKeyOption || pKeyOption ) + { + if( pKeyOption ) + { + if( ! ( pOtherKeyOption = getValue( pOtherKey ) ) ) + continue; // this should not happen, PPD broken + + if( pKeyOption == pNewValue && + pOtherKeyOption->m_aOption != "None" && + pOtherKeyOption->m_aOption != "False" ) + { + // check if the other value can be reset and + // do so if possible + if( bDoReset && resetValue( pOtherKey ) ) + continue; + + return false; + } + } + else if( pOtherKeyOption ) + { + if( getValue( pOtherKey ) == pOtherKeyOption && + pNewValue->m_aOption != "None" && + pNewValue->m_aOption != "False" ) + return false; + } + else + { + // this should not happen, PPD is broken + } + } + // syntax *Key1 *Key2 + else + { + const PPDValue* pOtherValue = getValue( pOtherKey ); + if( pOtherValue->m_aOption != "None" && + pOtherValue->m_aOption != "False" && + pNewValue->m_aOption != "None" && + pNewValue->m_aOption != "False" ) + return false; + } + } + return true; +} + +char* PPDContext::getStreamableBuffer( sal_uLong& rBytes ) const +{ + rBytes = 0; + if( m_aCurrentValues.empty() ) + return nullptr; + for (auto const& elem : m_aCurrentValues) + { + OString aCopy(OUStringToOString(elem.first->getKey(), RTL_TEXTENCODING_MS_1252)); + rBytes += aCopy.getLength(); + rBytes += 1; // for ':' + if( elem.second ) + { + aCopy = OUStringToOString(elem.second->m_aOption, RTL_TEXTENCODING_MS_1252); + rBytes += aCopy.getLength(); + } + else + rBytes += 4; + rBytes += 1; // for '\0' + } + rBytes += 1; + char* pBuffer = new char[ rBytes ]; + memset( pBuffer, 0, rBytes ); + char* pRun = pBuffer; + for (auto const& elem : m_aCurrentValues) + { + OString aCopy(OUStringToOString(elem.first->getKey(), RTL_TEXTENCODING_MS_1252)); + int nBytes = aCopy.getLength(); + memcpy( pRun, aCopy.getStr(), nBytes ); + pRun += nBytes; + *pRun++ = ':'; + if( elem.second ) + aCopy = OUStringToOString(elem.second->m_aOption, RTL_TEXTENCODING_MS_1252); + else + aCopy = "*nil"_ostr; + nBytes = aCopy.getLength(); + memcpy( pRun, aCopy.getStr(), nBytes ); + pRun += nBytes; + + *pRun++ = 0; + } + return pBuffer; +} + +void PPDContext::rebuildFromStreamBuffer(const std::vector &rBuffer) +{ + if( ! m_pParser ) + return; + + m_aCurrentValues.clear(); + + const size_t nBytes = rBuffer.size() - 1; + size_t nRun = 0; + while (nRun < nBytes && rBuffer[nRun]) + { + OString aLine(rBuffer.data() + nRun); + sal_Int32 nPos = aLine.indexOf(':'); + if( nPos != -1 ) + { + const PPDKey* pKey = m_pParser->getKey( OStringToOUString( aLine.subView( 0, nPos ), RTL_TEXTENCODING_MS_1252 ) ); + if( pKey ) + { + const PPDValue* pValue = nullptr; + OUString aOption( + OStringToOUString(aLine.subView(nPos+1), RTL_TEXTENCODING_MS_1252)); + if (aOption != "*nil") + pValue = pKey->getValue( aOption ); + m_aCurrentValues[ pKey ] = pValue; + SAL_INFO("vcl.unx.print", + "PPDContext::rebuildFromStreamBuffer: read PPDKeyValue { " + << pKey->getKey() << " , " + << (pValue ? aOption : "") + << " }"); + } + } + nRun += aLine.getLength()+1; + } +} + +int PPDContext::getRenderResolution() const +{ + // initialize to reasonable default, if parser is not set + int nDPI = 300; + if( m_pParser ) + { + int nDPIx = 300, nDPIy = 300; + const PPDKey* pKey = m_pParser->getKey( "Resolution" ); + if( pKey ) + { + const PPDValue* pValue = getValue( pKey ); + if( pValue ) + PPDParser::getResolutionFromString( pValue->m_aOption, nDPIx, nDPIy ); + else + m_pParser->getDefaultResolution( nDPIx, nDPIy ); + } + else + m_pParser->getDefaultResolution( nDPIx, nDPIy ); + + nDPI = std::max(nDPIx, nDPIy); + } + return nDPI; +} + +void PPDContext::getPageSize( OUString& rPaper, int& rWidth, int& rHeight ) const +{ + // initialize to reasonable default, if parser is not set + rPaper = "A4"; + rWidth = 595; + rHeight = 842; + if( !m_pParser ) + return; + + const PPDKey* pKey = m_pParser->getKey( "PageSize" ); + if( !pKey ) + return; + + const PPDValue* pValue = getValue( pKey ); + if( pValue ) + { + rPaper = pValue->m_aOption; + m_pParser->getPaperDimension( rPaper, rWidth, rHeight ); + } + else + { + rPaper = m_pParser->getDefaultPaperDimension(); + m_pParser->getDefaultPaperDimension( rWidth, rHeight ); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/printer/printerinfomanager.cxx b/vcl/unx/generic/printer/printerinfomanager.cxx new file mode 100644 index 0000000000..e920051364 --- /dev/null +++ b/vcl/unx/generic/printer/printerinfomanager.cxx @@ -0,0 +1,868 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include + +// filename of configuration files +constexpr OUString PRINT_FILENAME = u"psprint.conf"_ustr; +// the group of the global defaults +constexpr OString GLOBAL_DEFAULTS_GROUP = "__Global_Printer_Defaults__"_ostr; + +#include +#include +#include + +using namespace psp; +using namespace osl; + +namespace psp +{ + class SystemQueueInfo final : public Thread + { + mutable std::mutex m_aMutex; + bool m_bChanged; + std::vector< PrinterInfoManager::SystemPrintQueue > + m_aQueues; + OUString m_aCommand; + + virtual void SAL_CALL run() override; + + public: + SystemQueueInfo(); + virtual ~SystemQueueInfo() override; + + bool hasChanged() const; + OUString getCommand() const; + + // sets changed status to false; therefore not const + void getSystemQueues( std::vector< PrinterInfoManager::SystemPrintQueue >& rQueues ); + }; +} // namespace + +/* +* class PrinterInfoManager +*/ + +PrinterInfoManager& PrinterInfoManager::get() +{ + // can't move to GenericUnixSalData, because of vcl/null/printerinfomanager.cxx + GenericUnixSalData* pSalData = GetGenericUnixSalData(); + PrinterInfoManager* pPIM = pSalData->m_pPrinterInfoManager.get(); + if (pPIM) + return *pPIM; + + pPIM = CPDManager::tryLoadCPD(); + if (!pPIM) + pPIM = CUPSManager::tryLoadCUPS(); + if (!pPIM) + pPIM = new PrinterInfoManager(); + pSalData->m_pPrinterInfoManager.reset(pPIM); + pPIM->initialize(); + + SAL_INFO("vcl.unx.print", "created PrinterInfoManager of type " + << static_cast(pPIM->getType())); + return *pPIM; +} + +PrinterInfoManager::PrinterInfoManager( Type eType ) : + m_eType( eType ), + m_aSystemDefaultPaper( "A4" ) +{ + if( eType == Type::Default ) + m_pQueueInfo.reset( new SystemQueueInfo ); + + m_aSystemDefaultPaper = OStringToOUString( + PaperInfo::toPSName(PaperInfo::getSystemDefaultPaper().getPaper()), + RTL_TEXTENCODING_UTF8); +} + +PrinterInfoManager::~PrinterInfoManager() +{ +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.print", "PrinterInfoManager: " + << "destroyed Manager of type " + << ((int) getType())); +#endif +} + +bool PrinterInfoManager::checkPrintersChanged( bool bWait ) +{ + // check if files were created, deleted or modified since initialize() + bool bChanged = false; + for (auto const& watchFile : m_aWatchFiles) + { + DirectoryItem aItem; + if( DirectoryItem::get( watchFile.m_aFilePath, aItem ) ) + { + if( watchFile.m_aModified.Seconds != 0 ) + { + bChanged = true; // file probably has vanished + break; + } + } + else + { + FileStatus aStatus( osl_FileStatus_Mask_ModifyTime ); + if( aItem.getFileStatus( aStatus ) ) + { + bChanged = true; // unlikely but not impossible + break; + } + else + { + TimeValue aModified = aStatus.getModifyTime(); + if( aModified.Seconds != watchFile.m_aModified.Seconds ) + { + bChanged = true; + break; + } + } + } + } + + if( bWait && m_pQueueInfo ) + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.print", "syncing printer discovery thread."); +#endif + m_pQueueInfo->join(); +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.print", "done: syncing printer discovery thread."); +#endif + } + + if( ! bChanged && m_pQueueInfo ) + bChanged = m_pQueueInfo->hasChanged(); + if( bChanged ) + { + initialize(); + } + + return bChanged; +} + +void PrinterInfoManager::initialize() +{ + m_aPrinters.clear(); + m_aWatchFiles.clear(); + OUString aDefaultPrinter; + + // first initialize the global defaults + // have to iterate over all possible files + // there should be only one global setup section in all + // available config files + m_aGlobalDefaults = PrinterInfo(); + + // need a parser for the PPDContext. generic printer should do. + m_aGlobalDefaults.m_pParser = PPDParser::getParser( "SGENPRT" ); + m_aGlobalDefaults.m_aContext.setParser( m_aGlobalDefaults.m_pParser ); + + if( ! m_aGlobalDefaults.m_pParser ) + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.print", "Error: no default PPD file " + << "SGENPRT available, shutting down psprint..."); +#endif + return; + } + + std::vector< OUString > aDirList; + psp::getPrinterPathList( aDirList, nullptr ); + for (auto const& printDir : aDirList) + { + INetURLObject aFile( printDir, INetProtocol::File, INetURLObject::EncodeMechanism::All ); + aFile.Append( PRINT_FILENAME ); + Config aConfig( aFile.PathToFileName() ); + if( aConfig.HasGroup( GLOBAL_DEFAULTS_GROUP ) ) + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.print", "found global defaults in " + << aFile.PathToFileName()); +#endif + aConfig.SetGroup( GLOBAL_DEFAULTS_GROUP ); + + OString aValue( aConfig.ReadKey( "Copies"_ostr ) ); + if (!aValue.isEmpty()) + m_aGlobalDefaults.m_nCopies = aValue.toInt32(); + + aValue = aConfig.ReadKey( "Orientation"_ostr ); + if (!aValue.isEmpty()) + m_aGlobalDefaults.m_eOrientation = aValue.equalsIgnoreAsciiCase("Landscape") ? orientation::Landscape : orientation::Portrait; + + aValue = aConfig.ReadKey( "MarginAdjust"_ostr ); + if (!aValue.isEmpty()) + { + sal_Int32 nIdx {0}; + m_aGlobalDefaults.m_nLeftMarginAdjust = o3tl::toInt32(o3tl::getToken(aValue, 0, ',', nIdx)); + m_aGlobalDefaults.m_nRightMarginAdjust = o3tl::toInt32(o3tl::getToken(aValue, 0, ',', nIdx)); + m_aGlobalDefaults.m_nTopMarginAdjust = o3tl::toInt32(o3tl::getToken(aValue, 0, ',', nIdx)); + m_aGlobalDefaults.m_nBottomMarginAdjust = o3tl::toInt32(o3tl::getToken(aValue, 0, ',', nIdx)); + } + + aValue = aConfig.ReadKey( "ColorDepth"_ostr, "24"_ostr ); + if (!aValue.isEmpty()) + m_aGlobalDefaults.m_nColorDepth = aValue.toInt32(); + + aValue = aConfig.ReadKey( "ColorDevice"_ostr ); + if (!aValue.isEmpty()) + m_aGlobalDefaults.m_nColorDevice = aValue.toInt32(); + + // get the PPDContext of global JobData + for( int nKey = 0; nKey < aConfig.GetKeyCount(); ++nKey ) + { + OString aKey( aConfig.GetKeyName( nKey ) ); + if (aKey.startsWith("PPD_")) + { + aValue = aConfig.ReadKey( aKey ); + const PPDKey* pKey = m_aGlobalDefaults.m_pParser->getKey(OStringToOUString(aKey.subView(4), RTL_TEXTENCODING_ISO_8859_1)); + if( pKey ) + { + m_aGlobalDefaults.m_aContext. + setValue( pKey, + aValue == "*nil" ? nullptr : pKey->getValue(OStringToOUString(aValue, RTL_TEXTENCODING_ISO_8859_1)), + true ); + } + } + } + } + } + setDefaultPaper( m_aGlobalDefaults.m_aContext ); + + // now collect all available printers + for (auto const& printDir : aDirList) + { + INetURLObject aDir( printDir, INetProtocol::File, INetURLObject::EncodeMechanism::All ); + INetURLObject aFile( aDir ); + aFile.Append( PRINT_FILENAME ); + + // check directory validity + OUString aUniPath; + FileBase::getFileURLFromSystemPath( aDir.PathToFileName(), aUniPath ); + Directory aDirectory( aUniPath ); + if( aDirectory.open() ) + continue; + aDirectory.close(); + + FileBase::getFileURLFromSystemPath( aFile.PathToFileName(), aUniPath ); + FileStatus aStatus( osl_FileStatus_Mask_ModifyTime ); + DirectoryItem aItem; + + // setup WatchFile list + WatchFile aWatchFile; + aWatchFile.m_aFilePath = aUniPath; + if( ! DirectoryItem::get( aUniPath, aItem ) && + ! aItem.getFileStatus( aStatus ) ) + { + aWatchFile.m_aModified = aStatus.getModifyTime(); + } + else + { + aWatchFile.m_aModified.Seconds = 0; + aWatchFile.m_aModified.Nanosec = 0; + } + m_aWatchFiles.push_back( aWatchFile ); + + Config aConfig( aFile.PathToFileName() ); + for( int nGroup = 0; nGroup < aConfig.GetGroupCount(); nGroup++ ) + { + aConfig.SetGroup( aConfig.GetGroupName( nGroup ) ); + OString aValue = aConfig.ReadKey( "Printer"_ostr ); + if (!aValue.isEmpty()) + { + OUString aPrinterName; + + sal_Int32 nNamePos = aValue.indexOf('/'); + // check for valid value of "Printer" + if (nNamePos == -1) + continue; + + Printer aPrinter; + // initialize to global defaults + aPrinter.m_aInfo = m_aGlobalDefaults; + + aPrinterName = OStringToOUString(aValue.subView(nNamePos+1), + RTL_TEXTENCODING_UTF8); + aPrinter.m_aInfo.m_aPrinterName = aPrinterName; + aPrinter.m_aInfo.m_aDriverName = OStringToOUString(aValue.subView(0, nNamePos), RTL_TEXTENCODING_UTF8); + + // set parser, merge settings + // don't do this for CUPS printers as this is done + // by the CUPS system itself + if( !aPrinter.m_aInfo.m_aDriverName.startsWith( "CUPS:" ) ) + { + aPrinter.m_aInfo.m_pParser = PPDParser::getParser( aPrinter.m_aInfo.m_aDriverName ); + aPrinter.m_aInfo.m_aContext.setParser( aPrinter.m_aInfo.m_pParser ); + // note: setParser also purges the context + + // ignore this printer if its driver is not found + if( ! aPrinter.m_aInfo.m_pParser ) + continue; + + // merge the ppd context keys if the printer has the same keys and values + // this is a bit tricky, since it involves mixing two PPDs + // without constraints which might end up badly + // this feature should be use with caution + // it is mainly to select default paper sizes for new printers + for( std::size_t nPPDValueModified = 0; nPPDValueModified < m_aGlobalDefaults.m_aContext.countValuesModified(); nPPDValueModified++ ) + { + const PPDKey* pDefKey = m_aGlobalDefaults.m_aContext.getModifiedKey( nPPDValueModified ); + const PPDValue* pDefValue = m_aGlobalDefaults.m_aContext.getValue( pDefKey ); + const PPDKey* pPrinterKey = pDefKey ? aPrinter.m_aInfo.m_pParser->getKey( pDefKey->getKey() ) : nullptr; + if( pDefKey && pPrinterKey ) + // at least the options exist in both PPDs + { + if( pDefValue ) + { + const PPDValue* pPrinterValue = pPrinterKey->getValue( pDefValue->m_aOption ); + if( pPrinterValue ) + // the printer has a corresponding option for the key + aPrinter.m_aInfo.m_aContext.setValue( pPrinterKey, pPrinterValue ); + } + else + aPrinter.m_aInfo.m_aContext.setValue( pPrinterKey, nullptr ); + } + } + + aValue = aConfig.ReadKey( "Command"_ostr ); + // no printer without a command + if (aValue.isEmpty()) + { + /* TODO: + * porters: please append your platform to the Solaris + * case if your platform has SystemV printing per default. + */ + #if defined __sun + aValue = "lp"; + #else + aValue = "lpr"_ostr; + #endif + } + aPrinter.m_aInfo.m_aCommand = OStringToOUString(aValue, RTL_TEXTENCODING_UTF8); + } + + aValue = aConfig.ReadKey( "QuickCommand"_ostr ); + aPrinter.m_aInfo.m_aQuickCommand = OStringToOUString(aValue, RTL_TEXTENCODING_UTF8); + + aValue = aConfig.ReadKey( "Features"_ostr ); + aPrinter.m_aInfo.m_aFeatures = OStringToOUString(aValue, RTL_TEXTENCODING_UTF8); + + // override the settings in m_aGlobalDefaults if keys exist + aValue = aConfig.ReadKey( "DefaultPrinter"_ostr ); + if (aValue != "0" && !aValue.equalsIgnoreAsciiCase("false")) + aDefaultPrinter = aPrinterName; + + aValue = aConfig.ReadKey( "Location"_ostr ); + aPrinter.m_aInfo.m_aLocation = OStringToOUString(aValue, RTL_TEXTENCODING_UTF8); + + aValue = aConfig.ReadKey( "Comment"_ostr ); + aPrinter.m_aInfo.m_aComment = OStringToOUString(aValue, RTL_TEXTENCODING_UTF8); + + aValue = aConfig.ReadKey( "Copies"_ostr ); + if (!aValue.isEmpty()) + aPrinter.m_aInfo.m_nCopies = aValue.toInt32(); + + aValue = aConfig.ReadKey( "Orientation"_ostr ); + if (!aValue.isEmpty()) + aPrinter.m_aInfo.m_eOrientation = aValue.equalsIgnoreAsciiCase("Landscape") ? orientation::Landscape : orientation::Portrait; + + aValue = aConfig.ReadKey( "MarginAdjust"_ostr ); + if (!aValue.isEmpty()) + { + sal_Int32 nIdx {0}; + aPrinter.m_aInfo.m_nLeftMarginAdjust = o3tl::toInt32(o3tl::getToken(aValue, 0, ',', nIdx)); + aPrinter.m_aInfo.m_nRightMarginAdjust = o3tl::toInt32(o3tl::getToken(aValue, 0, ',', nIdx)); + aPrinter.m_aInfo.m_nTopMarginAdjust = o3tl::toInt32(o3tl::getToken(aValue, 0, ',', nIdx)); + aPrinter.m_aInfo.m_nBottomMarginAdjust = o3tl::toInt32(o3tl::getToken(aValue, 0, ',', nIdx)); + } + + aValue = aConfig.ReadKey( "ColorDepth"_ostr ); + if (!aValue.isEmpty()) + aPrinter.m_aInfo.m_nColorDepth = aValue.toInt32(); + + aValue = aConfig.ReadKey( "ColorDevice"_ostr ); + if (!aValue.isEmpty()) + aPrinter.m_aInfo.m_nColorDevice = aValue.toInt32(); + + // now iterate over all keys to extract multi key information: + // 1. PPDContext information + for( int nKey = 0; nKey < aConfig.GetKeyCount(); ++nKey ) + { + OString aKey( aConfig.GetKeyName( nKey ) ); + if( aKey.startsWith("PPD_") && aPrinter.m_aInfo.m_pParser ) + { + aValue = aConfig.ReadKey( aKey ); + const PPDKey* pKey = aPrinter.m_aInfo.m_pParser->getKey(OStringToOUString(aKey.subView(4), RTL_TEXTENCODING_ISO_8859_1)); + if( pKey ) + { + aPrinter.m_aInfo.m_aContext. + setValue( pKey, + aValue == "*nil" ? nullptr : pKey->getValue(OStringToOUString(aValue, RTL_TEXTENCODING_ISO_8859_1)), + true ); + } + } + } + + setDefaultPaper( aPrinter.m_aInfo.m_aContext ); + + // finally insert printer + FileBase::getFileURLFromSystemPath( aFile.PathToFileName(), aPrinter.m_aFile ); + std::unordered_map< OUString, Printer >::const_iterator find_it = + m_aPrinters.find( aPrinterName ); + if( find_it != m_aPrinters.end() ) + { + aPrinter.m_aAlternateFiles = find_it->second.m_aAlternateFiles; + aPrinter.m_aAlternateFiles.insert( find_it->second.m_aFile ); + } + m_aPrinters[ aPrinterName ] = aPrinter; + } + } + } + + // set default printer + if( !m_aPrinters.empty() ) + { + if( m_aPrinters.find( aDefaultPrinter ) == m_aPrinters.end() ) + aDefaultPrinter = m_aPrinters.begin()->first; + } + else + aDefaultPrinter.clear(); + m_aDefaultPrinter = aDefaultPrinter; + + if( m_eType != Type::Default ) + return; + + // add a default printer for every available print queue + // merge paper default printer, all else from global defaults + PrinterInfo aMergeInfo( m_aGlobalDefaults ); + aMergeInfo.m_aDriverName = "SGENPRT"; + aMergeInfo.m_aFeatures = "autoqueue"; + + if( !m_aDefaultPrinter.isEmpty() ) + { + PrinterInfo aDefaultInfo( getPrinterInfo( m_aDefaultPrinter ) ); + + const PPDKey* pDefKey = aDefaultInfo.m_pParser->getKey( "PageSize" ); + const PPDKey* pMergeKey = aMergeInfo.m_pParser->getKey( "PageSize" ); + const PPDValue* pDefValue = aDefaultInfo.m_aContext.getValue( pDefKey ); + const PPDValue* pMergeValue = pMergeKey ? pMergeKey->getValue( pDefValue->m_aOption ) : nullptr; + if( pMergeKey && pMergeValue ) + aMergeInfo.m_aContext.setValue( pMergeKey, pMergeValue ); + } + + if( m_pQueueInfo && m_pQueueInfo->hasChanged() ) + { + m_aSystemPrintCommand = m_pQueueInfo->getCommand(); + m_pQueueInfo->getSystemQueues( m_aSystemPrintQueues ); + m_pQueueInfo.reset(); + } + for (auto const& printQueue : m_aSystemPrintQueues) + { + OUString aPrinterName = "<" + printQueue.m_aQueue + ">"; + + if( m_aPrinters.find( aPrinterName ) != m_aPrinters.end() ) + // probably user made this one permanent + continue; + + OUString aCmd( m_aSystemPrintCommand ); + aCmd = aCmd.replaceAll( "(PRINTER)", printQueue.m_aQueue ); + + Printer aPrinter; + + // initialize to merged defaults + aPrinter.m_aInfo = aMergeInfo; + aPrinter.m_aInfo.m_aPrinterName = aPrinterName; + aPrinter.m_aInfo.m_aCommand = aCmd; + aPrinter.m_aInfo.m_aComment = printQueue.m_aComment; + aPrinter.m_aInfo.m_aLocation = printQueue.m_aLocation; + + m_aPrinters[ aPrinterName ] = aPrinter; + } +} + +void PrinterInfoManager::listPrinters( ::std::vector< OUString >& rVector ) const +{ + rVector.clear(); + for (auto const& printer : m_aPrinters) + rVector.push_back(printer.first); +} + +const PrinterInfo& PrinterInfoManager::getPrinterInfo( const OUString& rPrinter ) const +{ + static PrinterInfo aEmptyInfo; + std::unordered_map< OUString, Printer >::const_iterator it = m_aPrinters.find( rPrinter ); + + SAL_WARN_IF( it == m_aPrinters.end(), "vcl", "Do not ask for info about nonexistent printers" ); + + return it != m_aPrinters.end() ? it->second.m_aInfo : aEmptyInfo; +} + +bool PrinterInfoManager::checkFeatureToken( const OUString& rPrinterName, std::string_view pToken ) const +{ + const PrinterInfo& rPrinterInfo( getPrinterInfo( rPrinterName ) ); + sal_Int32 nIndex = 0; + while( nIndex != -1 ) + { + OUString aOuterToken = rPrinterInfo.m_aFeatures.getToken( 0, ',', nIndex ); + if( aOuterToken.getToken( 0, '=' ).equalsIgnoreAsciiCaseAscii( pToken ) ) + return true; + } + return false; +} + +FILE* PrinterInfoManager::startSpool( const OUString& rPrintername, bool bQuickCommand ) +{ + const PrinterInfo& rPrinterInfo = getPrinterInfo (rPrintername); + const OUString& rCommand = (bQuickCommand && !rPrinterInfo.m_aQuickCommand.isEmpty() ) ? + rPrinterInfo.m_aQuickCommand : rPrinterInfo.m_aCommand; + OString aShellCommand = OUStringToOString (rCommand, RTL_TEXTENCODING_ISO_8859_1) + + " 2>/dev/null"; + + return popen (aShellCommand.getStr(), "w"); +} + +bool PrinterInfoManager::endSpool( const OUString& /*rPrintername*/, const OUString& /*rJobTitle*/, FILE* pFile, const JobData& /*rDocumentJobData*/, bool /*bBanner*/, const OUString& /*rFaxNumber*/ ) +{ + return (0 == pclose( pFile )); +} + +void PrinterInfoManager::setupJobContextData( JobData& rData ) +{ + std::unordered_map< OUString, Printer >::iterator it = + m_aPrinters.find( rData.m_aPrinterName ); + if( it != m_aPrinters.end() ) + { + rData.m_pParser = it->second.m_aInfo.m_pParser; + rData.m_aContext = it->second.m_aInfo.m_aContext; + } +} + +void PrinterInfoManager::setDefaultPaper( PPDContext& rContext ) const +{ + if( ! rContext.getParser() ) + return; + + const PPDKey* pPageSizeKey = rContext.getParser()->getKey( "PageSize" ); + if( ! pPageSizeKey ) + return; + + std::size_t nModified = rContext.countValuesModified(); + auto set = false; + for (std::size_t i = 0; i != nModified; ++i) { + if (rContext.getModifiedKey(i) == pPageSizeKey) { + set = true; + break; + } + } + + if( set ) // paper was set already, do not modify + { +#if OSL_DEBUG_LEVEL > 1 + SAL_WARN("vcl.unx.print", "not setting default paper, already set " + << rContext.getValue( pPageSizeKey )->m_aOption); +#endif + return; + } + + // paper not set, fill in default value + const PPDValue* pPaperVal = nullptr; + int nValues = pPageSizeKey->countValues(); + for( int i = 0; i < nValues && ! pPaperVal; i++ ) + { + const PPDValue* pVal = pPageSizeKey->getValue( i ); + if( pVal->m_aOption.equalsIgnoreAsciiCase( m_aSystemDefaultPaper ) ) + pPaperVal = pVal; + } + if( pPaperVal ) + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.print", "setting default paper " + << pPaperVal->m_aOption); +#endif + rContext.setValue( pPageSizeKey, pPaperVal ); +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.print", "-> got paper " + << rContext.getValue( pPageSizeKey )->m_aOption); +#endif + } +} + +SystemQueueInfo::SystemQueueInfo() : + m_bChanged( false ) +{ + create(); +} + +SystemQueueInfo::~SystemQueueInfo() +{ + static const char* pNoSyncDetection = getenv( "SAL_DISABLE_SYNCHRONOUS_PRINTER_DETECTION" ); + if( ! pNoSyncDetection || !*pNoSyncDetection ) + join(); + else + terminate(); +} + +bool SystemQueueInfo::hasChanged() const +{ + std::unique_lock aGuard( m_aMutex ); + return m_bChanged; +} + +void SystemQueueInfo::getSystemQueues( std::vector< PrinterInfoManager::SystemPrintQueue >& rQueues ) +{ + std::unique_lock aGuard( m_aMutex ); + rQueues = m_aQueues; + m_bChanged = false; +} + +OUString SystemQueueInfo::getCommand() const +{ + std::unique_lock aGuard( m_aMutex ); + return m_aCommand; +} + +namespace { + +struct SystemCommandParameters; + +} + +typedef void(* tokenHandler)(const std::vector< OString >&, + std::vector< PrinterInfoManager::SystemPrintQueue >&, + const SystemCommandParameters*); + +namespace { + +struct SystemCommandParameters +{ + const char* pQueueCommand; + const char* pPrintCommand; + const char* pForeToken; + const char* pAftToken; + unsigned int nForeTokenCount; + tokenHandler pHandler; +}; + +} + +#if ! (defined(LINUX) || defined(NETBSD) || defined(FREEBSD) || defined(OPENBSD)) +static void lpgetSysQueueTokenHandler( + const std::vector< OString >& i_rLines, + std::vector< PrinterInfoManager::SystemPrintQueue >& o_rQueues, + const SystemCommandParameters* ) +{ + rtl_TextEncoding aEncoding = osl_getThreadTextEncoding(); + std::unordered_set< OUString > aUniqueSet; + std::unordered_set< OUString > aOnlySet; + aUniqueSet.insert( OUString( "_all" ) ); + aUniqueSet.insert( OUString( "_default" ) ); + + // the eventual "all" attribute of the "_all" queue tells us, which + // printers are to be used for this user at all + + // find _all: line + OString aAllLine( "_all:" ); + OString aAllAttr( "all=" ); + auto it = std::find_if(i_rLines.begin(), i_rLines.end(), + [&aAllLine](const OString& rLine) { return rLine.indexOf( aAllLine, 0 ) == 0; }); + if( it != i_rLines.end() ) + { + // now find the "all" attribute + ++it; + it = std::find_if(it, i_rLines.end(), + [&aAllAttr](const OString& rLine) { return WhitespaceToSpace( rLine ).startsWith( aAllAttr ); }); + if( it != i_rLines.end() ) + { + // insert the comma separated entries into the set of printers to use + OString aClean( WhitespaceToSpace( *it ) ); + sal_Int32 nPos = aAllAttr.getLength(); + while( nPos != -1 ) + { + OString aTok( aClean.getToken( 0, ',', nPos ) ); + if( !aTok.isEmpty() ) + aOnlySet.insert( OStringToOUString( aTok, aEncoding ) ); + } + } + } + + bool bInsertAttribute = false; + OString aDescrStr( "description=" ); + OString aLocStr( "location=" ); + for (auto const& line : i_rLines) + { + sal_Int32 nPos = 0; + // find the begin of a new printer section + nPos = line.indexOf( ':', 0 ); + if( nPos != -1 ) + { + OUString aSysQueue( OStringToOUString( line.copy( 0, nPos ), aEncoding ) ); + // do not insert duplicates (e.g. lpstat tends to produce such lines) + // in case there was a "_all" section, insert only those printer explicitly + // set in the "all" attribute + if( aUniqueSet.find( aSysQueue ) == aUniqueSet.end() && + ( aOnlySet.empty() || aOnlySet.find( aSysQueue ) != aOnlySet.end() ) + ) + { + o_rQueues.push_back( PrinterInfoManager::SystemPrintQueue() ); + o_rQueues.back().m_aQueue = aSysQueue; + o_rQueues.back().m_aLocation = aSysQueue; + aUniqueSet.insert( aSysQueue ); + bInsertAttribute = true; + } + else + bInsertAttribute = false; + continue; + } + if( bInsertAttribute && ! o_rQueues.empty() ) + { + // look for "description" attribute, insert as comment + nPos = line.indexOf( aDescrStr, 0 ); + if( nPos != -1 ) + { + OString aComment( WhitespaceToSpace( line.copy(nPos+12) ) ); + if( !aComment.isEmpty() ) + o_rQueues.back().m_aComment = OStringToOUString(aComment, aEncoding); + continue; + } + // look for "location" attribute, insert as location + nPos = line.indexOf( aLocStr, 0 ); + if( nPos != -1 ) + { + OString aLoc( WhitespaceToSpace( line.copy(nPos+9) ) ); + if( !aLoc.isEmpty() ) + o_rQueues.back().m_aLocation = OStringToOUString(aLoc, aEncoding); + continue; + } + } + } +} +#endif +static void standardSysQueueTokenHandler( + const std::vector< OString >& i_rLines, + std::vector< PrinterInfoManager::SystemPrintQueue >& o_rQueues, + const SystemCommandParameters* i_pParms) +{ + rtl_TextEncoding aEncoding = osl_getThreadTextEncoding(); + std::unordered_set< OUString > aUniqueSet; + OString aForeToken( i_pParms->pForeToken ); + OString aAftToken( i_pParms->pAftToken ); + /* Normal Unix print queue discovery, also used for Darwin 5 LPR printing + */ + for (auto const& line : i_rLines) + { + sal_Int32 nPos = 0; + + // search for a line describing a printer: + // find if there are enough tokens before the name + for( unsigned int i = 0; i < i_pParms->nForeTokenCount && nPos != -1; i++ ) + { + nPos = line.indexOf( aForeToken, nPos ); + if( nPos != -1 && line.getLength() >= nPos+aForeToken.getLength() ) + nPos += aForeToken.getLength(); + } + if( nPos != -1 ) + { + // find if there is the token after the queue + sal_Int32 nAftPos = line.indexOf( aAftToken, nPos ); + if( nAftPos != -1 ) + { + // get the queue name between fore and aft tokens + OUString aSysQueue( OStringToOUString( line.subView( nPos, nAftPos - nPos ), aEncoding ) ); + // do not insert duplicates (e.g. lpstat tends to produce such lines) + if( aUniqueSet.insert( aSysQueue ).second ) + { + o_rQueues.emplace_back( ); + o_rQueues.back().m_aQueue = aSysQueue; + o_rQueues.back().m_aLocation = aSysQueue; + } + } + } + } +} + +const struct SystemCommandParameters aParms[] = +{ + #if defined(LINUX) || defined(NETBSD) || defined(FREEBSD) || defined(OPENBSD) + { "/usr/sbin/lpc status", "lpr -P \"(PRINTER)\"", "", ":", 0, standardSysQueueTokenHandler }, + { "lpc status", "lpr -P \"(PRINTER)\"", "", ":", 0, standardSysQueueTokenHandler }, + { "LANG=C;LC_ALL=C;export LANG LC_ALL;lpstat -s", "lp -d \"(PRINTER)\"", "system for ", ": ", 1, standardSysQueueTokenHandler } + #else + { "LANG=C;LC_ALL=C;export LANG LC_ALL;lpget list", "lp -d \"(PRINTER)\"", "", ":", 0, lpgetSysQueueTokenHandler }, + { "LANG=C;LC_ALL=C;export LANG LC_ALL;lpstat -s", "lp -d \"(PRINTER)\"", "system for ", ": ", 1, standardSysQueueTokenHandler }, + { "/usr/sbin/lpc status", "lpr -P \"(PRINTER)\"", "", ":", 0, standardSysQueueTokenHandler }, + { "lpc status", "lpr -P \"(PRINTER)\"", "", ":", 0, standardSysQueueTokenHandler } + #endif +}; + +void SystemQueueInfo::run() +{ + osl_setThreadName("LPR psp::SystemQueueInfo"); + + char pBuffer[1024]; + std::vector< OString > aLines; + + /* Discover which command we can use to get a list of all printer queues */ + for(const auto & rParm : aParms) + { + aLines.clear(); +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.print", "trying print queue command \"" + << rParm.pQueueCommand + << "\" ..."); +#endif + OString aCmdLine = rParm.pQueueCommand + OString::Concat(" 2>/dev/null"); + FILE *pPipe; + if( (pPipe = popen( aCmdLine.getStr(), "r" )) ) + { + while( fgets( pBuffer, 1024, pPipe ) ) + aLines.emplace_back( pBuffer ); + if( ! pclose( pPipe ) ) + { + std::vector< PrinterInfoManager::SystemPrintQueue > aSysPrintQueues; + rParm.pHandler( aLines, aSysPrintQueues, &rParm ); + std::unique_lock aGuard( m_aMutex ); + m_bChanged = true; + m_aQueues = aSysPrintQueues; + m_aCommand = OUString::createFromAscii( rParm.pPrintCommand ); +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.print", "printing queue command: success."); +#endif + break; + } + } +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.unx.print", "printing queue command: failed."); +#endif + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/window/salframe.cxx b/vcl/unx/generic/window/salframe.cxx new file mode 100644 index 0000000000..e3300de343 --- /dev/null +++ b/vcl/unx/generic/window/salframe.cxx @@ -0,0 +1,3871 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +#include + +#ifndef Button6 +# define Button6 6 +#endif +#ifndef Button7 +# define Button7 7 +#endif + +using namespace vcl_sal; + +constexpr auto CLIENT_EVENTS = StructureNotifyMask + | SubstructureNotifyMask + | KeyPressMask + | KeyReleaseMask + | ButtonPressMask + | ButtonReleaseMask + | PointerMotionMask + | EnterWindowMask + | LeaveWindowMask + | FocusChangeMask + | ExposureMask + | VisibilityChangeMask + | PropertyChangeMask + | ColormapChangeMask; + +static ::Window hPresentationWindow = None, hPresFocusWindow = None; +static ::std::list< ::Window > aPresentationReparentList; +static int nVisibleFloats = 0; + +static void doReparentPresentationDialogues( SalDisplay const * pDisplay ) +{ + GetGenericUnixSalData()->ErrorTrapPush(); + for (auto const& elem : aPresentationReparentList) + { + int x, y; + ::Window aRoot, aChild; + unsigned int w, h, bw, d; + XGetGeometry( pDisplay->GetDisplay(), + elem, + &aRoot, + &x, &y, &w, &h, &bw, &d ); + XTranslateCoordinates( pDisplay->GetDisplay(), + hPresentationWindow, + aRoot, + x, y, + &x, &y, + &aChild ); + XReparentWindow( pDisplay->GetDisplay(), + elem, + aRoot, + x, y ); + } + aPresentationReparentList.clear(); + if( hPresFocusWindow ) + XSetInputFocus( pDisplay->GetDisplay(), hPresFocusWindow, PointerRoot, CurrentTime ); + XSync( pDisplay->GetDisplay(), False ); + GetGenericUnixSalData()->ErrorTrapPop(); +} + +bool X11SalFrame::IsOverrideRedirect() const +{ + return + ((nStyle_ & SalFrameStyleFlags::INTRO) && !pDisplay_->getWMAdaptor()->supportsSplash()) + || + (!( nStyle_ & ~SalFrameStyleFlags::DEFAULT ) && !pDisplay_->getWMAdaptor()->supportsFullScreen()) + ; +} + +bool X11SalFrame::IsFloatGrabWindow() const +{ + static const char* pDisableGrab = getenv( "SAL_DISABLE_FLOATGRAB" ); + + return + ( ( !pDisableGrab || !*pDisableGrab ) && + ( + (nStyle_ & SalFrameStyleFlags::FLOAT) && + ! (nStyle_ & SalFrameStyleFlags::TOOLTIP) && + ! (nStyle_ & SalFrameStyleFlags::OWNERDRAWDECORATION) + ) + ); +} + +void X11SalFrame::setXEmbedInfo() +{ + if( !m_bXEmbed ) + return; + + tools::Long aInfo[2]; + aInfo[0] = 1; // XEMBED protocol version + aInfo[1] = (bMapped_ ? 1 : 0); // XEMBED_MAPPED + XChangeProperty( pDisplay_->GetDisplay(), + mhWindow, + pDisplay_->getWMAdaptor()->getAtom( WMAdaptor::XEMBED_INFO ), + pDisplay_->getWMAdaptor()->getAtom( WMAdaptor::XEMBED_INFO ), + 32, + PropModeReplace, + reinterpret_cast(aInfo), + SAL_N_ELEMENTS(aInfo) ); +} + +void X11SalFrame::askForXEmbedFocus( sal_Int32 i_nTimeCode ) +{ + XEvent aEvent; + + memset( &aEvent, 0, sizeof(aEvent) ); + aEvent.xclient.window = mhForeignParent; + aEvent.xclient.type = ClientMessage; + aEvent.xclient.message_type = pDisplay_->getWMAdaptor()->getAtom( WMAdaptor::XEMBED ); + aEvent.xclient.format = 32; + aEvent.xclient.data.l[0] = i_nTimeCode ? i_nTimeCode : CurrentTime; + aEvent.xclient.data.l[1] = 3; // XEMBED_REQUEST_FOCUS + aEvent.xclient.data.l[2] = 0; + aEvent.xclient.data.l[3] = 0; + aEvent.xclient.data.l[4] = 0; + + GetGenericUnixSalData()->ErrorTrapPush(); + XSendEvent( pDisplay_->GetDisplay(), + mhForeignParent, + False, NoEventMask, &aEvent ); + XSync( pDisplay_->GetDisplay(), False ); + GetGenericUnixSalData()->ErrorTrapPop(); +} + +typedef std::vector< unsigned long > NetWmIconData; + +namespace +{ + constexpr OUString SV_ICON_SIZE48[] = + { + MAINAPP_48_8, + MAINAPP_48_8, + ODT_48_8, + OTT_48_8, + ODS_48_8, + OTS_48_8, + ODG_48_8, + MAINAPP_48_8, + ODP_48_8, + MAINAPP_48_8, + ODM_48_8, + MAINAPP_48_8, + ODB_48_8, + ODF_48_8 + }; + + constexpr OUString SV_ICON_SIZE32[] = + { + MAINAPP_32_8, + MAINAPP_32_8, + ODT_32_8, + OTT_32_8, + ODS_32_8, + OTS_32_8, + ODG_32_8, + MAINAPP_32_8, + ODP_32_8, + MAINAPP_32_8, + ODM_32_8, + MAINAPP_32_8, + ODB_32_8, + ODF_32_8 + }; + + constexpr OUString SV_ICON_SIZE16[] = + { + MAINAPP_16_8, + MAINAPP_16_8, + ODT_16_8, + OTT_16_8, + ODS_16_8, + OTS_16_8, + ODG_16_8, + MAINAPP_16_8, + ODP_16_8, + MAINAPP_16_8, + ODM_16_8, + MAINAPP_16_8, + ODB_16_8, + ODF_16_8 + }; +} + +static void CreateNetWmAppIcon( sal_uInt16 nIcon, NetWmIconData& netwm_icon ) +{ + const int sizes[ 3 ] = { 48, 32, 16 }; + netwm_icon.resize( 48 * 48 + 32 * 32 + 16 * 16 + 3 * 2 ); + int pos = 0; + for(int size : sizes) + { + OUString sIcon; + if( size >= 48 ) + sIcon = SV_ICON_SIZE48[nIcon]; + else if( size >= 32 ) + sIcon = SV_ICON_SIZE32[nIcon]; + else + sIcon = SV_ICON_SIZE16[nIcon]; + + BitmapEx aIcon = vcl::bitmap::loadFromName(sIcon, ImageLoadFlags::IgnoreScalingFactor); + + if( aIcon.IsEmpty()) + continue; + vcl::bitmap::convertBitmap32To24Plus8(aIcon, aIcon); + Bitmap icon = aIcon.GetBitmap(); + AlphaMask mask = aIcon.GetAlphaMask(); + BitmapScopedReadAccess iconData(icon); + BitmapScopedReadAccess maskData(mask); + netwm_icon[ pos++ ] = size; // width + netwm_icon[ pos++ ] = size; // height + for( int y = 0; y < size; ++y ) + for( int x = 0; x < size; ++x ) + { + BitmapColor col = iconData->GetColor( y, x ); + BitmapColor alpha = maskData->GetColor( y, x ); + netwm_icon[ pos++ ] = (((( 255 - alpha.GetBlue()) * 256U ) + col.GetRed()) * 256 + col.GetGreen()) * 256 + col.GetBlue(); + } + } + netwm_icon.resize( pos ); +} + +void X11SalFrame::Init( SalFrameStyleFlags nSalFrameStyle, SalX11Screen nXScreen, SystemParentData const * pParentData, bool bUseGeometry ) +{ + if( nXScreen.getXScreen() >= GetDisplay()->GetXScreenCount() ) + nXScreen = GetDisplay()->GetDefaultXScreen(); + if( mpParent ) + nXScreen = mpParent->m_nXScreen; + + m_nXScreen = nXScreen; + nStyle_ = nSalFrameStyle; + XWMHints Hints; + Hints.flags = InputHint; + Hints.input = (nSalFrameStyle & SalFrameStyleFlags::OWNERDRAWDECORATION) ? False : True; + NetWmIconData netwm_icon; + + int x = 0, y = 0; + unsigned int w = 500, h = 500; + XSetWindowAttributes Attributes; + + int nAttrMask = CWBorderPixel + | CWBackPixmap + | CWColormap + | CWOverrideRedirect + | CWEventMask + ; + Attributes.border_pixel = 0; + Attributes.background_pixmap = None; + Attributes.colormap = GetDisplay()->GetColormap( m_nXScreen ).GetXColormap(); + Attributes.override_redirect = False; + Attributes.event_mask = CLIENT_EVENTS; + + const SalVisual& rVis = GetDisplay()->GetVisual( m_nXScreen ); + ::Window aFrameParent = pParentData ? pParentData->aWindow : GetDisplay()->GetRootWindow( m_nXScreen ); + ::Window aClientLeader = None; + + if( bUseGeometry ) + { + x = maGeometry.x(); + y = maGeometry.y(); + w = maGeometry.width(); + h = maGeometry.height(); + } + + if( (nSalFrameStyle & SalFrameStyleFlags::FLOAT) && + ! (nSalFrameStyle & SalFrameStyleFlags::OWNERDRAWDECORATION) + ) + { + if( nShowState_ == X11ShowState::Unknown ) + { + w = 10; + h = 10; + } + Attributes.override_redirect = True; + } + else if( nSalFrameStyle & SalFrameStyleFlags::SYSTEMCHILD ) + { + SAL_WARN_IF( !mpParent, "vcl", "SalFrameStyleFlags::SYSTEMCHILD window without parent" ); + if( mpParent ) + { + aFrameParent = mpParent->mhWindow; + // FIXME: since with SalFrameStyleFlags::SYSTEMCHILD + // multiple X11SalFrame objects can have the same shell window + // dispatching events in saldisp.cxx is unclear (the first frame) + // wins. HTH this correctly is unclear yet + // for the time being, treat set the shell window to own window + // like for a normal frame + // mhShellWindow = mpParent->GetShellWindow(); + } + } + else if( pParentData ) + { + // plugin parent may be killed unexpectedly by plugging + // process; start permanently ignoring X errors... + GetGenericUnixSalData()->ErrorTrapPush(); + + nStyle_ |= SalFrameStyleFlags::PLUG; + Attributes.override_redirect = True; + if( pParentData->nSize >= sizeof(SystemParentData) ) + m_bXEmbed = pParentData->bXEmbedSupport; + + int x_ret, y_ret; + unsigned int bw, d; + ::Window aRoot, aParent; + + XGetGeometry( GetXDisplay(), pParentData->aWindow, + &aRoot, &x_ret, &y_ret, &w, &h, &bw, &d ); + mhForeignParent = pParentData->aWindow; + + mhShellWindow = aParent = mhForeignParent; + ::Window* pChildren; + unsigned int nChildren; + bool bBreak = false; + do + { + XQueryTree( GetDisplay()->GetDisplay(), mhShellWindow, + &aRoot, &aParent, &pChildren, &nChildren ); + XFree( pChildren ); + if( aParent != aRoot ) + mhShellWindow = aParent; + int nCount = 0; + Atom* pProps = XListProperties( GetDisplay()->GetDisplay(), + mhShellWindow, + &nCount ); + for( int i = 0; i < nCount && ! bBreak; ++i ) + bBreak = (pProps[i] == XA_WM_HINTS); + if( pProps ) + XFree( pProps ); + } while( aParent != aRoot && ! bBreak ); + + // check if this is really one of our own frames + // do not change the input mask in that case + bool bIsReallyOurFrame = false; + for (auto pSalFrame : GetDisplay()->getFrames() ) + if ( static_cast( pSalFrame )->GetWindow() == mhForeignParent ) + { + bIsReallyOurFrame = true; + break; + } + if (!bIsReallyOurFrame) + { + XSelectInput( GetDisplay()->GetDisplay(), mhForeignParent, StructureNotifyMask | FocusChangeMask ); + XSelectInput( GetDisplay()->GetDisplay(), mhShellWindow, StructureNotifyMask | FocusChangeMask ); + } + } + else + { + if( ! bUseGeometry ) + { + Size aScreenSize( GetDisplay()->getDataForScreen( m_nXScreen ).m_aSize ); + w = aScreenSize.Width(); + h = aScreenSize.Height(); + if( nSalFrameStyle & SalFrameStyleFlags::SIZEABLE && + nSalFrameStyle & SalFrameStyleFlags::MOVEABLE ) + { + Size aBestFitSize(bestmaxFrameSizeForScreenSize(aScreenSize)); + w = aBestFitSize.Width(); + h = aBestFitSize.Height(); + } + if( ! mpParent ) + { + // find the last document window (if any) + const X11SalFrame* pFrame = nullptr; + bool bIsDocumentWindow = false; + for (auto pSalFrame : GetDisplay()->getFrames() ) + { + pFrame = static_cast< const X11SalFrame* >( pSalFrame ); + if( !pFrame->mpParent + && !pFrame->mbFullScreen + && ( pFrame->nStyle_ & SalFrameStyleFlags::SIZEABLE ) + && pFrame->GetUnmirroredGeometry().width() + && pFrame->GetUnmirroredGeometry().height() ) + { + bIsDocumentWindow = true; + break; + } + } + + if( bIsDocumentWindow ) + { + // set a document position and size + // the first frame gets positioned by the window manager + const SalFrameGeometry& rGeom( pFrame->GetUnmirroredGeometry() ); + x = rGeom.x(); + y = rGeom.y(); + if( x+static_cast(w)+40 <= static_cast(aScreenSize.Width()) && + y+static_cast(h)+40 <= static_cast(aScreenSize.Height()) + ) + { + y += 40; + x += 40; + } + else + { + x = 10; // leave some space for decoration + y = 20; + } + } + else if( GetDisplay()->IsXinerama() ) + { + // place frame on same screen as mouse pointer + ::Window aRoot, aChild; + int root_x = 0, root_y = 0, lx, ly; + unsigned int mask; + XQueryPointer( GetXDisplay(), + GetDisplay()->GetRootWindow( m_nXScreen ), + &aRoot, &aChild, + &root_x, &root_y, &lx, &ly, &mask ); + const std::vector< AbsoluteScreenPixelRectangle >& rScreens = GetDisplay()->GetXineramaScreens(); + for(const auto & rScreen : rScreens) + if( rScreen.Contains( AbsoluteScreenPixelPoint( root_x, root_y ) ) ) + { + x = rScreen.Left(); + y = rScreen.Top(); + break; + } + } + } + } + Attributes.win_gravity = pDisplay_->getWMAdaptor()->getInitWinGravity(); + nAttrMask |= CWWinGravity; + if( mpParent ) + { + Attributes.save_under = True; + nAttrMask |= CWSaveUnder; + } + if( IsOverrideRedirect() ) + Attributes.override_redirect = True; + // default icon + if( !(nStyle_ & SalFrameStyleFlags::INTRO) && !(nStyle_ & SalFrameStyleFlags::NOICON)) + { + try + { + CreateNetWmAppIcon( mnIconID != SV_ICON_ID_OFFICE ? mnIconID : + (mpParent ? mpParent->mnIconID : SV_ICON_ID_OFFICE), netwm_icon ); + } + catch( css::uno::Exception& ) + { + // can happen - no ucb during early startup + } + } + + // find the top level frame of the transience hierarchy + X11SalFrame* pFrame = this; + while( pFrame->mpParent ) + pFrame = pFrame->mpParent; + if( pFrame->nStyle_ & SalFrameStyleFlags::PLUG ) + { + // if the top level window is a plugin window, + // then we should place us in the same window group as + // the parent application (or none if there is no window group + // hint in the parent). + if( pFrame->GetShellWindow() ) + { + XWMHints* pWMHints = XGetWMHints( pDisplay_->GetDisplay(), + pFrame->GetShellWindow() ); + if( pWMHints ) + { + if( pWMHints->flags & WindowGroupHint ) + { + Hints.flags |= WindowGroupHint; + Hints.window_group = pWMHints->window_group; + } + XFree( pWMHints ); + } + } + } + else + { + Hints.flags |= WindowGroupHint; + Hints.window_group = pFrame->GetShellWindow(); + // note: for a normal document window this will produce None + // as the window is not yet created and the shell window is + // initialized to None. This must be corrected after window creation. + aClientLeader = GetDisplay()->GetDrawable( m_nXScreen ); + } + } + + nShowState_ = X11ShowState::Unknown; + bViewable_ = true; + bMapped_ = false; + nVisibility_ = VisibilityFullyObscured; + mhWindow = XCreateWindow( GetXDisplay(), + aFrameParent, + x, y, + w, h, + 0, + rVis.GetDepth(), + InputOutput, + rVis.GetVisual(), + nAttrMask, + &Attributes ); + mpSurface = cairo_xlib_surface_create(GetXDisplay(), mhWindow, + rVis.GetVisual(), + w, h); + + // FIXME: see above: fake shell window for now to own window + if( pParentData == nullptr ) + { + mhShellWindow = mhWindow; + } + + // correct window group if necessary + if( (Hints.flags & WindowGroupHint) == WindowGroupHint ) + { + if( Hints.window_group == None ) + Hints.window_group = GetShellWindow(); + } + + maGeometry.setPosSize({ x, y }, { w, h }); + updateScreenNumber(); + + XSync( GetXDisplay(), False ); + setXEmbedInfo(); + + Time nUserTime = (nStyle_ & (SalFrameStyleFlags::OWNERDRAWDECORATION | SalFrameStyleFlags::TOOLWINDOW) ) == SalFrameStyleFlags::NONE ? + pDisplay_->GetLastUserEventTime() : 0; + pDisplay_->getWMAdaptor()->setUserTime( this, nUserTime ); + + if( ! pParentData && ! IsChildWindow() && ! Attributes.override_redirect ) + { + XSetWMHints( GetXDisplay(), mhWindow, &Hints ); + // WM Protocols && internals + Atom a[3]; + int n = 0; + a[n++] = pDisplay_->getWMAdaptor()->getAtom( WMAdaptor::WM_DELETE_WINDOW ); + +// LibreOffice advertises NET_WM_PING atom, so mutter rightfully warns of an unresponsive application during debugging. +// Hack that out unconditionally for debug builds, as per https://bugzilla.redhat.com/show_bug.cgi?id=981149 +// upstream refuses to make this configurable in any way. +// NOTE: You need to use the 'gen' backend for this to work (SAL_USE_VCLPLUGIN=gen) +#if OSL_DEBUG_LEVEL < 1 + if( pDisplay_->getWMAdaptor()->getAtom( WMAdaptor::NET_WM_PING ) ) + a[n++] = pDisplay_->getWMAdaptor()->getAtom( WMAdaptor::NET_WM_PING ); +#endif + + if( nSalFrameStyle & SalFrameStyleFlags::OWNERDRAWDECORATION ) + a[n++] = pDisplay_->getWMAdaptor()->getAtom( WMAdaptor::WM_TAKE_FOCUS ); + XSetWMProtocols( GetXDisplay(), GetShellWindow(), a, n ); + + // force wm class hint + mnExtStyle = ~0; + if (mpParent) + m_sWMClass = mpParent->m_sWMClass; + SetExtendedFrameStyle( 0 ); + + XSizeHints* pHints = XAllocSizeHints(); + pHints->flags = PWinGravity | PPosition; + pHints->win_gravity = GetDisplay()->getWMAdaptor()->getPositionWinGravity(); + pHints->x = 0; + pHints->y = 0; + if( mbFullScreen ) + { + pHints->flags |= PMaxSize | PMinSize; + pHints->max_width = w+100; + pHints->max_height = h+100; + pHints->min_width = w; + pHints->min_height = h; + } + XSetWMNormalHints( GetXDisplay(), + GetShellWindow(), + pHints ); + XFree (pHints); + + // set PID and WM_CLIENT_MACHINE + pDisplay_->getWMAdaptor()->setClientMachine( this ); + pDisplay_->getWMAdaptor()->setPID( this ); + + // set client leader + if( aClientLeader ) + { + XChangeProperty( GetXDisplay(), + mhWindow, + pDisplay_->getWMAdaptor()->getAtom( WMAdaptor::WM_CLIENT_LEADER), + XA_WINDOW, + 32, + PropModeReplace, + reinterpret_cast(&aClientLeader), + 1 + ); + } +#define DECOFLAGS (SalFrameStyleFlags::MOVEABLE | SalFrameStyleFlags::SIZEABLE | SalFrameStyleFlags::CLOSEABLE) + int nDecoFlags = WMAdaptor::decoration_All; + if (m_bIsPartialFullScreen || (nStyle_ & SalFrameStyleFlags::OWNERDRAWDECORATION)) + nDecoFlags = 0; + else if( (nStyle_ & DECOFLAGS ) != DECOFLAGS || (nStyle_ & SalFrameStyleFlags::TOOLWINDOW) ) + { + if( nStyle_ & DECOFLAGS ) + // if any decoration, then show a border + nDecoFlags = WMAdaptor::decoration_Border; + else + nDecoFlags = 0; + + if( ! mpParent && (nStyle_ & DECOFLAGS) ) + // don't add a min button if window should be decorationless + nDecoFlags |= WMAdaptor::decoration_MinimizeBtn; + if( nStyle_ & SalFrameStyleFlags::CLOSEABLE ) + nDecoFlags |= WMAdaptor::decoration_CloseBtn; + if( nStyle_ & SalFrameStyleFlags::SIZEABLE ) + { + nDecoFlags |= WMAdaptor::decoration_Resize; + if( ! (nStyle_ & SalFrameStyleFlags::TOOLWINDOW) ) + nDecoFlags |= WMAdaptor::decoration_MaximizeBtn; + } + if( nStyle_ & SalFrameStyleFlags::MOVEABLE ) + nDecoFlags |= WMAdaptor::decoration_Title; + } + + WMWindowType eType = WMWindowType::Normal; + if( nStyle_ & SalFrameStyleFlags::INTRO ) + eType = WMWindowType::Splash; + if( (nStyle_ & SalFrameStyleFlags::DIALOG) && hPresentationWindow == None ) + eType = WMWindowType::ModelessDialogue; + if( nStyle_ & SalFrameStyleFlags::TOOLWINDOW ) + eType = WMWindowType::Utility; + if( nStyle_ & SalFrameStyleFlags::OWNERDRAWDECORATION ) + eType = WMWindowType::Toolbar; + if (m_bIsPartialFullScreen && GetDisplay()->getWMAdaptor()->isLegacyPartialFullscreen()) + eType = WMWindowType::Dock; + + GetDisplay()->getWMAdaptor()-> + setFrameTypeAndDecoration( this, + eType, + nDecoFlags, + hPresentationWindow ? nullptr : mpParent ); + + if (!m_bIsPartialFullScreen && (nStyle_ & (SalFrameStyleFlags::DEFAULT | + SalFrameStyleFlags::OWNERDRAWDECORATION| + SalFrameStyleFlags::FLOAT | + SalFrameStyleFlags::INTRO)) + == SalFrameStyleFlags::DEFAULT ) + pDisplay_->getWMAdaptor()->maximizeFrame( this ); + + if( !netwm_icon.empty() && GetDisplay()->getWMAdaptor()->getAtom( WMAdaptor::NET_WM_ICON )) + XChangeProperty( GetXDisplay(), mhWindow, + GetDisplay()->getWMAdaptor()->getAtom( WMAdaptor::NET_WM_ICON ), + XA_CARDINAL, 32, PropModeReplace, reinterpret_cast(netwm_icon.data()), netwm_icon.size()); + } + + m_nWorkArea = GetDisplay()->getWMAdaptor()->getCurrentWorkArea(); + + // Pointer + SetPointer( PointerStyle::Arrow ); +} + +X11SalFrame::X11SalFrame( SalFrame *pParent, SalFrameStyleFlags nSalFrameStyle, + SystemParentData const * pSystemParent ) : + m_nXScreen( 0 ), + maAlwaysOnTopRaiseTimer( "vcl::X11SalFrame maAlwaysOnTopRaiseTimer" ) +{ + GenericUnixSalData *pData = GetGenericUnixSalData(); + + mpParent = static_cast< X11SalFrame* >( pParent ); + + mbTransientForRoot = false; + + pDisplay_ = vcl_sal::getSalDisplay(pData); + // insert frame in framelist + pDisplay_->registerFrame( this ); + + mhWindow = None; + mpSurface = nullptr; + mhShellWindow = None; + mhStackingWindow = None; + mhForeignParent = None; + m_bSetFocusOnMap = false; + + pGraphics_ = nullptr; + pFreeGraphics_ = nullptr; + + hCursor_ = None; + nCaptured_ = 0; + + mbSendExtKeyModChange = false; + mnExtKeyMod = ModKeyFlags::NONE; + + nShowState_ = X11ShowState::Unknown; + nWidth_ = 0; + nHeight_ = 0; + nStyle_ = SalFrameStyleFlags::NONE; + mnExtStyle = 0; + bAlwaysOnTop_ = false; + + // set bViewable_ to true: hack GetClientSize to report something + // different to 0/0 before first map + bViewable_ = true; + bMapped_ = false; + bDefaultPosition_ = true; + nVisibility_ = VisibilityFullyObscured; + m_nWorkArea = 0; + m_bXEmbed = false; + + + mpInputContext = nullptr; + mbInputFocus = False; + + maAlwaysOnTopRaiseTimer.SetInvokeHandler( LINK( this, X11SalFrame, HandleAlwaysOnTopRaise ) ); + maAlwaysOnTopRaiseTimer.SetTimeout( 100 ); + + meWindowType = WMWindowType::Normal; + mbMaximizedVert = false; + mbMaximizedHorz = false; + mbFullScreen = false; + m_bIsPartialFullScreen = false; + + mnIconID = SV_ICON_ID_OFFICE; + + if( mpParent ) + mpParent->maChildren.push_back( this ); + + Init( nSalFrameStyle, GetDisplay()->GetDefaultXScreen(), pSystemParent ); +} + +X11SalFrame::~X11SalFrame() +{ + notifyDelete(); + + m_vClipRectangles.clear(); + + if( mhStackingWindow ) + aPresentationReparentList.remove( mhStackingWindow ); + + // remove from parent's list + if( mpParent ) + mpParent->maChildren.remove( this ); + + // deregister on SalDisplay + pDisplay_->deregisterFrame( this ); + + // unselect all events, some may be still in the queue anyway + if( ! IsSysChildWindow() ) + XSelectInput( GetXDisplay(), GetShellWindow(), 0 ); + XSelectInput( GetXDisplay(), GetWindow(), 0 ); + + ShowFullScreen( false, 0 ); + + if( bMapped_ ) + Show( false ); + + if( mpInputContext ) + { + mpInputContext->UnsetICFocus(); + mpInputContext->Unmap(); + mpInputContext.reset(); + } + + if( GetWindow() == hPresentationWindow ) + { + hPresentationWindow = None; + doReparentPresentationDialogues( GetDisplay() ); + } + + if( pGraphics_ ) + { + pGraphics_->DeInit(); + pGraphics_.reset(); + } + + if( pFreeGraphics_ ) + { + pFreeGraphics_->DeInit(); + pFreeGraphics_.reset(); + } + + // reset all OpenGL contexts using this window + rtl::Reference pContext = ImplGetSVData()->maGDIData.mpLastContext; + while( pContext.is() ) + { + if (static_cast(pContext->getOpenGLWindow()).win == mhWindow) + pContext->reset(); + pContext = pContext->mpPrevContext; + } + + if (mpSurface) + cairo_surface_destroy(mpSurface); + + XDestroyWindow( GetXDisplay(), mhWindow ); +} + +void X11SalFrame::SetExtendedFrameStyle( SalExtStyle nStyle ) +{ + if( nStyle != mnExtStyle && ! IsChildWindow() ) + { + mnExtStyle = nStyle; + updateWMClass(); + } +} + +const SystemEnvData* X11SalFrame::GetSystemData() const +{ + X11SalFrame *pFrame = const_cast(this); + pFrame->maSystemChildData.pDisplay = GetXDisplay(); + pFrame->maSystemChildData.SetWindowHandle(pFrame->GetWindow()); + pFrame->maSystemChildData.pSalFrame = pFrame; + pFrame->maSystemChildData.pWidget = nullptr; + pFrame->maSystemChildData.pVisual = GetDisplay()->GetVisual( m_nXScreen ).GetVisual(); + pFrame->maSystemChildData.nScreen = m_nXScreen.getXScreen(); + pFrame->maSystemChildData.aShellWindow = pFrame->GetShellWindow(); + pFrame->maSystemChildData.toolkit = SystemEnvData::Toolkit::Gen; + pFrame->maSystemChildData.platform = SystemEnvData::Platform::Xcb; + return &maSystemChildData; +} + +SalGraphics *X11SalFrame::AcquireGraphics() +{ + if( pGraphics_ ) + return nullptr; + + if( pFreeGraphics_ ) + { + pGraphics_ = std::move(pFreeGraphics_); + } + else + { + pGraphics_.reset(new X11SalGraphics()); + pGraphics_->Init(*this, GetWindow(), m_nXScreen); + } + + return pGraphics_.get(); +} + +void X11SalFrame::ReleaseGraphics( SalGraphics *pGraphics ) +{ + SAL_WARN_IF( pGraphics != pGraphics_.get(), "vcl", "SalFrame::ReleaseGraphics pGraphics!=pGraphics_" ); + + if( pGraphics != pGraphics_.get() ) + return; + + pFreeGraphics_ = std::move(pGraphics_); +} + +void X11SalFrame::updateGraphics( bool bClear ) +{ + Drawable aDrawable = bClear ? None : GetWindow(); + if( pGraphics_ ) + pGraphics_->SetDrawable( aDrawable, mpSurface, m_nXScreen ); + if( pFreeGraphics_ ) + pFreeGraphics_->SetDrawable( aDrawable, mpSurface, m_nXScreen ); +} + +void X11SalFrame::SetIcon( sal_uInt16 nIcon ) +{ + if ( IsChildWindow() ) + return; + + // 0 == default icon -> #1 + if ( nIcon == 0 ) + nIcon = 1; + + mnIconID = nIcon; + + NetWmIconData netwm_icon; + CreateNetWmAppIcon( nIcon, netwm_icon ); + + if( !netwm_icon.empty() && GetDisplay()->getWMAdaptor()->getAtom( WMAdaptor::NET_WM_ICON )) + XChangeProperty( GetXDisplay(), mhWindow, + GetDisplay()->getWMAdaptor()->getAtom( WMAdaptor::NET_WM_ICON ), + XA_CARDINAL, 32, PropModeReplace, reinterpret_cast(netwm_icon.data()), netwm_icon.size()); +} + +void X11SalFrame::SetMaxClientSize( tools::Long nWidth, tools::Long nHeight ) +{ + if( IsChildWindow() ) + return; + + if( !GetShellWindow() || + (nStyle_ & (SalFrameStyleFlags::FLOAT|SalFrameStyleFlags::OWNERDRAWDECORATION) ) == SalFrameStyleFlags::FLOAT ) + return; + + XSizeHints* pHints = XAllocSizeHints(); + tools::Long nSupplied = 0; + XGetWMNormalHints( GetXDisplay(), + GetShellWindow(), + pHints, + &nSupplied + ); + pHints->max_width = nWidth; + pHints->max_height = nHeight; + pHints->flags |= PMaxSize; + XSetWMNormalHints( GetXDisplay(), + GetShellWindow(), + pHints ); + XFree( pHints ); +} + +void X11SalFrame::SetMinClientSize( tools::Long nWidth, tools::Long nHeight ) +{ + if( IsChildWindow() ) + return; + + if( !GetShellWindow() || + (nStyle_ & (SalFrameStyleFlags::FLOAT|SalFrameStyleFlags::OWNERDRAWDECORATION) ) == SalFrameStyleFlags::FLOAT ) + return; + + XSizeHints* pHints = XAllocSizeHints(); + tools::Long nSupplied = 0; + XGetWMNormalHints( GetXDisplay(), + GetShellWindow(), + pHints, + &nSupplied + ); + pHints->min_width = nWidth; + pHints->min_height = nHeight; + pHints->flags |= PMinSize; + XSetWMNormalHints( GetXDisplay(), + GetShellWindow(), + pHints ); + XFree( pHints ); +} + +// Show + Pos (x,y,z) + Size (width,height) + +void X11SalFrame::Show( bool bVisible, bool bNoActivate ) +{ + if( ( bVisible && bMapped_ ) + || ( !bVisible && !bMapped_ ) ) + return; + + // HACK: this is a workaround for (at least) kwin + // even though transient frames should be kept above their parent + // this does not necessarily hold true for DOCK type windows + // so artificially set ABOVE and remove it again on hide + if( mpParent && mpParent->m_bIsPartialFullScreen && pDisplay_->getWMAdaptor()->isLegacyPartialFullscreen()) + pDisplay_->getWMAdaptor()->enableAlwaysOnTop( this, bVisible ); + + bMapped_ = bVisible; + bViewable_ = bVisible; + setXEmbedInfo(); + if( bVisible ) + { + if( ! (nStyle_ & SalFrameStyleFlags::INTRO) ) + { + // hide all INTRO frames + for (auto pSalFrame : GetDisplay()->getFrames() ) + { + const X11SalFrame* pFrame = static_cast< const X11SalFrame* >( pSalFrame ); + // look for intro bit map; if present, hide it + if( pFrame->nStyle_ & SalFrameStyleFlags::INTRO ) + { + if( pFrame->bMapped_ ) + const_cast(pFrame)->Show( false ); + } + } + } + + // update NET_WM_STATE which may have been deleted due to earlier Show(false) + if( nShowState_ == X11ShowState::Hidden ) + GetDisplay()->getWMAdaptor()->frameIsMapping( this ); + + /* + * Actually this is rather exotic and currently happens only in conjunction + * with the basic dialogue editor, + * which shows a frame and instantly hides it again. After that the + * editor window is shown and the WM takes this as an opportunity + * to show our hidden transient frame also. So Show( false ) must + * withdraw the frame AND delete the WM_TRANSIENT_FOR property. + * In case the frame is shown again, the transient hint must be restored here. + */ + if( ! IsChildWindow() + && ! IsOverrideRedirect() + && ! IsFloatGrabWindow() + && mpParent + ) + { + GetDisplay()->getWMAdaptor()->changeReferenceFrame( this, mpParent ); + } + + // #i45160# switch to desktop where a dialog with parent will appear + if( mpParent && mpParent->m_nWorkArea != m_nWorkArea ) + GetDisplay()->getWMAdaptor()->switchToWorkArea( mpParent->m_nWorkArea ); + + if( IsFloatGrabWindow() && + mpParent && + nVisibleFloats == 0 && + ! GetDisplay()->GetCaptureFrame() ) + { + /* #i39420# + * outsmart KWin's "focus strictly under mouse" mode + * which insists on taking the focus from the document + * to the new float. Grab focus to parent frame BEFORE + * showing the float (cannot grab it to the float + * before show). + */ + XGrabPointer( GetXDisplay(), + mpParent->GetWindow(), + True, + PointerMotionMask | ButtonPressMask | ButtonReleaseMask, + GrabModeAsync, + GrabModeAsync, + None, + mpParent ? mpParent->GetCursor() : None, + CurrentTime + ); + } + + Time nUserTime = 0; + if( ! bNoActivate && !(nStyle_ & SalFrameStyleFlags::OWNERDRAWDECORATION) ) + nUserTime = pDisplay_->GetX11ServerTime(); + GetDisplay()->getWMAdaptor()->setUserTime( this, nUserTime ); + if( ! bNoActivate && (nStyle_ & SalFrameStyleFlags::TOOLWINDOW) ) + m_bSetFocusOnMap = true; + + // actually map the window + if( m_bXEmbed ) + askForXEmbedFocus( 0 ); + else + { + if( GetWindow() != GetShellWindow() && ! IsSysChildWindow() ) + { + if( IsChildWindow() ) + XMapWindow( GetXDisplay(), GetShellWindow() ); + XSelectInput( GetXDisplay(), GetShellWindow(), CLIENT_EVENTS ); + } + if( nStyle_ & SalFrameStyleFlags::FLOAT ) + XMapRaised( GetXDisplay(), GetWindow() ); + else + XMapWindow( GetXDisplay(), GetWindow() ); + } + XSelectInput( GetXDisplay(), GetWindow(), CLIENT_EVENTS ); + + if( maGeometry.width() > 0 + && maGeometry.height() > 0 + && ( nWidth_ != static_cast(maGeometry.width()) + || nHeight_ != static_cast(maGeometry.height()) ) ) + { + nWidth_ = maGeometry.width(); + nHeight_ = maGeometry.height(); + } + + XSync( GetXDisplay(), False ); + + if( IsFloatGrabWindow() ) + { + /* + * Sawfish and twm can be switched to enter-exit focus behaviour. In this case + * we must grab the pointer else the dumb WM will put the focus to the + * override-redirect float window. The application window will be deactivated + * which causes that the floats are destroyed, so the user can never click on + * a menu because it vanishes as soon as he enters it. + */ + nVisibleFloats++; + if( nVisibleFloats == 1 && ! GetDisplay()->GetCaptureFrame() ) + { + /* #i39420# now move grab to the new float window */ + XGrabPointer( GetXDisplay(), + GetWindow(), + True, + PointerMotionMask | ButtonPressMask | ButtonReleaseMask, + GrabModeAsync, + GrabModeAsync, + None, + mpParent ? mpParent->GetCursor() : None, + CurrentTime + ); + } + } + CallCallback( SalEvent::Resize, nullptr ); + + /* + * sometimes a message box/dialogue is brought up when a frame is not mapped + * the corresponding TRANSIENT_FOR hint is then set to the root window + * so that the dialogue shows in all cases. Correct it here if the + * frame is shown afterwards. + */ + if( ! IsChildWindow() + && ! IsOverrideRedirect() + && ! IsFloatGrabWindow() + ) + { + for (auto const& child : maChildren) + { + if( child->mbTransientForRoot ) + GetDisplay()->getWMAdaptor()->changeReferenceFrame( child, this ); + } + } + /* + * leave X11ShowState::Unknown as this indicates first mapping + * and is only reset int HandleSizeEvent + */ + if( nShowState_ != X11ShowState::Unknown ) + nShowState_ = X11ShowState::Normal; + + /* + * plugged windows don't necessarily get the + * focus on show because the parent may already be mapped + * and have the focus. So try to set the focus + * to the child on Show(true) + */ + if( (nStyle_ & SalFrameStyleFlags::PLUG) && ! m_bXEmbed ) + XSetInputFocus( GetXDisplay(), + GetWindow(), + RevertToParent, + CurrentTime ); + + if( mpParent ) + { + // push this frame so it will be in front of its siblings + // only necessary for insane transient behaviour of Dtwm/olwm + mpParent->maChildren.remove( this ); + mpParent->maChildren.push_front(this); + } + } + else + { + if( getInputContext() ) + getInputContext()->Unmap(); + + if( ! IsChildWindow() ) + { + /* FIXME: Is deleting the property really necessary ? It hurts + * owner drawn windows at least. + */ + if( mpParent && ! (nStyle_ & SalFrameStyleFlags::OWNERDRAWDECORATION) ) + XDeleteProperty( GetXDisplay(), GetShellWindow(), GetDisplay()->getWMAdaptor()->getAtom( WMAdaptor::WM_TRANSIENT_FOR ) ); + XWithdrawWindow( GetXDisplay(), GetShellWindow(), m_nXScreen.getXScreen() ); + } + else if( ! m_bXEmbed ) + XUnmapWindow( GetXDisplay(), GetWindow() ); + + nShowState_ = X11ShowState::Hidden; + if( IsFloatGrabWindow() && nVisibleFloats ) + { + nVisibleFloats--; + if( nVisibleFloats == 0 && ! GetDisplay()->GetCaptureFrame() ) + XUngrabPointer( GetXDisplay(), + CurrentTime ); + } + // flush here; there may be a very seldom race between + // the display connection used for clipboard and our connection + Flush(); + } +} + +void X11SalFrame::ToTop( SalFrameToTop nFlags ) +{ + if( ( nFlags & SalFrameToTop::RestoreWhenMin ) + && ! ( nStyle_ & SalFrameStyleFlags::FLOAT ) + && nShowState_ != X11ShowState::Hidden + && nShowState_ != X11ShowState::Unknown + ) + { + GetDisplay()->getWMAdaptor()->frameIsMapping( this ); + if( GetWindow() != GetShellWindow() && ! IsSysChildWindow() ) + XMapWindow( GetXDisplay(), GetShellWindow() ); + XMapWindow( GetXDisplay(), GetWindow() ); + } + + ::Window aToTopWindow = IsSysChildWindow() ? GetWindow() : GetShellWindow(); + if( ! (nFlags & SalFrameToTop::GrabFocusOnly) ) + { + XRaiseWindow( GetXDisplay(), aToTopWindow ); + } + + if( ( ( nFlags & SalFrameToTop::GrabFocus ) || ( nFlags & SalFrameToTop::GrabFocusOnly ) ) + && bMapped_ ) + { + if( m_bXEmbed ) + askForXEmbedFocus( 0 ); + else + XSetInputFocus( GetXDisplay(), aToTopWindow, RevertToParent, CurrentTime ); + } + else if( ( nFlags & SalFrameToTop::RestoreWhenMin ) || ( nFlags & SalFrameToTop::ForegroundTask ) ) + { + Time nTimestamp = pDisplay_->GetX11ServerTime(); + GetDisplay()->getWMAdaptor()->activateWindow( this, nTimestamp ); + } +} + +void X11SalFrame::GetWorkArea( AbsoluteScreenPixelRectangle& rWorkArea ) +{ + rWorkArea = pDisplay_->getWMAdaptor()->getWorkArea( 0 ); +} + +void X11SalFrame::GetClientSize( tools::Long &rWidth, tools::Long &rHeight ) +{ + if( ! bViewable_ ) + { + rWidth = rHeight = 0; + return; + } + + rWidth = maGeometry.width(); + rHeight = maGeometry.height(); + + if( !rWidth || !rHeight ) + { + XWindowAttributes aAttrib; + + XGetWindowAttributes( GetXDisplay(), GetWindow(), &aAttrib ); + + rWidth = aAttrib.width; + rHeight = aAttrib.height; + maGeometry.setSize({ aAttrib.width, aAttrib.height }); + } +} + +void X11SalFrame::Center( ) +{ + int nX, nY; + AbsoluteScreenPixelSize aRealScreenSize(GetDisplay()->getDataForScreen(m_nXScreen).m_aSize); + AbsoluteScreenPixelRectangle aScreen({ 0, 0 }, aRealScreenSize); + + if( GetDisplay()->IsXinerama() ) + { + // get xinerama screen we are on + // if there is a parent, use its center for screen determination + // else use the pointer + ::Window aRoot, aChild; + int root_x, root_y, x, y; + unsigned int mask; + if( mpParent ) + { + root_x = mpParent->maGeometry.x() + mpParent->maGeometry.width() / 2; + root_y = mpParent->maGeometry.y() + mpParent->maGeometry.height() / 2; + } + else + XQueryPointer( GetXDisplay(), + GetShellWindow(), + &aRoot, &aChild, + &root_x, &root_y, + &x, &y, + &mask ); + const std::vector< AbsoluteScreenPixelRectangle >& rScreens = GetDisplay()->GetXineramaScreens(); + for(const auto & rScreen : rScreens) + if( rScreen.Contains( AbsoluteScreenPixelPoint( root_x, root_y ) ) ) + { + aScreen.SetPos(rScreen.GetPos()); + aRealScreenSize = rScreen.GetSize(); + break; + } + } + + if( mpParent ) + { + X11SalFrame* pFrame = mpParent; + while( pFrame->mpParent ) + pFrame = pFrame->mpParent; + if( pFrame->maGeometry.width() < 1 || pFrame->maGeometry.height() < 1 ) + { + AbsoluteScreenPixelRectangle aRect; + pFrame->GetPosSize( aRect ); + pFrame->maGeometry.setPosSize(tools::Rectangle(aRect)); + } + + if( pFrame->nStyle_ & SalFrameStyleFlags::PLUG ) + { + ::Window aRoot; + unsigned int nScreenWidth, nScreenHeight, bw, depth; + int nScreenX, nScreenY; + XGetGeometry( GetXDisplay(), + pFrame->GetShellWindow(), + &aRoot, + &nScreenX, &nScreenY, + &nScreenWidth, &nScreenHeight, + &bw, &depth ); + aScreen = {{ nScreenX, nScreenY }, Size(nScreenWidth, nScreenHeight)}; + } + else + aScreen = AbsoluteScreenPixelRectangle(pFrame->maGeometry.posSize()); + } + + if( mpParent && mpParent->nShowState_ == X11ShowState::Normal ) + { + if( maGeometry.width() >= mpParent->maGeometry.width() && + maGeometry.height() >= mpParent->maGeometry.height() ) + { + nX = aScreen.getX() + 40; + nY = aScreen.getY() + 40; + } + else + { + // center the window relative to the top level frame + nX = (aScreen.GetWidth() - static_cast(maGeometry.width()) ) / 2 + aScreen.getX(); + nY = (aScreen.GetHeight() - static_cast(maGeometry.height())) / 2 + aScreen.getY(); + } + } + else + { + // center the window relative to screen + nX = (aRealScreenSize.getWidth() - static_cast(maGeometry.width()) ) / 2 + aScreen.getX(); + nY = (aRealScreenSize.getHeight() - static_cast(maGeometry.height())) / 2 + aScreen.getY(); + } + nX = nX < 0 ? 0 : nX; + nY = nY < 0 ? 0 : nY; + + bDefaultPosition_ = False; + if( mpParent ) + { + nX -= mpParent->maGeometry.x(); + nY -= mpParent->maGeometry.y(); + } + + SetPosSize({ { nX, nY }, maGeometry.size() }); +} + +void X11SalFrame::updateScreenNumber() +{ + if( GetDisplay()->IsXinerama() && GetDisplay()->GetXineramaScreens().size() > 1 ) + { + AbsoluteScreenPixelPoint aPoint( maGeometry.x(), maGeometry.y() ); + const std::vector& rScreenRects( GetDisplay()->GetXineramaScreens() ); + size_t nScreens = rScreenRects.size(); + for( size_t i = 0; i < nScreens; i++ ) + { + if( rScreenRects[i].Contains( aPoint ) ) + { + maGeometry.setScreen(static_cast(i)); + break; + } + } + } + else + maGeometry.setScreen(m_nXScreen.getXScreen()); +} + +void X11SalFrame::SetPosSize( tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight, sal_uInt16 nFlags ) +{ + if( nStyle_ & SalFrameStyleFlags::PLUG ) + return; + + // relative positioning in X11SalFrame::SetPosSize + AbsoluteScreenPixelRectangle aPosSize( AbsoluteScreenPixelPoint( maGeometry.x(), maGeometry.y() ), AbsoluteScreenPixelSize( maGeometry.width(), maGeometry.height() ) ); + aPosSize.Normalize(); + + if( ! ( nFlags & SAL_FRAME_POSSIZE_X ) ) + { + nX = aPosSize.Left(); + if( mpParent ) + nX -= mpParent->maGeometry.x(); + } + if( ! ( nFlags & SAL_FRAME_POSSIZE_Y ) ) + { + nY = aPosSize.Top(); + if( mpParent ) + nY -= mpParent->maGeometry.y(); + } + if( ! ( nFlags & SAL_FRAME_POSSIZE_WIDTH ) ) + nWidth = aPosSize.GetWidth(); + if( ! ( nFlags & SAL_FRAME_POSSIZE_HEIGHT ) ) + nHeight = aPosSize.GetHeight(); + + aPosSize = AbsoluteScreenPixelRectangle( AbsoluteScreenPixelPoint( nX, nY ), AbsoluteScreenPixelSize( nWidth, nHeight ) ); + + if( ! ( nFlags & ( SAL_FRAME_POSSIZE_X | SAL_FRAME_POSSIZE_Y ) ) ) + { + if( bDefaultPosition_ ) + { + maGeometry.setSize(Size(aPosSize.GetSize())); + Center(); + } + else + SetSize( Size( nWidth, nHeight ) ); + } + else + SetPosSize( aPosSize ); + + bDefaultPosition_ = False; +} + +void X11SalFrame::SetAlwaysOnTop( bool bOnTop ) +{ + if( ! IsOverrideRedirect() ) + { + bAlwaysOnTop_ = bOnTop; + pDisplay_->getWMAdaptor()->enableAlwaysOnTop( this, bOnTop ); + } +} + +constexpr auto FRAMESTATE_MASK_MAXIMIZED_GEOMETRY = + vcl::WindowDataMask::MaximizedX | vcl::WindowDataMask::MaximizedY | + vcl::WindowDataMask::MaximizedWidth | vcl::WindowDataMask::MaximizedHeight; + +void X11SalFrame::SetWindowState( const vcl::WindowData *pState ) +{ + if (pState == nullptr) + return; + + // Request for position or size change + if (pState->mask() & vcl::WindowDataMask::PosSize) + { + /* #i44325# + * if maximized, set restore size and guess maximized size from last time + * in state change below maximize window + */ + if( ! IsChildWindow() && + (pState->mask() & vcl::WindowDataMask::PosSizeState) == vcl::WindowDataMask::PosSizeState && + (pState->state() & vcl::WindowState::Maximized) && + (pState->mask() & FRAMESTATE_MASK_MAXIMIZED_GEOMETRY) == FRAMESTATE_MASK_MAXIMIZED_GEOMETRY + ) + { + XSizeHints* pHints = XAllocSizeHints(); + tools::Long nSupplied = 0; + XGetWMNormalHints( GetXDisplay(), + GetShellWindow(), + pHints, + &nSupplied ); + pHints->flags |= PPosition | PWinGravity; + pHints->x = pState->x(); + pHints->y = pState->y(); + pHints->win_gravity = pDisplay_->getWMAdaptor()->getPositionWinGravity(); + XSetWMNormalHints(GetXDisplay(), GetShellWindow(), pHints); + XFree( pHints ); + + XMoveResizeWindow(GetXDisplay(), GetShellWindow(), pState->x(), pState->y(), + pState->width(), pState->height()); + // guess maximized geometry from last time + maGeometry.setPos({ pState->GetMaximizedX(), pState->GetMaximizedY() }); + maGeometry.setSize({ pState->GetMaximizedWidth(), pState->GetMaximizedHeight() }); + cairo_xlib_surface_set_size(mpSurface, pState->GetMaximizedWidth(), pState->GetMaximizedHeight()); + updateScreenNumber(); + } + else + { + bool bDoAdjust = false; + AbsoluteScreenPixelRectangle aPosSize; + // initialize with current geometry + if ((pState->mask() & vcl::WindowDataMask::PosSize) != vcl::WindowDataMask::PosSize) + GetPosSize(aPosSize); + + sal_uInt16 nPosFlags = 0; + + // change requested properties + if (pState->mask() & vcl::WindowDataMask::X) + { + aPosSize.SetPosX(pState->x() - (mpParent ? mpParent->maGeometry.x() : 0)); + nPosFlags |= SAL_FRAME_POSSIZE_X; + } + if (pState->mask() & vcl::WindowDataMask::Y) + { + aPosSize.SetPosY(pState->y() - (mpParent ? mpParent->maGeometry.y() : 0)); + nPosFlags |= SAL_FRAME_POSSIZE_Y; + } + if (pState->mask() & vcl::WindowDataMask::Width) + { + tools::Long nWidth = pState->width() > 0 ? pState->width() - 1 : 0; + aPosSize.setWidth (nWidth); + bDoAdjust = true; + } + if (pState->mask() & vcl::WindowDataMask::Height) + { + int nHeight = pState->height() > 0 ? pState->height() - 1 : 0; + aPosSize.setHeight (nHeight); + bDoAdjust = true; + } + + const AbsoluteScreenPixelSize& aScreenSize = pDisplay_->getDataForScreen( m_nXScreen ).m_aSize; + + if( bDoAdjust && aPosSize.GetWidth() <= aScreenSize.Width() + && aPosSize.GetHeight() <= aScreenSize.Height() ) + { + SalFrameGeometry aGeom = maGeometry; + + if( ! (nStyle_ & ( SalFrameStyleFlags::FLOAT | SalFrameStyleFlags::PLUG ) ) && + mpParent && aGeom.leftDecoration() == 0 && aGeom.topDecoration() == 0) + { + aGeom = mpParent->maGeometry; + if (aGeom.leftDecoration() == 0 && aGeom.topDecoration() == 0) + aGeom.setDecorations(5, 20, 5, 5); + } + + auto nRight = aPosSize.Right() + (mpParent ? mpParent->maGeometry.x() : 0); + auto nBottom = aPosSize.Bottom() + (mpParent ? mpParent->maGeometry.y() : 0); + auto nLeft = aPosSize.Left() + (mpParent ? mpParent->maGeometry.x() : 0); + auto nTop = aPosSize.Top() + (mpParent ? mpParent->maGeometry.y() : 0); + + // adjust position so that frame fits onto screen + if( nRight+static_cast(aGeom.rightDecoration()) > aScreenSize.Width()-1 ) + aPosSize.Move( aScreenSize.Width() - nRight - static_cast(aGeom.rightDecoration()), 0 ); + if( nBottom+static_cast(aGeom.bottomDecoration()) > aScreenSize.Height()-1 ) + aPosSize.Move( 0, aScreenSize.Height() - nBottom - static_cast(aGeom.bottomDecoration()) ); + if( nLeft < static_cast(aGeom.leftDecoration()) ) + aPosSize.Move( static_cast(aGeom.leftDecoration()) - nLeft, 0 ); + if( nTop < static_cast(aGeom.topDecoration()) ) + aPosSize.Move( 0, static_cast(aGeom.topDecoration()) - nTop ); + } + + SetPosSize(aPosSize.getX(), aPosSize.getY(), + aPosSize.GetWidth(), aPosSize.GetHeight(), + SAL_FRAME_POSSIZE_WIDTH | SAL_FRAME_POSSIZE_HEIGHT | + nPosFlags); + } + } + + // request for status change + if (!(pState->mask() & vcl::WindowDataMask::State)) + return; + + if (pState->state() & vcl::WindowState::Maximized) + { + nShowState_ = X11ShowState::Normal; + if( ! (pState->state() & (vcl::WindowState::MaximizedHorz|vcl::WindowState::MaximizedVert) ) ) + Maximize(); + else + { + bool bHorz(pState->state() & vcl::WindowState::MaximizedHorz); + bool bVert(pState->state() & vcl::WindowState::MaximizedVert); + GetDisplay()->getWMAdaptor()->maximizeFrame( this, bHorz, bVert ); + } + maRestorePosSize = AbsoluteScreenPixelRectangle(pState->posSize()); + } + else if( mbMaximizedHorz || mbMaximizedVert ) + GetDisplay()->getWMAdaptor()->maximizeFrame( this, false, false ); + + if (pState->state() & vcl::WindowState::Minimized) + { + if (nShowState_ == X11ShowState::Unknown) + nShowState_ = X11ShowState::Normal; + Minimize(); + } + if (pState->state() & vcl::WindowState::Normal) + { + if (nShowState_ != X11ShowState::Normal) + Restore(); + } +} + +bool X11SalFrame::GetWindowState( vcl::WindowData* pState ) +{ + if( X11ShowState::Minimized == nShowState_ ) + pState->setState(vcl::WindowState::Minimized); + else + pState->setState(vcl::WindowState::Normal); + + AbsoluteScreenPixelRectangle aPosSize; + if( maRestorePosSize.IsEmpty() ) + GetPosSize( aPosSize ); + else + aPosSize = maRestorePosSize; + + if( mbMaximizedHorz ) + pState->rState() |= vcl::WindowState::MaximizedHorz; + if( mbMaximizedVert ) + pState->rState() |= vcl::WindowState::MaximizedVert; + + pState->setPosSize(tools::Rectangle(aPosSize)); + pState->setMask(vcl::WindowDataMask::PosSizeState); + + if (! maRestorePosSize.IsEmpty() ) + { + GetPosSize( aPosSize ); + pState->rState() |= vcl::WindowState::Maximized; + pState->SetMaximizedX(aPosSize.Left()); + pState->SetMaximizedY(aPosSize.Top()); + pState->SetMaximizedWidth(aPosSize.GetWidth()); + pState->SetMaximizedHeight(aPosSize.GetHeight()); + pState->rMask() |= FRAMESTATE_MASK_MAXIMIZED_GEOMETRY; + } + + return true; +} + +void X11SalFrame::SetMenu( SalMenu* ) +{ +} + +void X11SalFrame::GetPosSize( AbsoluteScreenPixelRectangle &rPosSize ) +{ + if( maGeometry.width() < 1 || maGeometry.height() < 1 ) + { + const AbsoluteScreenPixelSize& aScreenSize = pDisplay_->getDataForScreen( m_nXScreen ).m_aSize; + tools::Long w = aScreenSize.Width() - maGeometry.leftDecoration() - maGeometry.rightDecoration(); + tools::Long h = aScreenSize.Height() - maGeometry.topDecoration() - maGeometry.bottomDecoration(); + + rPosSize = AbsoluteScreenPixelRectangle( AbsoluteScreenPixelPoint( maGeometry.x(), maGeometry.y() ), AbsoluteScreenPixelSize( w, h ) ); + } + else + rPosSize = AbsoluteScreenPixelRectangle( AbsoluteScreenPixelPoint( maGeometry.x(), maGeometry.y() ), + AbsoluteScreenPixelSize( maGeometry.width(), maGeometry.height() ) ); +} + +void X11SalFrame::SetSize( const Size &rSize ) +{ + if( rSize.IsEmpty() ) + return; + + if( ! ( nStyle_ & SalFrameStyleFlags::SIZEABLE ) + && ! IsChildWindow() + && ( nStyle_ & (SalFrameStyleFlags::FLOAT|SalFrameStyleFlags::OWNERDRAWDECORATION) ) != SalFrameStyleFlags::FLOAT ) + { + XSizeHints* pHints = XAllocSizeHints(); + tools::Long nSupplied = 0; + XGetWMNormalHints( GetXDisplay(), + GetShellWindow(), + pHints, + &nSupplied + ); + pHints->min_width = rSize.Width(); + pHints->min_height = rSize.Height(); + pHints->max_width = rSize.Width(); + pHints->max_height = rSize.Height(); + pHints->flags |= PMinSize | PMaxSize; + XSetWMNormalHints( GetXDisplay(), + GetShellWindow(), + pHints ); + XFree( pHints ); + } + XResizeWindow( GetXDisplay(), IsSysChildWindow() ? GetWindow() : GetShellWindow(), rSize.Width(), rSize.Height() ); + if( GetWindow() != GetShellWindow() ) + { + if( nStyle_ & SalFrameStyleFlags::PLUG ) + XMoveResizeWindow( GetXDisplay(), GetWindow(), 0, 0, rSize.Width(), rSize.Height() ); + else + XResizeWindow( GetXDisplay(), GetWindow(), rSize.Width(), rSize.Height() ); + } + + cairo_xlib_surface_set_size(mpSurface, rSize.Width(), rSize.Height()); + maGeometry.setSize(rSize); + + // allow the external status window to reposition + if (mbInputFocus && mpInputContext != nullptr) + mpInputContext->SetICFocus ( this ); +} + +void X11SalFrame::SetPosSize( const AbsoluteScreenPixelRectangle &rPosSize ) +{ + XWindowChanges values; + values.x = rPosSize.Left(); + values.y = rPosSize.Top(); + values.width = rPosSize.GetWidth(); + values.height = rPosSize.GetHeight(); + + if( !values.width || !values.height ) + return; + + if( mpParent && ! IsSysChildWindow() ) + { + if( AllSettings::GetLayoutRTL() ) + values.x = mpParent->maGeometry.width()-values.width-1-values.x; + + ::Window aChild; + // coordinates are relative to parent, so translate to root coordinates + XTranslateCoordinates( GetDisplay()->GetDisplay(), + mpParent->GetWindow(), + GetDisplay()->GetRootWindow( m_nXScreen ), + values.x, values.y, + &values.x, &values.y, + & aChild ); + } + + bool bMoved = false; + bool bSized = false; + if( values.x != maGeometry.x() || values.y != maGeometry.y() ) + bMoved = true; + if( values.width != static_cast(maGeometry.width()) || values.height != static_cast(maGeometry.height()) ) + bSized = true; + + // do not set WMNormalHints for... + if( + // child windows + ! IsChildWindow() + // popups (menu, help window, etc.) + && (nStyle_ & (SalFrameStyleFlags::FLOAT|SalFrameStyleFlags::OWNERDRAWDECORATION) ) != SalFrameStyleFlags::FLOAT + // shown, sizeable windows + && ( nShowState_ == X11ShowState::Unknown || + nShowState_ == X11ShowState::Hidden || + ! ( nStyle_ & SalFrameStyleFlags::SIZEABLE ) + ) + ) + { + XSizeHints* pHints = XAllocSizeHints(); + tools::Long nSupplied = 0; + XGetWMNormalHints( GetXDisplay(), + GetShellWindow(), + pHints, + &nSupplied + ); + if( ! ( nStyle_ & SalFrameStyleFlags::SIZEABLE ) ) + { + pHints->min_width = rPosSize.GetWidth(); + pHints->min_height = rPosSize.GetHeight(); + pHints->max_width = rPosSize.GetWidth(); + pHints->max_height = rPosSize.GetHeight(); + pHints->flags |= PMinSize | PMaxSize; + } + if( nShowState_ == X11ShowState::Unknown || nShowState_ == X11ShowState::Hidden ) + { + pHints->flags |= PPosition | PWinGravity; + pHints->x = values.x; + pHints->y = values.y; + pHints->win_gravity = pDisplay_->getWMAdaptor()->getPositionWinGravity(); + } + if( mbFullScreen ) + { + pHints->max_width = 10000; + pHints->max_height = 10000; + pHints->flags |= PMaxSize; + } + XSetWMNormalHints( GetXDisplay(), + GetShellWindow(), + pHints ); + XFree( pHints ); + } + + XMoveResizeWindow( GetXDisplay(), IsSysChildWindow() ? GetWindow() : GetShellWindow(), values.x, values.y, values.width, values.height ); + if( GetShellWindow() != GetWindow() ) + { + if( nStyle_ & SalFrameStyleFlags::PLUG ) + XMoveResizeWindow( GetXDisplay(), GetWindow(), 0, 0, values.width, values.height ); + else + XMoveResizeWindow( GetXDisplay(), GetWindow(), values.x, values.y, values.width, values.height ); + } + + cairo_xlib_surface_set_size(mpSurface, values.width, values.height); + maGeometry.setPosSize({ values.x, values.y }, { values.width, values.height }); + if( IsSysChildWindow() && mpParent ) + // translate back to root coordinates + maGeometry.move(mpParent->maGeometry.x(), mpParent->maGeometry.y()); + + updateScreenNumber(); + if( bSized && ! bMoved ) + CallCallback( SalEvent::Resize, nullptr ); + else if( bMoved && ! bSized ) + CallCallback( SalEvent::Move, nullptr ); + else + CallCallback( SalEvent::MoveResize, nullptr ); + + // allow the external status window to reposition + if (mbInputFocus && mpInputContext != nullptr) + mpInputContext->SetICFocus ( this ); +} + +void X11SalFrame::Minimize() +{ + if( IsSysChildWindow() ) + return; + + if( X11ShowState::Unknown == nShowState_ || X11ShowState::Hidden == nShowState_ ) + { + SAL_WARN( "vcl", "X11SalFrame::Minimize on withdrawn window" ); + return; + } + + if( XIconifyWindow( GetXDisplay(), + GetShellWindow(), + pDisplay_->GetDefaultXScreen().getXScreen() ) ) + nShowState_ = X11ShowState::Minimized; +} + +void X11SalFrame::Maximize() +{ + if( IsSysChildWindow() ) + return; + + if( X11ShowState::Minimized == nShowState_ ) + { + GetDisplay()->getWMAdaptor()->frameIsMapping( this ); + XMapWindow( GetXDisplay(), GetShellWindow() ); + nShowState_ = X11ShowState::Normal; + } + + pDisplay_->getWMAdaptor()->maximizeFrame( this ); +} + +void X11SalFrame::Restore() +{ + if( IsSysChildWindow() ) + return; + + if( X11ShowState::Unknown == nShowState_ || X11ShowState::Hidden == nShowState_ ) + { + SAL_INFO( "vcl", "X11SalFrame::Restore on withdrawn window" ); + return; + } + + if( X11ShowState::Minimized == nShowState_ ) + { + GetDisplay()->getWMAdaptor()->frameIsMapping( this ); + XMapWindow( GetXDisplay(), GetShellWindow() ); + nShowState_ = X11ShowState::Normal; + } + + pDisplay_->getWMAdaptor()->maximizeFrame( this, false, false ); +} + +void X11SalFrame::SetScreenNumber( unsigned int nNewScreen ) +{ + if( nNewScreen == maGeometry.screen() ) + return; + + if( GetDisplay()->IsXinerama() && GetDisplay()->GetXineramaScreens().size() > 1 ) + { + if( nNewScreen >= GetDisplay()->GetXineramaScreens().size() ) + return; + + tools::Rectangle aOldScreenRect( GetDisplay()->GetXineramaScreens()[maGeometry.screen()] ); + tools::Rectangle aNewScreenRect( GetDisplay()->GetXineramaScreens()[nNewScreen] ); + bool bVisible = bMapped_; + if( bVisible ) + Show( false ); + maGeometry.setX(aNewScreenRect.Left() + (maGeometry.x() - aOldScreenRect.Left())); + maGeometry.setY(aNewScreenRect.Top() + (maGeometry.y() - aOldScreenRect.Top())); + createNewWindow( None, m_nXScreen ); + if( bVisible ) + Show( true ); + maGeometry.setScreen(nNewScreen); + } + else if( nNewScreen < GetDisplay()->GetXScreenCount() ) + { + bool bVisible = bMapped_; + if( bVisible ) + Show( false ); + createNewWindow( None, SalX11Screen( nNewScreen ) ); + if( bVisible ) + Show( true ); + maGeometry.setScreen(nNewScreen); + } +} + +void X11SalFrame::SetApplicationID( const OUString &rWMClass ) +{ + if( rWMClass != m_sWMClass && ! IsChildWindow() ) + { + m_sWMClass = rWMClass; + updateWMClass(); + for (auto const& child : maChildren) + child->SetApplicationID(rWMClass); + } +} + +void X11SalFrame::updateWMClass() +{ + XClassHint* pClass = XAllocClassHint(); + OString aResName = SalGenericSystem::getFrameResName(); + pClass->res_name = const_cast(aResName.getStr()); + + OString aResClass = OUStringToOString(m_sWMClass, RTL_TEXTENCODING_ASCII_US); + const char *pResClass = !aResClass.isEmpty() ? aResClass.getStr() : + SalGenericSystem::getFrameClassName(); + + pClass->res_class = const_cast(pResClass); + XSetClassHint( GetXDisplay(), GetShellWindow(), pClass ); + XFree( pClass ); +} + +void X11SalFrame::ShowFullScreen( bool bFullScreen, sal_Int32 nScreen ) +{ + if( GetDisplay()->IsXinerama() && GetDisplay()->GetXineramaScreens().size() > 1 ) + { + if( mbFullScreen == bFullScreen ) + return; + if( bFullScreen ) + { + maRestorePosSize = AbsoluteScreenPixelRectangle(maGeometry.posSize()); + AbsoluteScreenPixelRectangle aRect; + if( nScreen < 0 || o3tl::make_unsigned(nScreen) >= GetDisplay()->GetXineramaScreens().size() ) + aRect = AbsoluteScreenPixelRectangle( AbsoluteScreenPixelPoint(0,0), GetDisplay()->GetScreenSize( m_nXScreen ) ); + else + aRect = GetDisplay()->GetXineramaScreens()[nScreen]; + m_bIsPartialFullScreen = true; + bool bVisible = bMapped_; + if( bVisible ) + Show( false ); + maGeometry.setPosSize(tools::Rectangle(aRect)); + mbMaximizedHorz = mbMaximizedVert = false; + mbFullScreen = true; + createNewWindow( None, m_nXScreen ); + if( GetDisplay()->getWMAdaptor()->isLegacyPartialFullscreen() ) + GetDisplay()->getWMAdaptor()->enableAlwaysOnTop( this, true ); + else + GetDisplay()->getWMAdaptor()->showFullScreen( this, true ); + if( bVisible ) + Show(true); + + } + else + { + mbFullScreen = false; + m_bIsPartialFullScreen = false; + bool bVisible = bMapped_; + AbsoluteScreenPixelRectangle aRect = maRestorePosSize; + maRestorePosSize = AbsoluteScreenPixelRectangle(); + if( bVisible ) + Show( false ); + createNewWindow( None, m_nXScreen ); + if( !aRect.IsEmpty() ) + SetPosSize( aRect.Left(), aRect.Top(), aRect.GetWidth(), aRect.GetHeight(), + SAL_FRAME_POSSIZE_X | SAL_FRAME_POSSIZE_Y | + SAL_FRAME_POSSIZE_WIDTH | SAL_FRAME_POSSIZE_HEIGHT ); + if( bVisible ) + Show( true ); + } + } + else + { + if( nScreen < 0 || o3tl::make_unsigned(nScreen) >= GetDisplay()->GetXScreenCount() ) + nScreen = m_nXScreen.getXScreen(); + if( nScreen != static_cast(m_nXScreen.getXScreen()) ) + { + bool bVisible = bMapped_; + if( mbFullScreen ) + pDisplay_->getWMAdaptor()->showFullScreen( this, false ); + if( bVisible ) + Show( false ); + createNewWindow( None, SalX11Screen( nScreen ) ); + if( mbFullScreen ) + pDisplay_->getWMAdaptor()->showFullScreen( this, true ); + if( bVisible ) + Show( true ); + } + if( mbFullScreen == bFullScreen ) + return; + + pDisplay_->getWMAdaptor()->showFullScreen( this, bFullScreen ); + } +} + +void X11SalFrame::StartPresentation( bool bStart ) +{ + maSessionManagerInhibitor.inhibit( bStart, + u"presentation", + APPLICATION_INHIBIT_IDLE, + mhWindow, + GetXDisplay() ); + + if( ! bStart && hPresentationWindow != None ) + doReparentPresentationDialogues( GetDisplay() ); + hPresentationWindow = (bStart && IsOverrideRedirect() ) ? GetWindow() : None; + + if( bStart && hPresentationWindow ) + { + /* #i10559# workaround for WindowMaker: try to restore + * current focus after presentation window is gone + */ + int revert_to = 0; + XGetInputFocus( GetXDisplay(), &hPresFocusWindow, &revert_to ); + } +} + +// Pointer + +void X11SalFrame::SetPointer( PointerStyle ePointerStyle ) +{ + hCursor_ = pDisplay_->GetPointer( ePointerStyle ); + XDefineCursor( GetXDisplay(), GetWindow(), hCursor_ ); + + if( IsCaptured() || nVisibleFloats > 0 ) + XChangeActivePointerGrab( GetXDisplay(), + PointerMotionMask|ButtonPressMask|ButtonReleaseMask, + hCursor_, + CurrentTime ); +} + +void X11SalFrame::SetPointerPos(tools::Long nX, tools::Long nY) +{ + /* when the application tries to center the mouse in the dialog the + * window isn't mapped already. So use coordinates relative to the root window. + */ + unsigned int nWindowLeft = maGeometry.x() + nX; + unsigned int nWindowTop = maGeometry.y() + nY; + + XWarpPointer( GetXDisplay(), None, pDisplay_->GetRootWindow( pDisplay_->GetDefaultXScreen() ), + 0, 0, 0, 0, nWindowLeft, nWindowTop); +} + +// delay handling of extended text input +#if !defined(__synchronous_extinput__) +void +X11SalFrame::HandleExtTextEvent (XClientMessageEvent const *pEvent) +{ + #if SAL_TYPES_SIZEOFLONG > 4 + void* pExtTextEvent = reinterpret_cast( (pEvent->data.l[0] & 0xffffffff) + | (pEvent->data.l[1] << 32) ); + #else + void* pExtTextEvent = reinterpret_cast(pEvent->data.l[0]); + #endif + SalEvent nExtTextEventType = SalEvent(pEvent->data.l[2]); + + CallCallback(nExtTextEventType, pExtTextEvent); + + switch (nExtTextEventType) + { + case SalEvent::EndExtTextInput: + break; + + case SalEvent::ExtTextInput: + break; + + default: + SAL_WARN("vcl.window", + "X11SalFrame::HandleExtTextEvent: invalid extended input."); + } +} +#endif /* defined(__synchronous_extinput__) */ + +// PostEvent + +bool X11SalFrame::PostEvent(std::unique_ptr pData) +{ + GetDisplay()->SendInternalEvent( this, pData.release() ); + return true; +} + +// Title + +void X11SalFrame::SetTitle( const OUString& rTitle ) +{ + if( ! ( IsChildWindow() || (nStyle_ & SalFrameStyleFlags::FLOAT ) ) ) + { + m_aTitle = rTitle; + GetDisplay()->getWMAdaptor()->setWMName( this, rTitle ); + } +} + +void X11SalFrame::Flush() +{ + if( pGraphics_ ) + pGraphics_->Flush(); + XFlush( GetDisplay()->GetDisplay() ); +} + +// Keyboard + +void X11SalFrame::SetInputContext( SalInputContext* pContext ) +{ + if (pContext == nullptr) + return; + + // 1. We should create an input context for this frame + // only when InputContextFlags::Text is set. + + if (!(pContext->mnOptions & InputContextFlags::Text)) + { + if( mpInputContext ) + mpInputContext->Unmap(); + return; + } + + // 2. We should use on-the-spot inputstyle + // only when InputContextFlags::ExtTExt is set. + + if (mpInputContext == nullptr) + { + mpInputContext.reset( new SalI18N_InputContext( this ) ); + if (mpInputContext->UseContext()) + { + mpInputContext->ExtendEventMask( GetShellWindow() ); + if (mbInputFocus) + mpInputContext->SetICFocus( this ); + } + } + else + mpInputContext->Map( this ); +} + +void X11SalFrame::EndExtTextInput( EndExtTextInputFlags ) +{ + if (mpInputContext != nullptr) + mpInputContext->EndExtTextInput(); +} + +OUString X11SalFrame::GetKeyName( sal_uInt16 nKeyCode ) +{ + return GetDisplay()->GetKeyName( nKeyCode ); +} + +bool X11SalFrame::MapUnicodeToKeyCode( sal_Unicode , LanguageType , vcl::KeyCode& ) +{ + // not supported yet + return false; +} + +LanguageType X11SalFrame::GetInputLanguage() +{ + // could be improved by checking unicode ranges of the last input + return LANGUAGE_DONTKNOW; +} + +// Settings + +void X11SalFrame::UpdateSettings( AllSettings& rSettings ) +{ + StyleSettings aStyleSettings = rSettings.GetStyleSettings(); + aStyleSettings.SetCursorBlinkTime( 500 ); + aStyleSettings.SetMenuBarTextColor( aStyleSettings.GetPersonaMenuBarTextColor().value_or( COL_BLACK ) ); + rSettings.SetStyleSettings( aStyleSettings ); +} + +void X11SalFrame::CaptureMouse( bool bCapture ) +{ + nCaptured_ = pDisplay_->CaptureMouse( bCapture ? this : nullptr ); +} + +void X11SalFrame::SetParent( SalFrame* pNewParent ) +{ + if( mpParent != pNewParent ) + { + if( mpParent ) + mpParent->maChildren.remove( this ); + + mpParent = static_cast(pNewParent); + mpParent->maChildren.push_back( this ); + if( mpParent->m_nXScreen != m_nXScreen ) + createNewWindow( None, mpParent->m_nXScreen ); + GetDisplay()->getWMAdaptor()->changeReferenceFrame( this, mpParent ); + } +} + +SalFrame* X11SalFrame::GetParent() const +{ + return mpParent; +} + +void X11SalFrame::createNewWindow( ::Window aNewParent, SalX11Screen nXScreen ) +{ + bool bWasVisible = bMapped_; + if( bWasVisible ) + Show( false ); + + if( nXScreen.getXScreen() >= GetDisplay()->GetXScreenCount() ) + nXScreen = m_nXScreen; + + SystemParentData aParentData; + aParentData.nSize = sizeof(SystemParentData); + aParentData.aWindow = aNewParent; + aParentData.bXEmbedSupport = (aNewParent != None && m_bXEmbed); // caution: this is guesswork + if( aNewParent == None ) + { + aParentData.aWindow = None; + m_bXEmbed = false; + } + else + { + // is new parent a root window ? + Display* pDisp = GetDisplay()->GetDisplay(); + int nScreens = GetDisplay()->GetXScreenCount(); + for( int i = 0; i < nScreens; i++ ) + { + if( aNewParent == RootWindow( pDisp, i ) ) + { + nXScreen = SalX11Screen( i ); + aParentData.aWindow = None; + m_bXEmbed = false; + break; + } + } + } + + // first deinit frame + updateGraphics(true); + if( mpInputContext ) + { + mpInputContext->UnsetICFocus(); + mpInputContext->Unmap(); + } + if( GetWindow() == hPresentationWindow ) + { + hPresentationWindow = None; + doReparentPresentationDialogues( GetDisplay() ); + } + if (mpSurface) + { + cairo_surface_destroy(mpSurface); + mpSurface = nullptr; + } + XDestroyWindow( GetXDisplay(), mhWindow ); + mhWindow = None; + + // now init with new parent again + if ( aParentData.aWindow != None ) + Init( nStyle_ | SalFrameStyleFlags::PLUG, nXScreen, &aParentData ); + else + Init( nStyle_ & ~SalFrameStyleFlags::PLUG, nXScreen, nullptr, true ); + + // update graphics if necessary + updateGraphics(false); + + if( ! m_aTitle.isEmpty() ) + SetTitle( m_aTitle ); + + if( mpParent ) + { + if( mpParent->m_nXScreen != m_nXScreen ) + SetParent( nullptr ); + else + pDisplay_->getWMAdaptor()->changeReferenceFrame( this, mpParent ); + } + + if( bWasVisible ) + Show( true ); + + std::list< X11SalFrame* > aChildren = maChildren; + for (auto const& child : aChildren) + child->createNewWindow( None, m_nXScreen ); + + // FIXME: SalObjects +} + +void X11SalFrame::SetPluginParent( SystemParentData* pNewParent ) +{ + if( pNewParent->nSize >= sizeof(SystemParentData) ) + m_bXEmbed = pNewParent->aWindow != None && pNewParent->bXEmbedSupport; + + createNewWindow(pNewParent->aWindow); +} + +// Sound +void X11SalFrame::Beep() +{ + GetDisplay()->Beep(); +} + +// Event Handling + +static sal_uInt16 sal_GetCode( int state ) +{ + sal_uInt16 nCode = 0; + + if( state & Button1Mask ) + nCode |= MOUSE_LEFT; + if( state & Button2Mask ) + nCode |= MOUSE_MIDDLE; + if( state & Button3Mask ) + nCode |= MOUSE_RIGHT; + + if( state & ShiftMask ) + nCode |= KEY_SHIFT; + if( state & ControlMask ) + nCode |= KEY_MOD1; + if( state & Mod1Mask ) + nCode |= KEY_MOD2; + + // Map Meta/Super modifier to MOD3 on all Unix systems + // except macOS + if( state & Mod3Mask ) + nCode |= KEY_MOD3; + + return nCode; +} + +SalFrame::SalPointerState X11SalFrame::GetPointerState() +{ + SalPointerState aState; + ::Window aRoot, aChild; + int rx, ry, wx, wy; + unsigned int nMask = 0; + XQueryPointer( GetXDisplay(), + GetShellWindow(), + &aRoot, + &aChild, + &rx, &ry, + &wx, &wy, + &nMask + ); + + aState.maPos = Point(wx, wy); + aState.mnState = sal_GetCode( nMask ); + return aState; +} + +KeyIndicatorState X11SalFrame::GetIndicatorState() +{ + return vcl_sal::getSalDisplay(GetGenericUnixSalData())->GetIndicatorState(); +} + +void X11SalFrame::SimulateKeyPress( sal_uInt16 nKeyCode ) +{ + vcl_sal::getSalDisplay(GetGenericUnixSalData())->SimulateKeyPress(nKeyCode); +} + +namespace +{ +struct CompressWheelEventsData +{ + XEvent* firstEvent; + bool ignore; + int count; // number of compressed events +}; + +Bool compressWheelEvents( Display*, XEvent* event, XPointer p ) +{ + CompressWheelEventsData* data = reinterpret_cast< CompressWheelEventsData* >( p ); + if( data->ignore ) + return False; // we're already after the events to compress + if( event->type == ButtonPress || event->type == ButtonRelease ) + { + const unsigned int mask = Button1Mask << ( event->xbutton.button - Button1 ); + if( event->xbutton.button == data->firstEvent->xbutton.button + && event->xbutton.window == data->firstEvent->xbutton.window + && event->xbutton.x == data->firstEvent->xbutton.x + && event->xbutton.y == data->firstEvent->xbutton.y + && ( event->xbutton.state | mask ) == ( data->firstEvent->xbutton.state | mask )) + { + // Count if it's another press (i.e. wheel start event). + if( event->type == ButtonPress ) + ++data->count; + return True; // And remove the event from the queue. + } + } + // Non-matching event, skip certain events that cannot possibly affect input processing, + // but otherwise ignore all further events. + switch( event->type ) + { + case Expose: + case NoExpose: + break; + default: + data->ignore = true; + break; + } + return False; +} + +} // namespace + +bool X11SalFrame::HandleMouseEvent( XEvent *pEvent ) +{ + SalMouseEvent aMouseEvt; + SalEvent nEvent = SalEvent::NONE; + bool bClosePopups = false; + + if( nVisibleFloats && pEvent->type == EnterNotify ) + return false; + + if( LeaveNotify == pEvent->type || EnterNotify == pEvent->type ) + { + /* + * some WMs (and/or) applications have a passive grab on + * mouse buttons (XGrabButton). This leads to enter/leave notifies + * with mouse buttons pressed in the state mask before the actual + * ButtonPress event gets dispatched. But EnterNotify + * is reported in vcl as MouseMove event. Some office code + * decides that a pressed button in a MouseMove belongs to + * a drag operation which leads to doing things differently. + * + * ignore Enter/LeaveNotify resulting from grabs so that + * help windows do not disappear just after appearing + * + * hopefully this workaround will not break anything. + */ + if( pEvent->xcrossing.mode == NotifyGrab || pEvent->xcrossing.mode == NotifyUngrab ) + return false; + + aMouseEvt.mnX = pEvent->xcrossing.x; + aMouseEvt.mnY = pEvent->xcrossing.y; + aMouseEvt.mnTime = pEvent->xcrossing.time; + aMouseEvt.mnCode = sal_GetCode( pEvent->xcrossing.state ); + aMouseEvt.mnButton = 0; + + nEvent = LeaveNotify == pEvent->type + ? SalEvent::MouseLeave + : SalEvent::MouseMove; + } + else if( pEvent->type == MotionNotify ) + { + aMouseEvt.mnX = pEvent->xmotion.x; + aMouseEvt.mnY = pEvent->xmotion.y; + aMouseEvt.mnTime = pEvent->xmotion.time; + aMouseEvt.mnCode = sal_GetCode( pEvent->xmotion.state ); + + aMouseEvt.mnButton = 0; + + nEvent = SalEvent::MouseMove; + if( nVisibleFloats > 0 && mpParent ) + { + Cursor aCursor = mpParent->GetCursor(); + if( pEvent->xmotion.x >= 0 && pEvent->xmotion.x < static_cast(maGeometry.width()) && + pEvent->xmotion.y >= 0 && pEvent->xmotion.y < static_cast(maGeometry.height()) ) + aCursor = None; + + XChangeActivePointerGrab( GetXDisplay(), + PointerMotionMask|ButtonPressMask|ButtonReleaseMask, + aCursor, + CurrentTime ); + } + } + else + { + // let mouse events reach the correct window + if( nVisibleFloats < 1 ) + { + if( ! (nStyle_ & SalFrameStyleFlags::OWNERDRAWDECORATION) ) + XUngrabPointer( GetXDisplay(), CurrentTime ); + } + else if( pEvent->type == ButtonPress ) + { + // see if the user clicks outside all of the floats + // if yes release the grab + bool bInside = false; + for (auto pSalFrame : GetDisplay()->getFrames() ) + { + const X11SalFrame* pFrame = static_cast< const X11SalFrame* >( pSalFrame ); + if( pFrame->IsFloatGrabWindow() && + pFrame->bMapped_ && + pEvent->xbutton.x_root >= pFrame->maGeometry.x() && + pEvent->xbutton.x_root < pFrame->maGeometry.x() + static_cast(pFrame->maGeometry.width()) && + pEvent->xbutton.y_root >= pFrame->maGeometry.y() && + pEvent->xbutton.y_root < pFrame->maGeometry.y() + static_cast(pFrame->maGeometry.height()) ) + { + bInside = true; + break; + } + } + if( ! bInside ) + { + // need not take care of the XUngrabPointer in Show( false ) + // because XUngrabPointer does not produce errors if pointer + // is not grabbed + XUngrabPointer( GetXDisplay(), CurrentTime ); + bClosePopups = true; + + /* #i15246# only close popups if pointer is outside all our frames + * cannot use our own geometry data here because stacking + * is unknown (the above case implicitly assumes + * that floats are on top which should be true) + */ + ::Window aRoot, aChild; + int root_x, root_y, win_x, win_y; + unsigned int mask_return; + if( XQueryPointer( GetXDisplay(), + GetDisplay()->GetRootWindow( m_nXScreen ), + &aRoot, &aChild, + &root_x, &root_y, + &win_x, &win_y, + &mask_return ) + && aChild // pointer may not be in any child + ) + { + for (auto pSalFrame : GetDisplay()->getFrames() ) + { + const X11SalFrame* pFrame = static_cast< const X11SalFrame* >( pSalFrame ); + if( ! pFrame->IsFloatGrabWindow() + && ( pFrame->GetWindow() == aChild || + pFrame->GetShellWindow() == aChild || + pFrame->GetStackingWindow() == aChild ) + ) + { + // #i63638# check that pointer is inside window, not + // only inside stacking window + if( root_x >= pFrame->maGeometry.x() && root_x < sal::static_int_cast< int >(pFrame->maGeometry.x()+pFrame->maGeometry.width()) && + root_y >= pFrame->maGeometry.y() && root_y < sal::static_int_cast< int >(pFrame->maGeometry.x()+pFrame->maGeometry.height()) ) + { + bClosePopups = false; + } + break; + } + } + } + } + } + + if( m_bXEmbed && pEvent->xbutton.button == Button1 ) + askForXEmbedFocus( pEvent->xbutton.time ); + + if( pEvent->xbutton.button == Button1 || + pEvent->xbutton.button == Button2 || + pEvent->xbutton.button == Button3 ) + { + aMouseEvt.mnX = pEvent->xbutton.x; + aMouseEvt.mnY = pEvent->xbutton.y; + aMouseEvt.mnTime = pEvent->xbutton.time; + aMouseEvt.mnCode = sal_GetCode( pEvent->xbutton.state ); + + if( Button1 == pEvent->xbutton.button ) + aMouseEvt.mnButton = MOUSE_LEFT; + else if( Button2 == pEvent->xbutton.button ) + aMouseEvt.mnButton = MOUSE_MIDDLE; + else if( Button3 == pEvent->xbutton.button ) + aMouseEvt.mnButton = MOUSE_RIGHT; + + nEvent = ButtonPress == pEvent->type + ? SalEvent::MouseButtonDown + : SalEvent::MouseButtonUp; + } + else if( pEvent->xbutton.button == Button4 || + pEvent->xbutton.button == Button5 || + pEvent->xbutton.button == Button6 || + pEvent->xbutton.button == Button7 ) + { + const bool bIncrement( + pEvent->xbutton.button == Button4 || + pEvent->xbutton.button == Button6 ); + const bool bHoriz( + pEvent->xbutton.button == Button6 || + pEvent->xbutton.button == Button7 ); + + if( pEvent->type == ButtonRelease ) + return false; + + static sal_uLong nLines = 0; + if( ! nLines ) + { + char* pEnv = getenv( "SAL_WHEELLINES" ); + nLines = pEnv ? atoi( pEnv ) : 3; + if( nLines > 10 ) + nLines = SAL_WHEELMOUSE_EVENT_PAGESCROLL; + } + + // Compress consecutive wheel events (way too fine scrolling may cause lags if one scrolling steps takes long). + CompressWheelEventsData data; + data.firstEvent = pEvent; + data.count = 1; + XEvent dummy; + do + { + data.ignore = false; + } while( XCheckIfEvent( pEvent->xany.display, &dummy, compressWheelEvents, reinterpret_cast< XPointer >( &data ))); + + SalWheelMouseEvent aWheelEvt; + aWheelEvt.mnTime = pEvent->xbutton.time; + aWheelEvt.mnX = pEvent->xbutton.x; + aWheelEvt.mnY = pEvent->xbutton.y; + aWheelEvt.mnDelta = ( bIncrement ? 120 : -120 ) * data.count; + aWheelEvt.mnNotchDelta = bIncrement ? 1 : -1; + aWheelEvt.mnScrollLines = nLines * data.count; + aWheelEvt.mnCode = sal_GetCode( pEvent->xbutton.state ); + aWheelEvt.mbHorz = bHoriz; + + nEvent = SalEvent::WheelMouse; + + if( AllSettings::GetLayoutRTL() ) + aWheelEvt.mnX = nWidth_-1-aWheelEvt.mnX; + return CallCallback( nEvent, &aWheelEvt ); + } + } + + bool nRet = false; + if( nEvent == SalEvent::MouseLeave + || ( aMouseEvt.mnX < nWidth_ && aMouseEvt.mnX > -1 && + aMouseEvt.mnY < nHeight_ && aMouseEvt.mnY > -1 ) + || pDisplay_->MouseCaptured( this ) + ) + { + if( AllSettings::GetLayoutRTL() ) + aMouseEvt.mnX = nWidth_-1-aMouseEvt.mnX; + nRet = CallCallback( nEvent, &aMouseEvt ); + } + + if( bClosePopups ) + { + /* #108213# close popups after dispatching the event outside the popup; + * applications do weird things. + */ + ImplSVData* pSVData = ImplGetSVData(); + if (pSVData->mpWinData->mpFirstFloat) + { + if (!(pSVData->mpWinData->mpFirstFloat->GetPopupModeFlags() + & FloatWinPopupFlags::NoAppFocusClose)) + pSVData->mpWinData->mpFirstFloat->EndPopupMode(FloatWinPopupEndFlags::Cancel + | FloatWinPopupEndFlags::CloseAll); + } + } + + return nRet; +} + +namespace { + +// F10 means either KEY_F10 or KEY_MENU, which has to be decided +// in the independent part. +struct KeyAlternate +{ + sal_uInt16 nKeyCode; + sal_Unicode nCharCode; + KeyAlternate() : nKeyCode( 0 ), nCharCode( 0 ) {} + KeyAlternate( sal_uInt16 nKey, sal_Unicode nChar = 0 ) : nKeyCode( nKey ), nCharCode( nChar ) {} +}; + +} + +static KeyAlternate +GetAlternateKeyCode( const sal_uInt16 nKeyCode ) +{ + KeyAlternate aAlternate; + + switch( nKeyCode ) + { + case KEY_F10: aAlternate = KeyAlternate( KEY_MENU );break; + case KEY_F24: aAlternate = KeyAlternate( KEY_SUBTRACT, '-' );break; + } + + return aAlternate; +} + +void X11SalFrame::beginUnicodeSequence() +{ + OUString& rSeq( GetGenericUnixSalData()->GetUnicodeCommand() ); + vcl::DeletionListener aDeleteWatch( this ); + + if( !rSeq.isEmpty() ) + endUnicodeSequence(); + + rSeq = "u"; + + if( ! aDeleteWatch.isDeleted() ) + { + ExtTextInputAttr nTextAttr = ExtTextInputAttr::Underline; + SalExtTextInputEvent aEv; + aEv.maText = rSeq; + aEv.mpTextAttr = &nTextAttr; + aEv.mnCursorPos = 0; + aEv.mnCursorFlags = 0; + + CallCallback(SalEvent::ExtTextInput, static_cast(&aEv)); + } +} + +bool X11SalFrame::appendUnicodeSequence( sal_Unicode c ) +{ + bool bRet = false; + OUString& rSeq( GetGenericUnixSalData()->GetUnicodeCommand() ); + if( !rSeq.isEmpty() ) + { + // range check + if( (c >= '0' && c <= '9') || + (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F') ) + { + rSeq += OUStringChar(c); + std::vector attribs( rSeq.getLength(), ExtTextInputAttr::Underline ); + + SalExtTextInputEvent aEv; + aEv.maText = rSeq; + aEv.mpTextAttr = attribs.data(); + aEv.mnCursorPos = 0; + aEv.mnCursorFlags = 0; + + CallCallback(SalEvent::ExtTextInput, static_cast(&aEv)); + bRet = true; + } + else + bRet = endUnicodeSequence(); + } + else + endUnicodeSequence(); + return bRet; +} + +bool X11SalFrame::endUnicodeSequence() +{ + OUString& rSeq( GetGenericUnixSalData()->GetUnicodeCommand() ); + + vcl::DeletionListener aDeleteWatch( this ); + if( rSeq.getLength() > 1 && rSeq.getLength() < 6 ) + { + // cut the "u" + std::u16string_view aNumbers( rSeq.subView( 1 ) ); + sal_uInt32 nValue = o3tl::toUInt32(aNumbers, 16); + if( nValue >= 32 ) + { + ExtTextInputAttr nTextAttr = ExtTextInputAttr::Underline; + SalExtTextInputEvent aEv; + aEv.maText = OUString( sal_Unicode(nValue) ); + aEv.mpTextAttr = &nTextAttr; + aEv.mnCursorPos = 0; + aEv.mnCursorFlags = 0; + CallCallback(SalEvent::ExtTextInput, static_cast(&aEv)); + } + } + bool bWasInput = !rSeq.isEmpty(); + rSeq.clear(); + if( bWasInput && ! aDeleteWatch.isDeleted() ) + CallCallback(SalEvent::EndExtTextInput, nullptr); + return bWasInput; +} + +bool X11SalFrame::HandleKeyEvent( XKeyEvent *pEvent ) +{ + if( pEvent->type == KeyRelease ) + { + // Ignore autorepeat keyrelease events. If there is a series of keypress+keyrelease+keypress events + // generated by holding down a key, and if these are from autorepeat (keyrelease and the following keypress + // have the same timestamp), drop the autorepeat keyrelease event. Not exactly sure why this is done + // (possibly hiding differences between platforms, or just making it more sensible, because technically + // the key has not been released at all). + bool ignore = false; + // Discard queued excessive autorepeat events. + // If the user presses and holds down a key, the autorepeating keypress events + // may overload LO (e.g. if the key is PageDown and the LO cannot keep up scrolling). + // Reduce the load by simply discarding such excessive events (so for a KeyRelease event, + // check if it's followed by matching KeyPress+KeyRelease pair(s) and discard those). + // This shouldn't have any negative effects - unlike with normal (non-autorepeat + // events), the user is unlikely to rely on the exact number of resulting actions + // (since autorepeat generates keypress events rather quickly and it's hard to estimate + // how many exactly) and the idea should be just keeping the key pressed until something + // happens (in which case more events that just lag LO shouldn't make a difference). + Display* dpy = pEvent->display; + XKeyEvent previousRelease = *pEvent; + while( XPending( dpy )) + { + XEvent nextEvent1; + bool discard1 = false; + XNextEvent( dpy, &nextEvent1 ); + if( nextEvent1.type == KeyPress && nextEvent1.xkey.time == previousRelease.time + && !nextEvent1.xkey.send_event && nextEvent1.xkey.window == previousRelease.window + && nextEvent1.xkey.state == previousRelease.state && nextEvent1.xkey.keycode == previousRelease.keycode ) + { // This looks like another autorepeat keypress. + ignore = true; + if( XPending( dpy )) + { + XEvent nextEvent2; + XNextEvent( dpy, &nextEvent2 ); + if( nextEvent2.type == KeyRelease && nextEvent2.xkey.time <= ( previousRelease.time + 100 ) + && !nextEvent2.xkey.send_event && nextEvent2.xkey.window == previousRelease.window + && nextEvent2.xkey.state == previousRelease.state && nextEvent2.xkey.keycode == previousRelease.keycode ) + { // And the matching keyrelease -> drop them both. + discard1 = true; + previousRelease = nextEvent2.xkey; + ignore = false; // There either will be another autorepeating keypress that'll lead to discarding + // the pEvent keyrelease, it this discarding makes that keyrelease the last one. + } + else + { + XPutBackEvent( dpy, &nextEvent2 ); + break; + } + } + } + if( !discard1 ) + { // Unrelated event, put back and stop compressing. + XPutBackEvent( dpy, &nextEvent1 ); + break; + } + } + if( ignore ) // This autorepeating keyrelease is followed by another keypress. + return false; + } + + KeySym nKeySym; + KeySym nUnmodifiedKeySym; + int nLen = 2048; + char *pPrintable = static_cast(alloca( nLen )); + + // singlebyte code composed by input method, the new default + if (mpInputContext != nullptr && mpInputContext->UseContext()) + { + // returns a keysym as well as the pPrintable (in system encoding) + // printable may be empty. + Status nStatus; + nKeySym = pDisplay_->GetKeySym( pEvent, pPrintable, &nLen, + &nUnmodifiedKeySym, + &nStatus, mpInputContext->GetContext() ); + if ( nStatus == XBufferOverflow ) + { + // In case of overflow, XmbLookupString (called by GetKeySym) + // returns required size + // TODO : check if +1 is needed for 0 terminator + nLen += 1; + pPrintable = static_cast(alloca( nLen )); + nKeySym = pDisplay_->GetKeySym( pEvent, pPrintable, &nLen, + &nUnmodifiedKeySym, + &nStatus, mpInputContext->GetContext() ); + } + } + else + { + // fallback, this should never ever be called + Status nStatus = 0; + nKeySym = pDisplay_->GetKeySym( pEvent, pPrintable, &nLen, &nUnmodifiedKeySym, &nStatus ); + } + + SalKeyEvent aKeyEvt; + sal_uInt16 nKeyCode; + sal_uInt16 nModCode = 0; + char aDummy; + + if( pEvent->state & ShiftMask ) + nModCode |= KEY_SHIFT; + if( pEvent->state & ControlMask ) + nModCode |= KEY_MOD1; + if( pEvent->state & Mod1Mask ) + nModCode |= KEY_MOD2; + + if( nModCode != (KEY_SHIFT|KEY_MOD1) ) + endUnicodeSequence(); + + if( nKeySym == XK_Shift_L || nKeySym == XK_Shift_R + || nKeySym == XK_Control_L || nKeySym == XK_Control_R + || nKeySym == XK_Alt_L || nKeySym == XK_Alt_R + || nKeySym == XK_Meta_L || nKeySym == XK_Meta_R + || nKeySym == XK_Super_L || nKeySym == XK_Super_R ) + { + SalKeyModEvent aModEvt; + aModEvt.mbDown = false; // auto-accelerator feature not supported here. + aModEvt.mnModKeyCode = ModKeyFlags::NONE; + if( pEvent->type == KeyPress && mnExtKeyMod == ModKeyFlags::NONE ) + mbSendExtKeyModChange = true; + else if( pEvent->type == KeyRelease && mbSendExtKeyModChange ) + { + aModEvt.mnModKeyCode = mnExtKeyMod; + mnExtKeyMod = ModKeyFlags::NONE; + } + + // pressing just the ctrl key leads to a keysym of XK_Control but + // the event state does not contain ControlMask. In the release + // event it's the other way round: it does contain the Control mask. + // The modifier mode therefore has to be adapted manually. + ModKeyFlags nExtModMask = ModKeyFlags::NONE; + sal_uInt16 nModMask = 0; + switch( nKeySym ) + { + case XK_Control_L: + nExtModMask = ModKeyFlags::LeftMod1; + nModMask = KEY_MOD1; + break; + case XK_Control_R: + nExtModMask = ModKeyFlags::RightMod1; + nModMask = KEY_MOD1; + break; + case XK_Alt_L: + nExtModMask = ModKeyFlags::LeftMod2; + nModMask = KEY_MOD2; + break; + case XK_Alt_R: + nExtModMask = ModKeyFlags::RightMod2; + nModMask = KEY_MOD2; + break; + case XK_Shift_L: + nExtModMask = ModKeyFlags::LeftShift; + nModMask = KEY_SHIFT; + break; + case XK_Shift_R: + nExtModMask = ModKeyFlags::RightShift; + nModMask = KEY_SHIFT; + break; + // Map Meta/Super keys to MOD3 modifier on all Unix systems + // except macOS + case XK_Meta_L: + case XK_Super_L: + nExtModMask = ModKeyFlags::LeftMod3; + nModMask = KEY_MOD3; + break; + case XK_Meta_R: + case XK_Super_R: + nExtModMask = ModKeyFlags::RightMod3; + nModMask = KEY_MOD3; + break; + } + if( pEvent->type == KeyRelease ) + { + nModCode &= ~nModMask; + mnExtKeyMod &= ~nExtModMask; + } + else + { + nModCode |= nModMask; + mnExtKeyMod |= nExtModMask; + } + + aModEvt.mnCode = nModCode; + + return CallCallback( SalEvent::KeyModChange, &aModEvt ); + } + + mbSendExtKeyModChange = false; + + // try to figure out the vcl code for the keysym + // #i52338# use the unmodified KeySym if there is none for the real KeySym + // because the independent part has only keycodes for unshifted keys + nKeyCode = pDisplay_->GetKeyCode( nKeySym, &aDummy ); + if( nKeyCode == 0 ) + nKeyCode = pDisplay_->GetKeyCode( nUnmodifiedKeySym, &aDummy ); + + // try to figure out a printable if XmbLookupString returns only a keysym + // and NOT a printable. Do not store it in pPrintable[0] since it is expected to + // be in system encoding, not unicode. + // #i8988##, if KeySym and printable look equally promising then prefer KeySym + // the printable is bound to the encoding so the KeySym might contain more + // information (in et_EE locale: "Compose + Z + <" delivers "," in printable and + // (the desired) Zcaron in KeySym + sal_Unicode nKeyString = 0x0; + if ( (nLen == 0) + || ((nLen == 1) && (nKeySym > 0)) ) + nKeyString = KeysymToUnicode (nKeySym); + // if we have nothing we give up + if( !nKeyCode && !nLen && !nKeyString) + return false; + + vcl::DeletionListener aDeleteWatch( this ); + + if( nModCode == (KEY_SHIFT | KEY_MOD1) && pEvent->type == KeyPress ) + { + sal_uInt16 nSeqKeyCode = pDisplay_->GetKeyCode( nUnmodifiedKeySym, &aDummy ); + if( nSeqKeyCode == KEY_U ) + { + beginUnicodeSequence(); + return true; + } + else if( nSeqKeyCode >= KEY_0 && nSeqKeyCode <= KEY_9 ) + { + if( appendUnicodeSequence( u'0' + sal_Unicode(nSeqKeyCode - KEY_0) ) ) + return true; + } + else if( nSeqKeyCode >= KEY_A && nSeqKeyCode <= KEY_F ) + { + if( appendUnicodeSequence( u'a' + sal_Unicode(nSeqKeyCode - KEY_A) ) ) + return true; + } + else + endUnicodeSequence(); + } + + if( aDeleteWatch.isDeleted() ) + return false; + + rtl_TextEncoding nEncoding = osl_getThreadTextEncoding(); + + sal_Unicode *pBuffer; + sal_Unicode *pString; + sal_Size nBufferSize = nLen * 2; + sal_Size nSize; + pBuffer = static_cast(malloc( nBufferSize + 2 )); + pBuffer[ 0 ] = 0; + + if (nKeyString != 0) + { + pString = &nKeyString; + nSize = 1; + } + else if (nLen > 0 && nEncoding != RTL_TEXTENCODING_UNICODE) + { + // create text converter + rtl_TextToUnicodeConverter aConverter = + rtl_createTextToUnicodeConverter( nEncoding ); + rtl_TextToUnicodeContext aContext = + rtl_createTextToUnicodeContext( aConverter ); + + sal_uInt32 nConversionInfo; + sal_Size nConvertedChars; + + // convert to single byte text stream + nSize = rtl_convertTextToUnicode( + aConverter, aContext, + pPrintable, nLen, + pBuffer, nBufferSize, + RTL_TEXTTOUNICODE_FLAGS_UNDEFINED_IGNORE | + RTL_TEXTTOUNICODE_FLAGS_INVALID_IGNORE, + &nConversionInfo, &nConvertedChars ); + + // destroy converter + rtl_destroyTextToUnicodeContext( aConverter, aContext ); + rtl_destroyTextToUnicodeConverter( aConverter ); + + pString = pBuffer; + } + else if (nLen > 0 /* nEncoding == RTL_TEXTENCODING_UNICODE */) + { + pString = reinterpret_cast(pPrintable); + nSize = nLen; + } + else + { + pString = pBuffer; + nSize = 0; + } + + if ( mpInputContext != nullptr + && mpInputContext->UseContext() + && KeyRelease != pEvent->type + && ( (nSize > 1) + || (nSize > 0 && mpInputContext->IsPreeditMode())) ) + { + mpInputContext->CommitKeyEvent(pString, nSize); + } + else + // normal single character keyinput + { + aKeyEvt.mnCode = nKeyCode | nModCode; + aKeyEvt.mnRepeat = 0; + aKeyEvt.mnCharCode = pString[ 0 ]; + + if( KeyRelease == pEvent->type ) + { + CallCallback( SalEvent::KeyUp, &aKeyEvt ); + } + else + { + if ( ! CallCallback(SalEvent::KeyInput, &aKeyEvt) ) + { + // independent layer doesn't want to handle key-event, so check + // whether the keycode may have an alternate meaning + KeyAlternate aAlternate = GetAlternateKeyCode( nKeyCode ); + if ( aAlternate.nKeyCode != 0 ) + { + aKeyEvt.mnCode = aAlternate.nKeyCode | nModCode; + if( aAlternate.nCharCode ) + aKeyEvt.mnCharCode = aAlternate.nCharCode; + CallCallback(SalEvent::KeyInput, &aKeyEvt); + } + } + } + } + + // update the spot location for PreeditPosition IME style + + if (! aDeleteWatch.isDeleted()) + { + if (mpInputContext != nullptr && mpInputContext->UseContext()) + mpInputContext->UpdateSpotLocation(); + } + + free (pBuffer); + return true; +} + +bool X11SalFrame::HandleFocusEvent( XFocusChangeEvent const *pEvent ) +{ + // ReflectionX in Windows mode changes focus while mouse is grabbed + if( nVisibleFloats > 0 && GetDisplay()->getWMAdaptor()->getWindowManagerName() == "ReflectionX Windows" ) + return true; + + /* ignore focusout resulting from keyboard grabs + * we do not grab it and are not interested when + * someone else does CDE e.g. does a XGrabKey on arrow keys + * handle focus events with mode NotifyWhileGrabbed + * because with CDE alt-tab focus changing we do not get + * normal focus events + * cast focus event to the input context, otherwise the + * status window does not follow the application frame + */ + + if ( mpInputContext != nullptr ) + { + if( FocusIn == pEvent->type ) + mpInputContext->SetICFocus( this ); + } + + if ( pEvent->mode == NotifyNormal || pEvent->mode == NotifyWhileGrabbed || + ( ( nStyle_ & SalFrameStyleFlags::PLUG ) && pEvent->window == GetShellWindow() ) + ) + { + if( hPresentationWindow != None && hPresentationWindow != GetShellWindow() ) + return false; + + if( FocusIn == pEvent->type ) + { + GetSalInstance()->updatePrinterUpdate(); + mbInputFocus = True; + ImplSVData* pSVData = ImplGetSVData(); + + bool nRet = CallCallback( SalEvent::GetFocus, nullptr ); + if ((mpParent != nullptr && nStyle_ == SalFrameStyleFlags::NONE) + && pSVData->mpWinData->mpFirstFloat) + { + FloatWinPopupFlags nMode = pSVData->mpWinData->mpFirstFloat->GetPopupModeFlags(); + pSVData->mpWinData->mpFirstFloat->SetPopupModeFlags( + nMode & ~FloatWinPopupFlags::NoAppFocusClose); + } + return nRet; + } + else + { + mbInputFocus = False; + mbSendExtKeyModChange = false; + mnExtKeyMod = ModKeyFlags::NONE; + return CallCallback( SalEvent::LoseFocus, nullptr ); + } + } + + return false; +} + +bool X11SalFrame::HandleExposeEvent( XEvent const *pEvent ) +{ + XRectangle aRect = { 0, 0, 0, 0 }; + sal_uInt16 nCount = 0; + + if( pEvent->type == Expose ) + { + aRect.x = pEvent->xexpose.x; + aRect.y = pEvent->xexpose.y; + aRect.width = pEvent->xexpose.width; + aRect.height = pEvent->xexpose.height; + nCount = pEvent->xexpose.count; + } + else if( pEvent->type == GraphicsExpose ) + { + aRect.x = pEvent->xgraphicsexpose.x; + aRect.y = pEvent->xgraphicsexpose.y; + aRect.width = pEvent->xgraphicsexpose.width; + aRect.height = pEvent->xgraphicsexpose.height; + nCount = pEvent->xgraphicsexpose.count; + } + + if( IsOverrideRedirect() && mbFullScreen && + aPresentationReparentList.empty() ) + // we are in fullscreen mode -> override redirect + // focus is possibly lost, so reget it + XSetInputFocus( GetXDisplay(), GetShellWindow(), RevertToNone, CurrentTime ); + + // width and height are extents, so they are of by one for rectangle + maPaintRegion.Union( tools::Rectangle( Point(aRect.x, aRect.y), Size(aRect.width+1, aRect.height+1) ) ); + + if( nCount ) + // wait for last expose rectangle, do not wait for resize timer + // if a completed graphics expose sequence is available + return true; + + SalPaintEvent aPEvt( maPaintRegion.Left(), maPaintRegion.Top(), maPaintRegion.GetWidth(), maPaintRegion.GetHeight() ); + + CallCallback( SalEvent::Paint, &aPEvt ); + maPaintRegion = tools::Rectangle(); + + return true; +} + +void X11SalFrame::RestackChildren( ::Window* pTopLevelWindows, int nTopLevelWindows ) +{ + if( maChildren.empty() ) + return; + + int nWindow = nTopLevelWindows; + while( nWindow-- ) + if( pTopLevelWindows[nWindow] == GetStackingWindow() ) + break; + if( nWindow < 0 ) + return; + + for (auto const& child : maChildren) + { + if( child->bMapped_ ) + { + int nChild = nWindow; + while( nChild-- ) + { + if( pTopLevelWindows[nChild] == child->GetStackingWindow() ) + { + // if a child is behind its parent, place it above the + // parent (for insane WMs like Dtwm and olwm) + XWindowChanges aCfg; + aCfg.sibling = GetStackingWindow(); + aCfg.stack_mode = Above; + XConfigureWindow( GetXDisplay(), child->GetStackingWindow(), CWSibling|CWStackMode, &aCfg ); + break; + } + } + } + } + for (auto const& child : maChildren) + { + child->RestackChildren( pTopLevelWindows, nTopLevelWindows ); + } +} + +void X11SalFrame::RestackChildren() +{ + if( maChildren.empty() ) + return; + + ::Window aRoot, aParent, *pChildren = nullptr; + unsigned int nChildren; + if( XQueryTree( GetXDisplay(), + GetDisplay()->GetRootWindow( m_nXScreen ), + &aRoot, + &aParent, + &pChildren, + &nChildren ) ) + { + RestackChildren( pChildren, nChildren ); + XFree( pChildren ); + } +} + +static Bool size_event_predicate( Display*, XEvent* event, XPointer arg ) +{ + if( event->type != ConfigureNotify ) + return False; + X11SalFrame* frame = reinterpret_cast< X11SalFrame* >( arg ); + XConfigureEvent* pEvent = &event->xconfigure; + if( pEvent->window != frame->GetShellWindow() + && pEvent->window != frame->GetWindow() + && pEvent->window != frame->GetForeignParent() + && pEvent->window != frame->GetStackingWindow()) + { // ignored at top of HandleSizeEvent() + return False; + } + if( pEvent->window == frame->GetStackingWindow()) + return False; // filtered later in HandleSizeEvent() + // at this point we know that there is another similar event in the queue + frame->setPendingSizeEvent(); + return False; // but do not process the new event out of order +} + +void X11SalFrame::setPendingSizeEvent() +{ + mPendingSizeEvent = true; +} + +bool X11SalFrame::HandleSizeEvent( XConfigureEvent *pEvent ) +{ + // NOTE: if you add more tests in this function, make sure to update size_event_predicate() + // so that it finds exactly the same events + + if ( pEvent->window != GetShellWindow() + && pEvent->window != GetWindow() + && pEvent->window != GetForeignParent() + && pEvent->window != GetStackingWindow() + ) + { + // could be as well a sys-child window (aka SalObject) + return true; + } + + if( ( nStyle_ & SalFrameStyleFlags::PLUG ) && pEvent->window == GetShellWindow() ) + { + // just update the children's positions + RestackChildren(); + return true; + } + + if( pEvent->window == GetForeignParent() ) + { + XResizeWindow( GetXDisplay(), + GetWindow(), + pEvent->width, + pEvent->height ); + cairo_xlib_surface_set_size(mpSurface, pEvent->width, pEvent->height); + } + + ::Window hDummy; + XTranslateCoordinates( GetXDisplay(), + GetWindow(), + pDisplay_->GetRootWindow( pDisplay_->GetDefaultXScreen() ), + 0, 0, + &pEvent->x, &pEvent->y, + &hDummy ); + + if( pEvent->window == GetStackingWindow() ) + { + if( maGeometry.x() != pEvent->x || maGeometry.y() != pEvent->y ) + { + maGeometry.setPos({ pEvent->x, pEvent->y }); + CallCallback( SalEvent::Move, nullptr ); + } + return true; + } + + // check size hints in first time SalFrame::Show + if( X11ShowState::Unknown == nShowState_ && bMapped_ ) + nShowState_ = X11ShowState::Normal; + + // Avoid a race condition where resizing this window to one size and shortly after that + // to another size generates first size event with the old size and only after that + // with the new size, temporarily making us think the old size is valid (bnc#674806). + // So if there is another size event for this window pending, ignore this one. + mPendingSizeEvent = false; + XEvent dummy; + XCheckIfEvent( GetXDisplay(), &dummy, size_event_predicate, reinterpret_cast< XPointer >( this )); + if( mPendingSizeEvent ) + return true; + + nWidth_ = pEvent->width; + nHeight_ = pEvent->height; + + bool bMoved = ( pEvent->x != maGeometry.x() || pEvent->y != maGeometry.y() ); + bool bSized = ( pEvent->width != static_cast(maGeometry.width()) || pEvent->height != static_cast(maGeometry.height()) ); + + cairo_xlib_surface_set_size(mpSurface, pEvent->width, pEvent->height); + maGeometry.setPosSize({ pEvent->x, pEvent->y }, { pEvent->width, pEvent->height }); + updateScreenNumber(); + + // update children's position + RestackChildren(); + + if( bSized && ! bMoved ) + CallCallback( SalEvent::Resize, nullptr ); + else if( bMoved && ! bSized ) + CallCallback( SalEvent::Move, nullptr ); + else if( bMoved && bSized ) + CallCallback( SalEvent::MoveResize, nullptr ); + + return true; +} + +IMPL_LINK_NOARG(X11SalFrame, HandleAlwaysOnTopRaise, Timer *, void) +{ + if( bMapped_ ) + ToTop( SalFrameToTop::NONE ); +} + +bool X11SalFrame::HandleReparentEvent( XReparentEvent *pEvent ) +{ + Display *pDisplay = pEvent->display; + ::Window hWM_Parent; + ::Window hRoot, *Children, hDummy; + unsigned int nChildren; + + static const char* pDisableStackingCheck = getenv( "SAL_DISABLE_STACKING_CHECK" ); + + GetGenericUnixSalData()->ErrorTrapPush(); + + /* + * don't rely on the new parent from the event. + * the event may be "out of date", that is the window manager + * window may not exist anymore. This can happen if someone + * shows a frame and hides it again quickly (not that it would + * be very sensible) + */ + hWM_Parent = GetShellWindow(); + do + { + Children = nullptr; + XQueryTree( pDisplay, + hWM_Parent, + &hRoot, + &hDummy, + &Children, + &nChildren ); + + bool bError = GetGenericUnixSalData()->ErrorTrapPop( false ); + GetGenericUnixSalData()->ErrorTrapPush(); + + if( bError ) + { + hWM_Parent = GetShellWindow(); + break; + } + /* this sometimes happens if a Show(true) is + * immediately followed by Show(false) (which is braindead anyway) + */ + if( hDummy == hWM_Parent ) + hDummy = hRoot; + if( hDummy != hRoot ) + hWM_Parent = hDummy; + if( Children ) + XFree( Children ); + } while( hDummy != hRoot ); + + if( GetStackingWindow() == None + && hWM_Parent != hPresentationWindow + && hWM_Parent != GetShellWindow() + && ( ! pDisableStackingCheck || ! *pDisableStackingCheck ) + ) + { + mhStackingWindow = hWM_Parent; + XSelectInput( pDisplay, GetStackingWindow(), StructureNotifyMask ); + } + + if( hWM_Parent == pDisplay_->GetRootWindow( pDisplay_->GetDefaultXScreen() ) + || hWM_Parent == GetForeignParent() + || pEvent->parent == pDisplay_->GetRootWindow( pDisplay_->GetDefaultXScreen() ) + || ( nStyle_ & SalFrameStyleFlags::FLOAT ) ) + { + // Reparenting before Destroy + aPresentationReparentList.remove( GetStackingWindow() ); + mhStackingWindow = None; + GetGenericUnixSalData()->ErrorTrapPop(); + return false; + } + + /* + * evil hack to show decorated windows on top + * of override redirect presentation windows: + * reparent the window manager window to the presentation window + * does not work with non-reparenting WMs + * in future this should not be necessary anymore with + * _NET_WM_STATE_FULLSCREEN available + */ + if( hPresentationWindow != None + && hPresentationWindow != GetWindow() + && GetStackingWindow() != None + && GetStackingWindow() != GetDisplay()->GetRootWindow( m_nXScreen ) + ) + { + int x = 0, y = 0; + ::Window aChild; + XTranslateCoordinates( GetXDisplay(), + GetStackingWindow(), + GetDisplay()->GetRootWindow( m_nXScreen ), + 0, 0, + &x, &y, + &aChild + ); + XReparentWindow( GetXDisplay(), + GetStackingWindow(), + hPresentationWindow, + x, y + ); + aPresentationReparentList.push_back( GetStackingWindow() ); + } + + int nLeft = 0, nTop = 0; + XTranslateCoordinates( GetXDisplay(), + GetShellWindow(), + hWM_Parent, + 0, 0, + &nLeft, + &nTop, + &hDummy ); + maGeometry.setLeftDecoration(nLeft > 0 ? nLeft-1 : 0); + maGeometry.setTopDecoration(nTop > 0 ? nTop-1 : 0); + + /* + * decorations are not symmetric, + * so need real geometries here + * (this will fail with virtual roots ?) + */ + + // reset error occurred + GetGenericUnixSalData()->ErrorTrapPop(); + GetGenericUnixSalData()->ErrorTrapPush(); + + int xp, yp, x, y; + unsigned int wp, w, hp, h, bw, d; + XGetGeometry( GetXDisplay(), + GetShellWindow(), + &hRoot, + &x, &y, &w, &h, &bw, &d ); + XGetGeometry( GetXDisplay(), + hWM_Parent, + &hRoot, + &xp, &yp, &wp, &hp, &bw, &d ); + bool bResized = false; + bool bError = GetGenericUnixSalData()->ErrorTrapPop( false ); + GetGenericUnixSalData()->ErrorTrapPush(); + + if( ! bError ) + { + maGeometry.setRightDecoration(wp - w - maGeometry.leftDecoration()); + maGeometry.setBottomDecoration(hp - h - maGeometry.topDecoration()); + bResized = w != o3tl::make_unsigned(maGeometry.width()) || + h != o3tl::make_unsigned(maGeometry.height()); + /* + * note: this works because hWM_Parent is direct child of root, + * not necessarily parent of GetShellWindow() + */ + maGeometry.setPosSize({ xp + nLeft, yp + nTop }, { w, h }); + } + + // limit width and height if we are too large: #47757 + // olwm and fvwm need this, it doesn't harm the rest + + // #i81311# do this only for sizable frames + if( nStyle_ & SalFrameStyleFlags::SIZEABLE ) + { + AbsoluteScreenPixelSize aScreenSize = GetDisplay()->GetScreenSize( m_nXScreen ); + int nScreenWidth = aScreenSize.Width(); + int nScreenHeight = aScreenSize.Height(); + int nFrameWidth = maGeometry.width() + maGeometry.leftDecoration() + maGeometry.rightDecoration(); + int nFrameHeight = maGeometry.height() + maGeometry.topDecoration() + maGeometry.bottomDecoration(); + + if ((nFrameWidth > nScreenWidth) || (nFrameHeight > nScreenHeight)) + { + Size aSize(maGeometry.width(), maGeometry.height()); + + if (nFrameWidth > nScreenWidth) + aSize.setWidth( nScreenWidth - maGeometry.rightDecoration() - maGeometry.leftDecoration() ); + if (nFrameHeight > nScreenHeight) + aSize.setHeight( nScreenHeight - maGeometry.bottomDecoration() - maGeometry.topDecoration() ); + + SetSize( aSize ); + bResized = false; + } + } + if( bResized ) + CallCallback( SalEvent::Resize, nullptr ); + + GetGenericUnixSalData()->ErrorTrapPop(); + + return true; +} + +bool X11SalFrame::HandleStateEvent( XPropertyEvent const *pEvent ) +{ + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + unsigned char *prop = nullptr; + + if( 0 != XGetWindowProperty( GetXDisplay(), + GetShellWindow(), + pEvent->atom, // property + 0, // long_offset (32bit) + 2, // long_length (32bit) + False, // delete + pEvent->atom, // req_type + &actual_type, + &actual_format, + &nitems, + &bytes_after, + &prop ) + || ! prop + ) + return false; + + DBG_ASSERT( actual_type == pEvent->atom + && 32 == actual_format + && 2 == nitems + && 0 == bytes_after, "HandleStateEvent" ); + + if( *reinterpret_cast(prop) == NormalState ) + nShowState_ = X11ShowState::Normal; + else if( *reinterpret_cast(prop) == IconicState ) + nShowState_ = X11ShowState::Minimized; + + XFree( prop ); + return true; +} + +bool X11SalFrame::HandleClientMessage( XClientMessageEvent *pEvent ) +{ + const WMAdaptor& rWMAdaptor( *pDisplay_->getWMAdaptor() ); + +#if !defined(__synchronous_extinput__) + if( pEvent->message_type == rWMAdaptor.getAtom( WMAdaptor::SAL_EXTTEXTEVENT ) ) + { + HandleExtTextEvent (pEvent); + return true; + } +#endif + else if( pEvent->message_type == rWMAdaptor.getAtom( WMAdaptor::SAL_QUITEVENT ) ) + { + SAL_WARN( "vcl", "X11SalFrame::Dispatch Quit" ); + Close(); // ??? + return true; + } + else if( pEvent->message_type == rWMAdaptor.getAtom( WMAdaptor::WM_PROTOCOLS ) ) + { + if( static_cast(pEvent->data.l[0]) == rWMAdaptor.getAtom( WMAdaptor::NET_WM_PING ) ) + rWMAdaptor.answerPing( this, pEvent ); + else if( ! ( nStyle_ & SalFrameStyleFlags::PLUG ) + && ! (( nStyle_ & SalFrameStyleFlags::FLOAT ) && (nStyle_ & SalFrameStyleFlags::OWNERDRAWDECORATION)) + ) + { + if( static_cast(pEvent->data.l[0]) == rWMAdaptor.getAtom( WMAdaptor::WM_DELETE_WINDOW ) ) + { + Close(); + return true; + } + else if( static_cast(pEvent->data.l[0]) == rWMAdaptor.getAtom( WMAdaptor::WM_TAKE_FOCUS ) ) + { + // do nothing, we set the input focus in ToTop() if necessary +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.window", "got WM_TAKE_FOCUS on " + << ((nStyle_ & + SalFrameStyleFlags::OWNERDRAWDECORATION) ? + "ownerdraw" : + "NON OWNERDRAW" ) + << " window."); +#endif + } + } + } + else if( pEvent->message_type == rWMAdaptor.getAtom( WMAdaptor::XEMBED ) && + pEvent->window == GetWindow() ) + { + if( pEvent->data.l[1] == 1 || // XEMBED_WINDOW_ACTIVATE + pEvent->data.l[1] == 2 ) // XEMBED_WINDOW_DEACTIVATE + { + XFocusChangeEvent aEvent; + aEvent.type = (pEvent->data.l[1] == 1 ? FocusIn : FocusOut); + aEvent.serial = pEvent->serial; + aEvent.send_event = True; + aEvent.display = pEvent->display; + aEvent.window = pEvent->window; + aEvent.mode = NotifyNormal; + aEvent.detail = NotifyDetailNone; + HandleFocusEvent( &aEvent ); + } + } + return false; +} + +bool X11SalFrame::Dispatch( XEvent *pEvent ) +{ + bool nRet = false; + + if( -1 == nCaptured_ ) + { + CaptureMouse( true ); +#ifdef DBG_UTIL + if( -1 != nCaptured_ ) + pDisplay_->DbgPrintDisplayEvent("Captured", pEvent); +#endif + } + + if( pEvent->xany.window == GetShellWindow() || pEvent->xany.window == GetWindow() ) + { + switch( pEvent->type ) + { + case KeyPress: + nRet = HandleKeyEvent( &pEvent->xkey ); + break; + + case KeyRelease: + nRet = HandleKeyEvent( &pEvent->xkey ); + break; + + case ButtonPress: + // if we lose the focus in presentation mode + // there are good chances that we never get it back + // since the WM ignores us + if( IsOverrideRedirect() ) + { + XSetInputFocus( GetXDisplay(), GetShellWindow(), + RevertToNone, CurrentTime ); + } + [[fallthrough]]; + case ButtonRelease: + case MotionNotify: + case EnterNotify: + case LeaveNotify: + nRet = HandleMouseEvent( pEvent ); + break; + + case FocusIn: + case FocusOut: + nRet = HandleFocusEvent( &pEvent->xfocus ); + break; + + case Expose: + case GraphicsExpose: + nRet = HandleExposeEvent( pEvent ); + break; + + case MapNotify: + if( pEvent->xmap.window == GetShellWindow() ) + { + if( nShowState_ == X11ShowState::Hidden ) + { + /* + * workaround for (at least) KWin 2.2.2 + * which will map windows that were once transient + * even if they are withdrawn when the respective + * document is mapped. + */ + if( ! (nStyle_ & SalFrameStyleFlags::PLUG) ) + XUnmapWindow( GetXDisplay(), GetShellWindow() ); + break; + } + bMapped_ = true; + bViewable_ = true; + nRet = true; + if ( mpInputContext != nullptr ) + mpInputContext->Map( this ); + CallCallback( SalEvent::Resize, nullptr ); + + bool bSetFocus = m_bSetFocusOnMap; + + /* + * sometimes a message box/dialogue is brought up when a frame is not mapped + * the corresponding TRANSIENT_FOR hint is then set to the root window + * so that the dialogue shows in all cases. Correct it here if the + * frame is shown afterwards. + */ + if( ! IsChildWindow() + && ! IsOverrideRedirect() + && ! IsFloatGrabWindow() + ) + { + for (auto const& child : maChildren) + { + if( child->mbTransientForRoot ) + pDisplay_->getWMAdaptor()->changeReferenceFrame( child, this ); + } + } + + if( hPresentationWindow != None && GetShellWindow() == hPresentationWindow ) + XSetInputFocus( GetXDisplay(), GetShellWindow(), RevertToParent, CurrentTime ); + + if( bSetFocus ) + { + XSetInputFocus( GetXDisplay(), + GetShellWindow(), + RevertToParent, + CurrentTime ); + } + + RestackChildren(); + m_bSetFocusOnMap = false; + } + break; + + case UnmapNotify: + if( pEvent->xunmap.window == GetShellWindow() ) + { + bMapped_ = false; + bViewable_ = false; + nRet = true; + if ( mpInputContext != nullptr ) + mpInputContext->Unmap(); + CallCallback( SalEvent::Resize, nullptr ); + } + break; + + case ConfigureNotify: + if( pEvent->xconfigure.window == GetShellWindow() + || pEvent->xconfigure.window == GetWindow() ) + nRet = HandleSizeEvent( &pEvent->xconfigure ); + break; + + case VisibilityNotify: + nVisibility_ = pEvent->xvisibility.state; + nRet = true; + if( bAlwaysOnTop_ + && bMapped_ + && ! GetDisplay()->getWMAdaptor()->isAlwaysOnTopOK() + && nVisibility_ != VisibilityUnobscured ) + maAlwaysOnTopRaiseTimer.Start(); + break; + + case ReparentNotify: + nRet = HandleReparentEvent( &pEvent->xreparent ); + break; + + case MappingNotify: + break; + + case ColormapNotify: + nRet = false; + break; + + case PropertyNotify: + { + if( pEvent->xproperty.atom == pDisplay_->getWMAdaptor()->getAtom( WMAdaptor::WM_STATE ) ) + nRet = HandleStateEvent( &pEvent->xproperty ); + else + nRet = pDisplay_->getWMAdaptor()->handlePropertyNotify( this, &pEvent->xproperty ); + break; + } + + case ClientMessage: + nRet = HandleClientMessage( &pEvent->xclient ); + break; + } + } + else + { + switch( pEvent->type ) + { + case FocusIn: + case FocusOut: + if( ( nStyle_ & SalFrameStyleFlags::PLUG ) + && ( pEvent->xfocus.window == GetShellWindow() + || pEvent->xfocus.window == GetForeignParent() ) + ) + { + nRet = HandleFocusEvent( &pEvent->xfocus ); + } + break; + + case ConfigureNotify: + if( pEvent->xconfigure.window == GetForeignParent() || + pEvent->xconfigure.window == GetShellWindow() ) + nRet = HandleSizeEvent( &pEvent->xconfigure ); + + if( pEvent->xconfigure.window == GetStackingWindow() ) + nRet = HandleSizeEvent( &pEvent->xconfigure ); + + RestackChildren(); + break; + } + } + + return nRet; +} + +void X11SalFrame::ResetClipRegion() +{ + m_vClipRectangles.clear(); + + const int dest_kind = ShapeBounding; + const int op = ShapeSet; + const int ordering = YSorted; + + XWindowAttributes win_attrib; + XRectangle win_size; + + ::Window aShapeWindow = mhShellWindow; + + XGetWindowAttributes ( GetDisplay()->GetDisplay(), + aShapeWindow, + &win_attrib ); + + win_size.x = 0; + win_size.y = 0; + win_size.width = win_attrib.width; + win_size.height = win_attrib.height; + + XShapeCombineRectangles ( GetDisplay()->GetDisplay(), + aShapeWindow, + dest_kind, + 0, 0, // x_off, y_off + &win_size, // list of rectangles + 1, // number of rectangles + op, ordering ); +} + +void X11SalFrame::BeginSetClipRegion( sal_uInt32 /*nRects*/ ) +{ + m_vClipRectangles.clear(); +} + +void X11SalFrame::UnionClipRegion( tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight ) +{ + m_vClipRectangles.emplace_back( XRectangle { static_cast(nX), static_cast(nY), + static_cast(nWidth), static_cast(nHeight) } ); +} + +void X11SalFrame::EndSetClipRegion() +{ + const int dest_kind = ShapeBounding; + const int ordering = YSorted; + const int op = ShapeSet; + + ::Window aShapeWindow = mhShellWindow; + XShapeCombineRectangles ( GetDisplay()->GetDisplay(), + aShapeWindow, + dest_kind, + 0, 0, // x_off, y_off + m_vClipRectangles.data(), + m_vClipRectangles.size(), + op, ordering ); + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/window/salobj.cxx b/vcl/unx/generic/window/salobj.cxx new file mode 100644 index 0000000000..e2571c7911 --- /dev/null +++ b/vcl/unx/generic/window/salobj.cxx @@ -0,0 +1,506 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#if OSL_DEBUG_LEVEL > 1 +#include +#endif + +#include +#include +#include + + +#include +#include +#include + +#include +#include +#include + +#include +#include + +// SalInstance member to create and destroy a SalObject + +SalObject* X11SalInstance::CreateObject( SalFrame* pParent, SystemWindowData* pWindowData, bool bShow ) +{ + return X11SalObject::CreateObject( pParent, pWindowData, bShow ); +} + +X11SalObject* X11SalObject::CreateObject( SalFrame* pParent, SystemWindowData* pWindowData, bool bShow ) +{ + int error_base, event_base; + X11SalObject* pObject = new X11SalObject(); + SystemEnvData* pObjData = const_cast(pObject->GetSystemData()); + + if ( ! XShapeQueryExtension( static_cast(pObjData->pDisplay), + &event_base, &error_base ) ) + { + delete pObject; + return nullptr; + } + + pObject->mpParent = pParent; + + SalDisplay* pSalDisp = vcl_sal::getSalDisplay(GetGenericUnixSalData()); + const SystemEnvData* pEnv = pParent->GetSystemData(); + Display* pDisp = pSalDisp->GetDisplay(); + ::Window aObjectParent = static_cast<::Window>(pEnv->GetWindowHandle(pParent)); + pObject->maParentWin = aObjectParent; + + // find out on which screen that window is + XWindowAttributes aParentAttr; + XGetWindowAttributes( pDisp, aObjectParent, &aParentAttr ); + SalX11Screen nXScreen( XScreenNumberOfScreen( aParentAttr.screen ) ); + Visual* pVisual = (pWindowData && pWindowData->pVisual) ? + static_cast(pWindowData->pVisual) : + pSalDisp->GetVisual( nXScreen ).GetVisual(); + // get visual info + VisualID aVisID = XVisualIDFromVisual( pVisual ); + XVisualInfo aTemplate; + aTemplate.visualid = aVisID; + int nVisuals = 0; + XVisualInfo* pInfo = XGetVisualInfo( pDisp, VisualIDMask, &aTemplate, &nVisuals ); + // only one VisualInfo structure can match the visual id + SAL_WARN_IF( nVisuals != 1, "vcl", "match count for visual id is not 1" ); + unsigned int nDepth = pInfo->depth; + XFree( pInfo ); + XSetWindowAttributes aAttribs; + aAttribs.event_mask = StructureNotifyMask + | ButtonPressMask + | ButtonReleaseMask + | PointerMotionMask + | EnterWindowMask + | LeaveWindowMask + | FocusChangeMask + | ExposureMask + ; + + pObject->maPrimary = + XCreateSimpleWindow( pDisp, + aObjectParent, + 0, 0, + 1, 1, 0, + pSalDisp->GetColormap( nXScreen ).GetBlackPixel(), + pSalDisp->GetColormap( nXScreen ).GetWhitePixel() + ); + if( aVisID == pSalDisp->GetVisual( nXScreen ).GetVisualId() ) + { + pObject->maSecondary = + XCreateSimpleWindow( pDisp, + pObject->maPrimary, + 0, 0, + 1, 1, 0, + pSalDisp->GetColormap( nXScreen ).GetBlackPixel(), + pSalDisp->GetColormap( nXScreen ).GetWhitePixel() + ); + } + else + { +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.window", "visual id of vcl " + << std::hex + << static_cast + (pSalDisp->GetVisual( nXScreen ).GetVisualId()) + << ", of visual " + << static_cast + (aVisID)); +#endif + GetGenericUnixSalData()->ErrorTrapPush(); + + // create colormap for visual - there might not be one + pObject->maColormap = aAttribs.colormap = XCreateColormap( + pDisp, + pSalDisp->GetRootWindow( nXScreen ), + pVisual, + AllocNone ); + + pObject->maSecondary = + XCreateWindow( pDisp, + pSalDisp->GetRootWindow( nXScreen ), + 0, 0, + 1, 1, 0, + nDepth, InputOutput, + pVisual, + CWEventMask|CWColormap, &aAttribs ); + XSync( pDisp, False ); + if( GetGenericUnixSalData()->ErrorTrapPop( false ) ) + { + pObject->maSecondary = None; + delete pObject; + return nullptr; + } + XReparentWindow( pDisp, pObject->maSecondary, pObject->maPrimary, 0, 0 ); + } + + GetGenericUnixSalData()->ErrorTrapPush(); + if( bShow ) { + XMapWindow( pDisp, pObject->maSecondary ); + XMapWindow( pDisp, pObject->maPrimary ); + } + + pObjData->pDisplay = pDisp; + pObjData->SetWindowHandle(pObject->maSecondary); + pObjData->pWidget = nullptr; + pObjData->pVisual = pVisual; + + XSync(pDisp, False); + if( GetGenericUnixSalData()->ErrorTrapPop( false ) ) + { + delete pObject; + return nullptr; + } + + return pObject; +} + +void X11SalInstance::DestroyObject( SalObject* pObject ) +{ + delete pObject; +} + +// SalClipRegion is a member of SalObject +// definition of SalClipRegion my be found in vcl/inc/unx/salobj.h + +SalClipRegion::SalClipRegion() +{ + ClipRectangleList = nullptr; + numClipRectangles = 0; + maxClipRectangles = 0; +} + +SalClipRegion::~SalClipRegion() +{ +} + +void +SalClipRegion::BeginSetClipRegion( sal_uInt32 nRects ) +{ + ClipRectangleList.reset( new XRectangle[nRects] ); + numClipRectangles = 0; + maxClipRectangles = nRects; +} + +void +SalClipRegion::UnionClipRegion( tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight ) +{ + if ( nWidth && nHeight && (numClipRectangles < maxClipRectangles) ) + { + XRectangle& aRect = ClipRectangleList[numClipRectangles]; + + aRect.x = static_cast(nX); + aRect.y = static_cast(nY); + aRect.width = static_cast(nWidth); + aRect.height= static_cast(nHeight); + + numClipRectangles++; + } +} + +// SalObject Implementation +X11SalObject::X11SalObject() + : mpParent(nullptr) + , maParentWin(0) + , maPrimary(0) + , maSecondary(0) + , maColormap(0) + , mbVisible(false) +{ + maSystemChildData.pDisplay = vcl_sal::getSalDisplay(GetGenericUnixSalData())->GetDisplay(); + maSystemChildData.SetWindowHandle(None); + maSystemChildData.pSalFrame = nullptr; + maSystemChildData.pWidget = nullptr; + maSystemChildData.pVisual = nullptr; + maSystemChildData.aShellWindow = 0; + maSystemChildData.toolkit = SystemEnvData::Toolkit::Gen; + maSystemChildData.platform = SystemEnvData::Platform::Xcb; + + std::list< SalObject* >& rObjects = vcl_sal::getSalDisplay(GetGenericUnixSalData())->getSalObjects(); + rObjects.push_back( this ); +} + +X11SalObject::~X11SalObject() +{ + std::list< SalObject* >& rObjects = vcl_sal::getSalDisplay(GetGenericUnixSalData())->getSalObjects(); + rObjects.remove( this ); + + GetGenericUnixSalData()->ErrorTrapPush(); + ::Window aObjectParent = maParentWin; + XSetWindowBackgroundPixmap(static_cast(maSystemChildData.pDisplay), aObjectParent, None); + if ( maSecondary ) + XDestroyWindow( static_cast(maSystemChildData.pDisplay), maSecondary ); + if ( maPrimary ) + XDestroyWindow( static_cast(maSystemChildData.pDisplay), maPrimary ); + if ( maColormap ) + XFreeColormap(static_cast(maSystemChildData.pDisplay), maColormap); + XSync( static_cast(maSystemChildData.pDisplay), False ); + GetGenericUnixSalData()->ErrorTrapPop(); +} + +void +X11SalObject::ResetClipRegion() +{ + maClipRegion.ResetClipRegion(); + + const int dest_kind = ShapeBounding; + const int op = ShapeSet; + const int ordering = YSorted; + + XWindowAttributes win_attrib; + XRectangle win_size; + + ::Window aShapeWindow = maPrimary; + + XGetWindowAttributes ( static_cast(maSystemChildData.pDisplay), + aShapeWindow, + &win_attrib ); + + win_size.x = 0; + win_size.y = 0; + win_size.width = win_attrib.width; + win_size.height = win_attrib.height; + + XShapeCombineRectangles ( static_cast(maSystemChildData.pDisplay), + aShapeWindow, + dest_kind, + 0, 0, // x_off, y_off + &win_size, // list of rectangles + 1, // number of rectangles + op, ordering ); +} + +void +X11SalObject::BeginSetClipRegion( sal_uInt32 nRectCount ) +{ + maClipRegion.BeginSetClipRegion ( nRectCount ); +} + +void +X11SalObject::UnionClipRegion( tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight ) +{ + maClipRegion.UnionClipRegion ( nX, nY, nWidth, nHeight ); +} + +void +X11SalObject::EndSetClipRegion() +{ + XRectangle *pRectangles = maClipRegion.EndSetClipRegion (); + const int nRectangles = maClipRegion.GetRectangleCount(); + + ::Window aShapeWindow = maPrimary; + + XShapeCombineRectangles ( static_cast(maSystemChildData.pDisplay), + aShapeWindow, + ShapeBounding, + 0, 0, // x_off, y_off + pRectangles, + nRectangles, + ShapeSet, YSorted ); +} + +void +X11SalObject::SetPosSize( tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight ) +{ + if ( maPrimary && maSecondary && nWidth && nHeight ) + { + XMoveResizeWindow( static_cast(maSystemChildData.pDisplay), + maPrimary, + nX, nY, nWidth, nHeight ); + XMoveResizeWindow( static_cast(maSystemChildData.pDisplay), + maSecondary, + 0, 0, nWidth, nHeight ); + } +} + +void +X11SalObject::Show( bool bVisible ) +{ + if (!maSystemChildData.GetWindowHandle(mpParent)) + return; + + if ( bVisible ) { + XMapWindow( static_cast(maSystemChildData.pDisplay), + maSecondary ); + XMapWindow( static_cast(maSystemChildData.pDisplay), + maPrimary ); + } else { + XUnmapWindow( static_cast(maSystemChildData.pDisplay), + maPrimary ); + XUnmapWindow( static_cast(maSystemChildData.pDisplay), + maSecondary ); + } + mbVisible = bVisible; +} + +void X11SalObject::GrabFocus() +{ + if( mbVisible ) + XSetInputFocus( static_cast(maSystemChildData.pDisplay), + maSystemChildData.GetWindowHandle(mpParent), + RevertToNone, + CurrentTime ); +} + +const SystemEnvData* X11SalObject::GetSystemData() const +{ + return &maSystemChildData; +} + +static sal_uInt16 sal_GetCode( int state ) +{ + sal_uInt16 nCode = 0; + + if( state & Button1Mask ) + nCode |= MOUSE_LEFT; + if( state & Button2Mask ) + nCode |= MOUSE_MIDDLE; + if( state & Button3Mask ) + nCode |= MOUSE_RIGHT; + + if( state & ShiftMask ) + nCode |= KEY_SHIFT; + if( state & ControlMask ) + nCode |= KEY_MOD1; + if( state & Mod1Mask ) + nCode |= KEY_MOD2; + if( state & Mod3Mask ) + nCode |= KEY_MOD3; + + return nCode; +} + +bool X11SalObject::Dispatch( XEvent* pEvent ) +{ + std::list< SalObject* >& rObjects = vcl_sal::getSalDisplay(GetGenericUnixSalData())->getSalObjects(); + + for (auto const& elem : rObjects) + { + X11SalObject* pObject = static_cast(elem); + if( pEvent->xany.window == pObject->maPrimary || + pEvent->xany.window == pObject->maSecondary ) + { + if( pObject->IsMouseTransparent() && ( + pEvent->type == ButtonPress || + pEvent->type == ButtonRelease || + pEvent->type == EnterNotify || + pEvent->type == LeaveNotify || + pEvent->type == MotionNotify + ) + ) + { + SalMouseEvent aEvt; + int dest_x, dest_y; + ::Window aChild = None; + XTranslateCoordinates( pEvent->xbutton.display, + pEvent->xbutton.root, + pObject->maParentWin, + pEvent->xbutton.x_root, + pEvent->xbutton.y_root, + &dest_x, &dest_y, + &aChild ); + aEvt.mnX = dest_x; + aEvt.mnY = dest_y; + aEvt.mnTime = pEvent->xbutton.time; + aEvt.mnCode = sal_GetCode( pEvent->xbutton.state ); + aEvt.mnButton = 0; + SalEvent nEvent = SalEvent::NONE; + if( pEvent->type == ButtonPress || + pEvent->type == ButtonRelease ) + { + switch( pEvent->xbutton.button ) + { + case Button1: aEvt.mnButton = MOUSE_LEFT;break; + case Button2: aEvt.mnButton = MOUSE_MIDDLE;break; + case Button3: aEvt.mnButton = MOUSE_RIGHT;break; + } + nEvent = (pEvent->type == ButtonPress) ? + SalEvent::MouseButtonDown : + SalEvent::MouseButtonUp; + } + else if( pEvent->type == EnterNotify ) + nEvent = SalEvent::MouseLeave; + else + nEvent = SalEvent::MouseMove; + pObject->mpParent->CallCallback( nEvent, &aEvt ); + } + else + { + switch( pEvent->type ) + { + case UnmapNotify: + pObject->mbVisible = false; + return true; + case MapNotify: + pObject->mbVisible = true; + return true; + case ButtonPress: + pObject->CallCallback( SalObjEvent::ToTop ); + return true; + case FocusIn: + pObject->CallCallback( SalObjEvent::GetFocus ); + return true; + case FocusOut: + pObject->CallCallback( SalObjEvent::LoseFocus ); + return true; + default: break; + } + } + return false; + } + } + return false; +} + +void X11SalObject::SetLeaveEnterBackgrounds(const css::uno::Sequence& rLeaveArgs, const css::uno::Sequence& rEnterArgs) +{ + SalDisplay* pSalDisp = vcl_sal::getSalDisplay(GetGenericUnixSalData()); + Display* pDisp = pSalDisp->GetDisplay(); + ::Window aObjectParent = maParentWin; + + bool bFreePixmap = false; + Pixmap aPixmap = None; + if (rEnterArgs.getLength() == 2) + { + rEnterArgs[0] >>= bFreePixmap; + sal_Int64 pixmapHandle = None; + rEnterArgs[1] >>= pixmapHandle; + aPixmap = pixmapHandle; + } + + XSetWindowBackgroundPixmap(pDisp, aObjectParent, aPixmap); + if (bFreePixmap) + XFreePixmap(pDisp, aPixmap); + + bFreePixmap = false; + aPixmap = None; + if (rLeaveArgs.getLength() == 2) + { + rLeaveArgs[0] >>= bFreePixmap; + sal_Int64 pixmapHandle = None; + rLeaveArgs[1] >>= pixmapHandle; + aPixmap = pixmapHandle; + } + + XSetWindowBackgroundPixmap(pDisp, maSecondary, aPixmap); + if (bFreePixmap) + XFreePixmap(pDisp, aPixmap); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/generic/window/sessioninhibitor.cxx b/vcl/unx/generic/window/sessioninhibitor.cxx new file mode 100644 index 0000000000..300df9ff80 --- /dev/null +++ b/vcl/unx/generic/window/sessioninhibitor.cxx @@ -0,0 +1,370 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include + +#include + +#include +#include + +#include +#include + +#if !defined(__sun) +#include +#endif + +#include + +#if ENABLE_GIO +#include + +#define FDOSS_DBUS_SERVICE "org.freedesktop.ScreenSaver" +#define FDOSS_DBUS_PATH "/org/freedesktop/ScreenSaver" +#define FDOSS_DBUS_INTERFACE "org.freedesktop.ScreenSaver" + +#define FDOPM_DBUS_SERVICE "org.freedesktop.PowerManagement.Inhibit" +#define FDOPM_DBUS_PATH "/org/freedesktop/PowerManagement/Inhibit" +#define FDOPM_DBUS_INTERFACE "org.freedesktop.PowerManagement.Inhibit" + +#define GSM_DBUS_SERVICE "org.gnome.SessionManager" +#define GSM_DBUS_PATH "/org/gnome/SessionManager" +#define GSM_DBUS_INTERFACE "org.gnome.SessionManager" + +// Mate <= 1.10 uses org.mate.SessionManager, > 1.10 will use org.gnome.SessionManager +#define MSM_DBUS_SERVICE "org.mate.SessionManager" +#define MSM_DBUS_PATH "/org/mate/SessionManager" +#define MSM_DBUS_INTERFACE "org.mate.SessionManager" +#endif + +#include + +void SessionManagerInhibitor::inhibit(bool bInhibit, std::u16string_view sReason, ApplicationInhibitFlags eType, + unsigned int window_system_id, std::optional pDisplay, + const char* application_id) +{ + const char* appname = application_id ? application_id : SalGenericSystem::getFrameClassName(); + const OString aReason = OUStringToOString( sReason, RTL_TEXTENCODING_UTF8 ); + + if (eType == APPLICATION_INHIBIT_IDLE) + { + inhibitFDOSS( bInhibit, appname, aReason.getStr() ); + inhibitFDOPM( bInhibit, appname, aReason.getStr() ); + } + + if (eType == APPLICATION_INHIBIT_IDLE && pDisplay) + { + inhibitXScreenSaver( bInhibit, *pDisplay ); + inhibitXAutoLock( bInhibit, *pDisplay ); + inhibitDPMS( bInhibit, *pDisplay ); + } + + inhibitGSM(bInhibit, appname, aReason.getStr(), eType, window_system_id); + inhibitMSM(bInhibit, appname, aReason.getStr(), eType, window_system_id); +} + +#if ENABLE_GIO +static void dbusInhibit( bool bInhibit, + const gchar* service, const gchar* path, const gchar* interface, + const std::function& fInhibit, + const std::function& fUnInhibit, + std::optional& rCookie ) +{ + if ( ( !bInhibit && !rCookie ) || + ( bInhibit && rCookie ) ) + { + return; + } + + GError *error = nullptr; + GDBusConnection *session_connection = g_bus_get_sync( G_BUS_TYPE_SESSION, nullptr, &error ); + if (session_connection == nullptr) { + SAL_WARN( "vcl.sessioninhibitor", "failed to connect to dbus session bus" ); + + if (error != nullptr) { + SAL_WARN( "vcl.sessioninhibitor", "Error: " << error->message ); + g_error_free( error ); + } + + return; + } + + GDBusProxy *proxy = g_dbus_proxy_new_sync( session_connection, + G_DBUS_PROXY_FLAGS_NONE, + nullptr, + service, + path, + interface, + nullptr, + nullptr ); + + g_object_unref( G_OBJECT( session_connection ) ); + + if (proxy == nullptr) { + SAL_INFO( "vcl.sessioninhibitor", "could not get dbus proxy: " << service ); + return; + } + + GVariant *res = nullptr; + + if ( bInhibit ) + { + res = fInhibit( proxy, error ); + + if (res != nullptr) + { + guint nCookie; + + g_variant_get(res, "(u)", &nCookie); + g_variant_unref(res); + + rCookie = nCookie; + } + else + { + SAL_INFO( "vcl.sessioninhibitor", service << ".Inhibit failed"); + } + } + else + { + res = fUnInhibit( proxy, *rCookie, error ); + rCookie.reset(); + + if (res != nullptr) + { + g_variant_unref(res); + } + else + { + SAL_INFO( "vcl.sessioninhibitor", service << ".UnInhibit failed" ); + } + } + + if (error != nullptr) + { + SAL_INFO( "vcl.sessioninhibitor", "Error: " << error->message ); + g_error_free( error ); + } + + g_object_unref( G_OBJECT( proxy ) ); +} +#endif // ENABLE_GIO + +void SessionManagerInhibitor::inhibitFDOSS( bool bInhibit, const char* appname, const char* reason ) +{ +#if ENABLE_GIO + dbusInhibit( bInhibit, + FDOSS_DBUS_SERVICE, FDOSS_DBUS_PATH, FDOSS_DBUS_INTERFACE, + [appname, reason] ( GDBusProxy *proxy, GError*& error ) -> GVariant* { + return g_dbus_proxy_call_sync( proxy, "Inhibit", + g_variant_new("(ss)", appname, reason), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error ); + }, + [] ( GDBusProxy *proxy, const guint nCookie, GError*& error ) -> GVariant* { + return g_dbus_proxy_call_sync( proxy, "UnInhibit", + g_variant_new("(u)", nCookie), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error ); + }, + mnFDOSSCookie ); +#else + (void) this; + (void) bInhibit; + (void) appname; + (void) reason; +#endif // ENABLE_GIO +} + +void SessionManagerInhibitor::inhibitFDOPM( bool bInhibit, const char* appname, const char* reason ) +{ +#if ENABLE_GIO + dbusInhibit( bInhibit, + FDOPM_DBUS_SERVICE, FDOPM_DBUS_PATH, FDOPM_DBUS_INTERFACE, + [appname, reason] ( GDBusProxy *proxy, GError*& error ) -> GVariant* { + return g_dbus_proxy_call_sync( proxy, "Inhibit", + g_variant_new("(ss)", appname, reason), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error ); + }, + [] ( GDBusProxy *proxy, const guint nCookie, GError*& error ) -> GVariant* { + return g_dbus_proxy_call_sync( proxy, "UnInhibit", + g_variant_new("(u)", nCookie), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error ); + }, + mnFDOPMCookie ); +#else + (void) this; + (void) bInhibit; + (void) appname; + (void) reason; +#endif // ENABLE_GIO +} + +void SessionManagerInhibitor::inhibitGSM( bool bInhibit, const char* appname, const char* reason, ApplicationInhibitFlags eType, unsigned int window_system_id ) +{ +#if ENABLE_GIO + dbusInhibit( bInhibit, + GSM_DBUS_SERVICE, GSM_DBUS_PATH, GSM_DBUS_INTERFACE, + [appname, reason, eType, window_system_id] ( GDBusProxy *proxy, GError*& error ) -> GVariant* { + return g_dbus_proxy_call_sync( proxy, "Inhibit", + g_variant_new("(susu)", + appname, + window_system_id, + reason, + eType + ), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error ); + }, + [] ( GDBusProxy *proxy, const guint nCookie, GError*& error ) -> GVariant* { + return g_dbus_proxy_call_sync( proxy, "Uninhibit", + g_variant_new("(u)", nCookie), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error ); + }, + mnGSMCookie ); +#else + (void) this; + (void) bInhibit; + (void) appname; + (void) reason; + (void) eType; + (void) window_system_id; +#endif // ENABLE_GIO +} + +void SessionManagerInhibitor::inhibitMSM( bool bInhibit, const char* appname, const char* reason, ApplicationInhibitFlags eType, unsigned int window_system_id ) +{ +#if ENABLE_GIO + dbusInhibit( bInhibit, + MSM_DBUS_SERVICE, MSM_DBUS_PATH, MSM_DBUS_INTERFACE, + [appname, reason, eType, window_system_id] ( GDBusProxy *proxy, GError*& error ) -> GVariant* { + return g_dbus_proxy_call_sync( proxy, "Inhibit", + g_variant_new("(susu)", + appname, + window_system_id, + reason, + eType + ), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error ); + }, + [] ( GDBusProxy *proxy, const guint nCookie, GError*& error ) -> GVariant* { + return g_dbus_proxy_call_sync( proxy, "Uninhibit", + g_variant_new("(u)", nCookie), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error ); + }, + mnMSMCookie ); +#else + (void) this; + (void) bInhibit; + (void) appname; + (void) reason; + (void) eType; + (void) window_system_id; +#endif // ENABLE_GIO +} + +/** + * Disable screensavers using the XSetScreenSaver/XGetScreenSaver API. + * + * Worth noting: xscreensaver explicitly ignores this and does its own + * timeout handling. + */ +void SessionManagerInhibitor::inhibitXScreenSaver( bool bInhibit, Display* pDisplay ) +{ + int nTimeout, nInterval, bPreferBlanking, bAllowExposures; + XGetScreenSaver( pDisplay, &nTimeout, &nInterval, + &bPreferBlanking, &bAllowExposures ); + + // To disable/reenable we simply fiddle the timeout, whilst + // retaining all other properties. + if ( bInhibit && nTimeout) + { + mnXScreenSaverTimeout = nTimeout; + XResetScreenSaver( pDisplay ); + XSetScreenSaver( pDisplay, 0, nInterval, + bPreferBlanking, bAllowExposures ); + } + else if ( !bInhibit && mnXScreenSaverTimeout ) + { + XSetScreenSaver( pDisplay, *mnXScreenSaverTimeout, + nInterval, bPreferBlanking, + bAllowExposures ); + mnXScreenSaverTimeout.reset(); + } +} + + +/* definitions from xautolock.c (pl15) */ +#define XAUTOLOCK_DISABLE 1 +#define XAUTOLOCK_ENABLE 2 + +void SessionManagerInhibitor::inhibitXAutoLock( bool bInhibit, Display* pDisplay ) +{ + ::Window aRootWindow = RootWindowOfScreen( ScreenOfDisplay( pDisplay, 0 ) ); + + Atom nAtom = XInternAtom( pDisplay, + "XAUTOLOCK_MESSAGE", + False ); + + if ( nAtom == None ) + { + return; + } + + int nMessage = bInhibit ? XAUTOLOCK_DISABLE : XAUTOLOCK_ENABLE; + + XChangeProperty( pDisplay, + aRootWindow, + nAtom, + XA_INTEGER, + 8, // format -- 8 bit quantity + PropModeReplace, + reinterpret_cast( &nMessage ), + sizeof( nMessage ) ); +} + +void SessionManagerInhibitor::inhibitDPMS( bool bInhibit, Display* pDisplay ) +{ +#if !defined(__sun) + int dummy; + // This won't change while X11 is running, hence + // we can evaluate only once and store as static + static bool bDPMSExtensionAvailable = ( DPMSQueryExtension( pDisplay, &dummy, &dummy) != 0 ); + + if ( !bDPMSExtensionAvailable ) + { + return; + } + + if ( bInhibit ) + { + CARD16 state; // unused by us + DPMSInfo( pDisplay, &state, &mbDPMSWasEnabled ); + + if ( mbDPMSWasEnabled ) + { + DPMSGetTimeouts( pDisplay, + &mnDPMSStandbyTimeout, + &mnDPMSSuspendTimeout, + &mnDPMSOffTimeout ); + DPMSSetTimeouts( pDisplay, + 0, + 0, + 0 ); + } + } + else if ( !bInhibit && mbDPMSWasEnabled ) + { + DPMSSetTimeouts( pDisplay, + mnDPMSStandbyTimeout, + mnDPMSSuspendTimeout, + mnDPMSOffTimeout ); + } +#endif // !defined(__sun) +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/TODO b/vcl/unx/gtk3/a11y/TODO new file mode 100644 index 0000000000..1048bd96ef --- /dev/null +++ b/vcl/unx/gtk3/a11y/TODO @@ -0,0 +1,49 @@ +cws 'atkbridge' +#Issue number: i#47890# +Submitted by: mmeeks + +Hacked up prototype of atk bridge + + +Serious problems + + Threading/locking: + + incoming CORBA calls & the GDK lock + + how are these being processed & on what thread ? + + are we holding the GDK_THREADS lock ? + + can we even do that ? + + is it really necessary to be thread safe ? + + how does this work in combination with the (unsafe) GAIL code ? + + what should incoming CORBA calls be doing ? + + esp. since we can't tell if they're coming from + in-proc or not either [ though this is unlikely ] + + +Test: + + in-line text editing, does the TEXT_CHANGED signal get it right, + + why not copy/paste/delete etc. ? + + check vs. writer & other bits ... + + AtkSelection + + AtkHyper* + +* At-poke + + implement non-gui mode - for to-console event logging + + logging + + more detail from remaining events + + add a Tree navigation thing instead (?) + + poke a sub-child (?) + + embed a tree widget inside the tree view ? + + AtkHyperText testing (?) + + +Known bugs: + + AtkText + + selection interface - multiple selections ? + + word boundary issues + + copy AccessibleTextImpl.java's getAfterIndex eg. + + the 'getFoo' methods need to use UNO_QUERY_THROW & + throw an exception to avoid null pointer dereferences. + + AtkAttributeSet (etc.) + + AtkEditableText + + finish/test AtkTable + + HyperLink 'link_activated', HyperText 'link_selected' (?) + + tooltips create new toplevels with broken roles. diff --git a/vcl/unx/gtk3/a11y/atkaction.cxx b/vcl/unx/gtk3/a11y/atkaction.cxx new file mode 100644 index 0000000000..0a39d3bdf9 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkaction.cxx @@ -0,0 +1,269 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkwrapper.hxx" + +#include +#include + +#include +#include + +#include +#include +#include + +using namespace ::com::sun::star; + +// FIXME +static const gchar * +getAsConst( const OString& rString ) +{ + static const int nMax = 10; + static OString aUgly[nMax]; + static int nIdx = 0; + nIdx = (nIdx + 1) % nMax; + aUgly[nIdx] = rString; + return aUgly[ nIdx ].getStr(); +} + +/// @throws uno::RuntimeException +static css::uno::Reference + getAction( AtkAction *action ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( action ); + + if( pWrap ) + { + if( !pWrap->mpAction.is() ) + { + pWrap->mpAction.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpAction; + } + + return css::uno::Reference(); +} + +extern "C" { + +static gboolean +action_wrapper_do_action (AtkAction *action, + gint i) +{ + try { + css::uno::Reference pAction + = getAction( action ); + if( pAction.is() ) + return pAction->doAccessibleAction( i ); + } + catch(const uno::Exception&) { + g_warning( "Exception in doAccessibleAction()" ); + } + + return FALSE; +} + +static gint +action_wrapper_get_n_actions (AtkAction *action) +{ + try { + css::uno::Reference pAction + = getAction( action ); + if( pAction.is() ) + return pAction->getAccessibleActionCount(); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleActionCount()" ); + } + + return 0; +} + +static const gchar * +action_wrapper_get_description (AtkAction *, gint) +{ + // GAIL implement this only for cells + g_warning( "Not implemented: get_description()" ); + return ""; +} + +static const gchar * +action_wrapper_get_localized_name (AtkAction *, gint) +{ + // GAIL doesn't implement this as well + g_warning( "Not implemented: get_localized_name()" ); + return ""; +} + +static const gchar * +action_wrapper_get_name (AtkAction *action, + gint i) +{ + static std::map< OUString, const gchar * > aNameMap { + { "click", "click" }, + { "select", "click" } , + { "togglePopup", "push" } + }; + + try { + css::uno::Reference pAction + = getAction( action ); + if( pAction.is() ) + { + std::map< OUString, const gchar * >::iterator iter; + + OUString aDesc( pAction->getAccessibleActionDescription( i ) ); + + iter = aNameMap.find( aDesc ); + if( iter != aNameMap.end() ) + return iter->second; + + std::pair< const OUString, const gchar * > aNewVal( aDesc, + g_strdup( OUStringToConstGChar(aDesc) ) ); + + if( aNameMap.insert( aNewVal ).second ) + return aNewVal.second; + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleActionDescription()" ); + } + + return ""; +} + +/* +* GNOME Expects a string in the format: +* +* ;; +* +* The keybindings in should be separated by ":" +*/ + +static void +appendKeyStrokes(OStringBuffer& rBuffer, const uno::Sequence< awt::KeyStroke >& rKeyStrokes) +{ + for( const auto& rKeyStroke : rKeyStrokes ) + { + if( rKeyStroke.Modifiers & awt::KeyModifier::SHIFT ) + rBuffer.append(""); + if( rKeyStroke.Modifiers & awt::KeyModifier::MOD1 ) + rBuffer.append(""); + if( rKeyStroke.Modifiers & awt::KeyModifier::MOD2 ) + rBuffer.append(""); + + if( ( rKeyStroke.KeyCode >= awt::Key::A ) && ( rKeyStroke.KeyCode <= awt::Key::Z ) ) + rBuffer.append( static_cast( 'a' + ( rKeyStroke.KeyCode - awt::Key::A ) ) ); + else + { + char c = '\0'; + + switch( rKeyStroke.KeyCode ) + { + case awt::Key::TAB: c = '\t'; break; + case awt::Key::SPACE: c = ' '; break; + case awt::Key::ADD: c = '+'; break; + case awt::Key::SUBTRACT: c = '-'; break; + case awt::Key::MULTIPLY: c = '*'; break; + case awt::Key::DIVIDE: c = '/'; break; + case awt::Key::POINT: c = '.'; break; + case awt::Key::COMMA: c = ','; break; + case awt::Key::LESS: c = '<'; break; + case awt::Key::GREATER: c = '>'; break; + case awt::Key::EQUAL: c = '='; break; + case 0: + break; + default: + g_warning( "Unmapped KeyCode: %d", rKeyStroke.KeyCode ); + break; + } + + if( c != '\0' ) + rBuffer.append( c ); + else + { + // The KeyCode approach did not work, probably a non ascii character + // let's hope that there is a character given in KeyChar. + rBuffer.append(OUStringToOString(OUStringChar(rKeyStroke.KeyChar), RTL_TEXTENCODING_UTF8)); + } + } + } +} + +static const gchar * +action_wrapper_get_keybinding (AtkAction *action, + gint i) +{ + try { + css::uno::Reference pAction + = getAction( action ); + if( pAction.is() ) + { + uno::Reference< accessibility::XAccessibleKeyBinding > xBinding( pAction->getAccessibleActionKeyBinding( i )); + + if( xBinding.is() ) + { + OStringBuffer aRet; + + sal_Int32 nmax = std::min( xBinding->getAccessibleKeyBindingCount(), sal_Int32(3) ); + for( sal_Int32 n = 0; n < nmax; n++ ) + { + appendKeyStrokes( aRet, xBinding->getAccessibleKeyBinding( n ) ); + + if( n < 2 ) + aRet.append( ';' ); + } + + // !! FIXME !! remember keystroke in wrapper object ? + return getAsConst( aRet.makeStringAndClear() ); + } + } + } + catch(const uno::Exception&) { + g_warning( "Exception in get_keybinding()" ); + } + + return ""; +} + +static gboolean +action_wrapper_set_description (AtkAction *, gint, const gchar *) +{ + return FALSE; +} + +} // extern "C" + +void +actionIfaceInit (AtkActionIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->do_action = action_wrapper_do_action; + iface->get_n_actions = action_wrapper_get_n_actions; + iface->get_description = action_wrapper_get_description; + iface->get_keybinding = action_wrapper_get_keybinding; + iface->get_name = action_wrapper_get_name; + iface->get_localized_name = action_wrapper_get_localized_name; + iface->set_description = action_wrapper_set_description; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkbridge.cxx b/vcl/unx/gtk3/a11y/atkbridge.cxx new file mode 100644 index 0000000000..c7cb32ca3c --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkbridge.cxx @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include "atkutil.hxx" + +bool InitAtkBridge() +{ + ooo_atk_util_ensure_event_listener(); + return true; +} + +void DeInitAtkBridge() {} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkcomponent.cxx b/vcl/unx/gtk3/a11y/atkcomponent.cxx new file mode 100644 index 0000000000..154f0117f1 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkcomponent.cxx @@ -0,0 +1,431 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkwrapper.hxx" +#include +#include +#include + +using namespace ::com::sun::star; + +static AtkObjectWrapper* getObjectWrapper(AtkComponent *pComponent) +{ + AtkObjectWrapper *pWrap = nullptr; + if (ATK_IS_OBJECT_WRAPPER(pComponent)) + pWrap = ATK_OBJECT_WRAPPER(pComponent); + else if (GTK_IS_DRAWING_AREA(pComponent)) //when using a GtkDrawingArea as a custom widget in welded gtk3 + { + GtkWidget* pDrawingArea = GTK_WIDGET(pComponent); + AtkObject* pAtkObject = gtk_widget_get_accessible(pDrawingArea); + pWrap = ATK_IS_OBJECT_WRAPPER(pAtkObject) ? ATK_OBJECT_WRAPPER(pAtkObject) : nullptr; + } + return pWrap; +} + +/// @throws uno::RuntimeException +static css::uno::Reference + getComponent(AtkObjectWrapper *pWrap) +{ + if (pWrap) + { + if (!pWrap->mpComponent.is()) + pWrap->mpComponent.set(pWrap->mpContext, css::uno::UNO_QUERY); + return pWrap->mpComponent; + } + + return css::uno::Reference(); +} + +static awt::Point +lcl_getLocationInWindow(AtkComponent* pAtkComponent, + css::uno::Reference const& xComponent) +{ + // calculate position in window by adding the component's position in the parent + // to the parent's position in the window (unless parent is a window itself) + awt::Point aPos = xComponent->getLocation(); + AtkObject* pParent = atk_object_get_parent(ATK_OBJECT(pAtkComponent)); + if (ATK_IS_COMPONENT(pParent) && pParent->role != AtkRole::ATK_ROLE_DIALOG + && pParent->role != AtkRole::ATK_ROLE_FILE_CHOOSER + && pParent->role != AtkRole::ATK_ROLE_FRAME + && pParent->role != AtkRole::ATK_ROLE_WINDOW) + { + int nX; + int nY; + atk_component_get_extents(ATK_COMPONENT(pParent), &nX, &nY, nullptr, nullptr, ATK_XY_WINDOW); + aPos.X += nX; + aPos.Y += nY; + } + + return aPos; +} + +/*****************************************************************************/ + +static awt::Point +translatePoint( AtkComponent* pAtkComponent, + css::uno::Reference const & pComponent, + gint x, gint y, AtkCoordType t) +{ + awt::Point aOrigin( 0, 0 ); + if( t == ATK_XY_SCREEN ) + aOrigin = pComponent->getLocationOnScreen(); + else if (t == ATK_XY_WINDOW) + aOrigin = lcl_getLocationInWindow(pAtkComponent, pComponent); + return awt::Point( x - aOrigin.X, y - aOrigin.Y ); +} + +/*****************************************************************************/ + +extern "C" { + +static gboolean +component_wrapper_grab_focus (AtkComponent *component) +{ + AtkObjectWrapper* obj = getObjectWrapper(component); + //if we're a native GtkDrawingArea with custom a11y, use the default toolkit a11y + if (obj && obj->mpOrig) + return atk_component_grab_focus(ATK_COMPONENT(obj->mpOrig)); + + try + { + css::uno::Reference pComponent + = getComponent(obj); + if( pComponent.is() ) + { + pComponent->grabFocus(); + return true; + } + } + catch( const uno::Exception & ) + { + g_warning( "Exception in grabFocus()" ); + } + + return FALSE; +} + +/*****************************************************************************/ + +static gboolean +component_wrapper_contains (AtkComponent *component, + gint x, + gint y, + AtkCoordType coord_type) +{ + AtkObjectWrapper* obj = getObjectWrapper(component); + //if we're a native GtkDrawingArea with custom a11y, use the default toolkit a11y + if (obj && obj->mpOrig) + return atk_component_contains(ATK_COMPONENT(obj->mpOrig), x, y, coord_type); + + try + { + css::uno::Reference pComponent + = getComponent(obj); + if( pComponent.is() ) + return pComponent->containsPoint( + translatePoint(component, pComponent, x, y, coord_type)); + } + catch( const uno::Exception & ) + { + g_warning( "Exception in containsPoint()" ); + } + + return FALSE; +} + +/*****************************************************************************/ + +static AtkObject * +component_wrapper_ref_accessible_at_point (AtkComponent *component, + gint x, + gint y, + AtkCoordType coord_type) +{ + AtkObjectWrapper* obj = getObjectWrapper(component); + //if we're a native GtkDrawingArea with custom a11y, use the default toolkit a11y + if (obj && obj->mpOrig) + return atk_component_ref_accessible_at_point(ATK_COMPONENT(obj->mpOrig), x, y, coord_type); + + try + { + css::uno::Reference pComponent + = getComponent(obj); + + if( pComponent.is() ) + { + uno::Reference< accessibility::XAccessible > xAccessible = pComponent->getAccessibleAtPoint( + translatePoint(component, pComponent, x, y, coord_type)); + return atk_object_wrapper_ref( xAccessible ); + } + } + catch( const uno::Exception & ) + { + g_warning( "Exception in getAccessibleAtPoint()" ); + } + + return nullptr; +} + +/*****************************************************************************/ + +static void +component_wrapper_get_position (AtkComponent *component, + gint *x, + gint *y, + AtkCoordType coord_type) +{ + AtkObjectWrapper* obj = getObjectWrapper(component); + //if we're a native GtkDrawingArea with custom a11y, use the default toolkit a11y + if (obj && obj->mpOrig) + { + atk_component_get_extents(ATK_COMPONENT(obj->mpOrig), x, y, nullptr, nullptr, coord_type); + return; + } + + *x = *y = -1; + + try + { + css::uno::Reference pComponent + = getComponent(obj); + if( pComponent.is() ) + { + awt::Point aPos; + + if( coord_type == ATK_XY_SCREEN ) + aPos = pComponent->getLocationOnScreen(); + else if (coord_type == ATK_XY_WINDOW) + aPos = lcl_getLocationInWindow(component, pComponent); +#if ATK_CHECK_VERSION(2, 30, 0) + else if (coord_type == ATK_XY_PARENT) +#else + // ATK_XY_PARENT added in ATK 2.30, so can't use the constant here + else +#endif + aPos = pComponent->getLocation(); +#if ATK_CHECK_VERSION(2, 30, 0) + else + { + SAL_WARN("vcl.gtk", + "component_wrapper_get_position called with unknown AtkCoordType " + << coord_type); + return; + } +#endif + + *x = aPos.X; + *y = aPos.Y; + } + } + catch( const uno::Exception & ) + { + g_warning( "Exception in getLocation[OnScreen]()" ); + } +} + +/*****************************************************************************/ + +static void +component_wrapper_get_size (AtkComponent *component, + gint *width, + gint *height) +{ + AtkObjectWrapper* obj = getObjectWrapper(component); + //if we're a native GtkDrawingArea with custom a11y, use the default toolkit a11y + if (obj && obj->mpOrig) + { + atk_component_get_extents(ATK_COMPONENT(obj->mpOrig), nullptr, nullptr, width, height, ATK_XY_WINDOW); + return; + } + + *width = *height = -1; + + try + { + css::uno::Reference pComponent + = getComponent(obj); + if( pComponent.is() ) + { + awt::Size aSize = pComponent->getSize(); + *width = aSize.Width; + *height = aSize.Height; + } + } + catch( const uno::Exception & ) + { + g_warning( "Exception in getSize()" ); + } +} + +/*****************************************************************************/ + +static void +component_wrapper_get_extents (AtkComponent *component, + gint *x, + gint *y, + gint *width, + gint *height, + AtkCoordType coord_type) +{ + component_wrapper_get_position( component, x, y, coord_type ); + component_wrapper_get_size( component, width, height ); +} + +/*****************************************************************************/ + +static gboolean +component_wrapper_set_extents (AtkComponent *, gint, gint, gint, gint, AtkCoordType) +{ + g_warning( "AtkComponent::set_extents unimplementable" ); + return FALSE; +} + +/*****************************************************************************/ + +static gboolean +component_wrapper_set_position (AtkComponent *, gint, gint, AtkCoordType) +{ + g_warning( "AtkComponent::set_position unimplementable" ); + return FALSE; +} + +/*****************************************************************************/ + +static gboolean +component_wrapper_set_size (AtkComponent *, gint, gint) +{ + g_warning( "AtkComponent::set_size unimplementable" ); + return FALSE; +} + +/*****************************************************************************/ + +static AtkLayer +component_wrapper_get_layer (AtkComponent *component) +{ + AtkRole role = atk_object_get_role( ATK_OBJECT( component ) ); + AtkLayer layer = ATK_LAYER_WIDGET; + + switch (role) + { + case ATK_ROLE_POPUP_MENU: + case ATK_ROLE_MENU_ITEM: + case ATK_ROLE_CHECK_MENU_ITEM: + case ATK_ROLE_SEPARATOR: + case ATK_ROLE_LIST_ITEM: + layer = ATK_LAYER_POPUP; + break; + case ATK_ROLE_MENU: + { + AtkObject * parent = atk_object_get_parent( ATK_OBJECT( component ) ); + if( atk_object_get_role( parent ) != ATK_ROLE_MENU_BAR ) + layer = ATK_LAYER_POPUP; + } + break; + + case ATK_ROLE_LIST: + { + AtkObject * parent = atk_object_get_parent( ATK_OBJECT( component ) ); + if( atk_object_get_role( parent ) == ATK_ROLE_COMBO_BOX ) + layer = ATK_LAYER_POPUP; + } + break; + + default: + ; + } + + return layer; +} + +/*****************************************************************************/ + +static gint +component_wrapper_get_mdi_zorder (AtkComponent *) +{ + // only needed for ATK_LAYER_MDI (not used) or ATK_LAYER_WINDOW (inherited from GAIL) + return G_MININT; +} + +/*****************************************************************************/ + +// This code is mostly stolen from libgail .. + +static guint +component_wrapper_add_focus_handler (AtkComponent *component, + AtkFocusHandler handler) +{ + GSignalMatchType match_type; + gulong ret; + guint signal_id; + + match_type = GSignalMatchType(G_SIGNAL_MATCH_ID | G_SIGNAL_MATCH_FUNC); + signal_id = g_signal_lookup( "focus-event", ATK_TYPE_OBJECT ); + + ret = g_signal_handler_find( component, match_type, signal_id, 0, nullptr, + static_cast(&handler), nullptr); + if (!ret) + { + return g_signal_connect_closure_by_id (component, + signal_id, 0, + g_cclosure_new ( + G_CALLBACK (handler), nullptr, + nullptr), + FALSE); + } + else + { + return 0; + } +} + +/*****************************************************************************/ + +static void +component_wrapper_remove_focus_handler (AtkComponent *component, + guint handler_id) +{ + g_signal_handler_disconnect (component, handler_id); +} + +/*****************************************************************************/ + +} // extern "C" + +void +componentIfaceInit (AtkComponentIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->add_focus_handler = component_wrapper_add_focus_handler; + iface->contains = component_wrapper_contains; + iface->get_extents = component_wrapper_get_extents; + iface->get_layer = component_wrapper_get_layer; + iface->get_mdi_zorder = component_wrapper_get_mdi_zorder; + iface->get_position = component_wrapper_get_position; + iface->get_size = component_wrapper_get_size; + iface->grab_focus = component_wrapper_grab_focus; + iface->ref_accessible_at_point = component_wrapper_ref_accessible_at_point; + iface->remove_focus_handler = component_wrapper_remove_focus_handler; + iface->set_extents = component_wrapper_set_extents; + iface->set_position = component_wrapper_set_position; + iface->set_size = component_wrapper_set_size; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkeditabletext.cxx b/vcl/unx/gtk3/a11y/atkeditabletext.cxx new file mode 100644 index 0000000000..f240c32324 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkeditabletext.cxx @@ -0,0 +1,193 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkwrapper.hxx" +#include "atktextattributes.hxx" + +#include + +#include + +using namespace ::com::sun::star; + +/// @throws uno::RuntimeException +static css::uno::Reference + getEditableText( AtkEditableText *pEditableText ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pEditableText ); + if( pWrap ) + { + if( !pWrap->mpEditableText.is() ) + { + pWrap->mpEditableText.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpEditableText; + } + + return css::uno::Reference(); +} + +/*****************************************************************************/ + +extern "C" { + +static gboolean +editable_text_wrapper_set_run_attributes( AtkEditableText *text, + AtkAttributeSet *attribute_set, + gint nStartOffset, + gint nEndOffset) +{ + try { + css::uno::Reference + pEditableText = getEditableText( text ); + if( pEditableText.is() ) + { + uno::Sequence< beans::PropertyValue > aAttributeList; + + if( attribute_set_map_to_property_values( attribute_set, aAttributeList ) ) + return pEditableText->setAttributes(nStartOffset, nEndOffset, aAttributeList); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in setAttributes()" ); + } + + return FALSE; +} + +static void +editable_text_wrapper_set_text_contents( AtkEditableText *text, + const gchar *string ) +{ + try { + css::uno::Reference + pEditableText = getEditableText( text ); + if( pEditableText.is() ) + { + OUString aString ( string, strlen(string), RTL_TEXTENCODING_UTF8 ); + pEditableText->setText( aString ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in setText()" ); + } +} + +static void +editable_text_wrapper_insert_text( AtkEditableText *text, + const gchar *string, + gint length, + gint *pos ) +{ + try { + css::uno::Reference + pEditableText = getEditableText( text ); + if( pEditableText.is() ) + { + OUString aString ( string, length, RTL_TEXTENCODING_UTF8 ); + if( pEditableText->insertText( aString, *pos ) ) + *pos += length; + } + } + catch(const uno::Exception&) { + g_warning( "Exception in insertText()" ); + } +} + +static void +editable_text_wrapper_cut_text( AtkEditableText *text, + gint start, + gint end ) +{ + try { + css::uno::Reference + pEditableText = getEditableText( text ); + if( pEditableText.is() ) + pEditableText->cutText( start, end ); + } + catch(const uno::Exception&) { + g_warning( "Exception in cutText()" ); + } +} + +static void +editable_text_wrapper_delete_text( AtkEditableText *text, + gint start, + gint end ) +{ + try { + css::uno::Reference + pEditableText = getEditableText( text ); + if( pEditableText.is() ) + pEditableText->deleteText( start, end ); + } + catch(const uno::Exception&) { + g_warning( "Exception in deleteText()" ); + } +} + +static void +editable_text_wrapper_paste_text( AtkEditableText *text, + gint pos ) +{ + try { + css::uno::Reference + pEditableText = getEditableText( text ); + if( pEditableText.is() ) + pEditableText->pasteText( pos ); + } + catch(const uno::Exception&) { + g_warning( "Exception in pasteText()" ); + } +} + +static void +editable_text_wrapper_copy_text( AtkEditableText *text, + gint start, + gint end ) +{ + try { + css::uno::Reference + pEditableText = getEditableText( text ); + if( pEditableText.is() ) + pEditableText->copyText( start, end ); + } + catch(const uno::Exception&) { + g_warning( "Exception in copyText()" ); + } +} + +} // extern "C" + +void +editableTextIfaceInit (AtkEditableTextIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->set_text_contents = editable_text_wrapper_set_text_contents; + iface->insert_text = editable_text_wrapper_insert_text; + iface->copy_text = editable_text_wrapper_copy_text; + iface->cut_text = editable_text_wrapper_cut_text; + iface->delete_text = editable_text_wrapper_delete_text; + iface->paste_text = editable_text_wrapper_paste_text; + iface->set_run_attributes = editable_text_wrapper_set_run_attributes; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkfactory.cxx b/vcl/unx/gtk3/a11y/atkfactory.cxx new file mode 100644 index 0000000000..2fc407b7bc --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkfactory.cxx @@ -0,0 +1,186 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include "atkwrapper.hxx" +#include "atkfactory.hxx" +#include "atkregistry.hxx" + +using namespace ::com::sun::star; + +extern "C" { + +/* + * Instances of this dummy object class are returned whenever we have to + * create an AtkObject, but can't touch the OOo object anymore since it + * is already disposed. + */ + +static AtkStateSet * +noop_wrapper_ref_state_set( AtkObject * ) +{ + AtkStateSet *state_set = atk_state_set_new(); + atk_state_set_add_state( state_set, ATK_STATE_DEFUNCT ); + return state_set; +} + +static void +atk_noop_object_wrapper_class_init(AtkNoOpObjectClass *klass) +{ + AtkObjectClass *atk_class = ATK_OBJECT_CLASS( klass ); + atk_class->ref_state_set = noop_wrapper_ref_state_set; +} + +static GType +atk_noop_object_wrapper_get_type() +{ + static GType type = 0; + + if (!type) + { + static const GTypeInfo typeInfo = + { + sizeof (AtkNoOpObjectClass), + nullptr, + nullptr, + reinterpret_cast(atk_noop_object_wrapper_class_init), + nullptr, + nullptr, + sizeof (AtkObjectWrapper), + 0, + nullptr, + nullptr + } ; + + type = g_type_register_static (ATK_TYPE_OBJECT, "OOoAtkNoOpObj", &typeInfo, GTypeFlags(0)) ; + } + return type; +} + +static AtkObject* +atk_noop_object_wrapper_new() +{ + AtkObject *accessible; + + accessible = static_cast(g_object_new (atk_noop_object_wrapper_get_type(), nullptr)); + g_return_val_if_fail (accessible != nullptr, nullptr); + + accessible->role = ATK_ROLE_INVALID; + accessible->layer = ATK_LAYER_INVALID; + + return accessible; +} + +/* + * The wrapper factory + */ + +static GType +wrapper_factory_get_accessible_type() +{ + return atk_object_wrapper_get_type(); +} + +static AtkObject* +wrapper_factory_create_accessible( GObject *obj ) +{ + GtkWidget* pEventBox = gtk_widget_get_parent(GTK_WIDGET(obj)); + + // gail_container_real_remove_gtk tries to re-instantiate an accessible + // for a widget that is about to vanish .. + if (!pEventBox) + return atk_noop_object_wrapper_new(); + + GtkWidget* pTopLevelGrid = gtk_widget_get_parent(pEventBox); + if (!pTopLevelGrid) + return atk_noop_object_wrapper_new(); + + GtkWidget* pTopLevel = gtk_widget_get_parent(pTopLevelGrid); + if (!pTopLevel) + return atk_noop_object_wrapper_new(); + + GtkSalFrame* pFrame = GtkSalFrame::getFromWindow(pTopLevel); + g_return_val_if_fail(pFrame != nullptr, atk_noop_object_wrapper_new()); + + vcl::Window* pFrameWindow = pFrame->GetWindow(); + if( pFrameWindow ) + { + vcl::Window* pWindow = pFrameWindow; + + // skip accessible objects already exposed by the frame objects + if( WindowType::BORDERWINDOW == pWindow->GetType() ) + pWindow = pFrameWindow->GetAccessibleChildWindow(0); + + if( pWindow ) + { + uno::Reference< accessibility::XAccessible > xAccessible = pWindow->GetAccessible(); + if( xAccessible.is() ) + { + AtkObject *accessible = ooo_wrapper_registry_get( xAccessible ); + + if( accessible ) + g_object_ref( G_OBJECT(accessible) ); + else + accessible = atk_object_wrapper_new( xAccessible, gtk_widget_get_accessible(pTopLevel) ); + + return accessible; + } + } + } + + return atk_noop_object_wrapper_new(); +} + +AtkObject* ooo_fixed_get_accessible(GtkWidget *obj) +{ + return wrapper_factory_create_accessible(G_OBJECT(obj)); +} + +static void +wrapper_factory_class_init( AtkObjectFactoryClass *klass ) +{ + klass->create_accessible = wrapper_factory_create_accessible; + klass->get_accessible_type = wrapper_factory_get_accessible_type; +} + +GType +wrapper_factory_get_type() +{ + static GType t = 0; + + if (!t) { + static const GTypeInfo tinfo = + { + sizeof (AtkObjectFactoryClass), + nullptr, nullptr, reinterpret_cast(wrapper_factory_class_init), + nullptr, nullptr, sizeof (AtkObjectFactory), 0, nullptr, nullptr + }; + + t = g_type_register_static ( + ATK_TYPE_OBJECT_FACTORY, "OOoAtkObjectWrapperFactory", + &tinfo, GTypeFlags(0)); + } + + return t; +} + +} // extern C + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkfactory.hxx b/vcl/unx/gtk3/a11y/atkfactory.hxx new file mode 100644 index 0000000000..66c52eab76 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkfactory.hxx @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include + +extern "C" { + +GType wrapper_factory_get_type(); + +} // extern "C" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkhypertext.cxx b/vcl/unx/gtk3/a11y/atkhypertext.cxx new file mode 100644 index 0000000000..83f6817fc9 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkhypertext.cxx @@ -0,0 +1,277 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkwrapper.hxx" + +#include + +using namespace ::com::sun::star; + +// ---------------------- AtkHyperlink ---------------------- + +namespace { + +struct HyperLink { + AtkHyperlink const atk_hyper_link; + + uno::Reference< accessibility::XAccessibleHyperlink > xLink; +}; + +} + +static uno::Reference< accessibility::XAccessibleHyperlink > const & + getHyperlink( AtkHyperlink *pHyperlink ) +{ + HyperLink *pLink = reinterpret_cast(pHyperlink); + return pLink->xLink; +} + +static GObjectClass *hyper_parent_class = nullptr; + +extern "C" { + +static void +hyper_link_finalize (GObject *obj) +{ + HyperLink *hl = reinterpret_cast(obj); + hl->xLink.clear(); + hyper_parent_class->finalize (obj); +} + +static gchar * +hyper_link_get_uri( AtkHyperlink *pLink, + gint i ) +{ + try { + uno::Any aAny = getHyperlink( pLink )->getAccessibleActionObject( i ); + OUString aUri = aAny.get< OUString > (); + return OUStringToGChar(aUri); + } + catch(const uno::Exception&) { + g_warning( "Exception in hyper_link_get_uri" ); + } + return nullptr; +} + +static AtkObject * +hyper_link_get_object( AtkHyperlink *, + gint ) +{ + g_warning( "FIXME: hyper_link_get_object unimplemented" ); + return nullptr; +} +static gint +hyper_link_get_end_index( AtkHyperlink *pLink ) +{ + try { + return getHyperlink( pLink )->getEndIndex(); + } + catch(const uno::Exception&) { + } + return -1; +} +static gint +hyper_link_get_start_index( AtkHyperlink *pLink ) +{ + try { + return getHyperlink( pLink )->getStartIndex(); + } + catch(const uno::Exception&) { + } + return -1; +} +static gboolean +hyper_link_is_valid( AtkHyperlink *pLink ) +{ + try { + return getHyperlink( pLink )->isValid(); + } + catch(const uno::Exception&) { + } + return FALSE; +} +static gint +hyper_link_get_n_anchors( AtkHyperlink *pLink ) +{ + try { + return getHyperlink( pLink )->getAccessibleActionCount(); + } + catch(const uno::Exception&) { + } + return 0; +} + +static guint +hyper_link_link_state( AtkHyperlink * ) +{ + g_warning( "FIXME: hyper_link_link_state unimplemented" ); + return 0; +} +static gboolean +hyper_link_is_selected_link( AtkHyperlink * ) +{ + g_warning( "FIXME: hyper_link_is_selected_link unimplemented" ); + return FALSE; +} + +static void +hyper_link_class_init (AtkHyperlinkClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = hyper_link_finalize; + + hyper_parent_class = static_cast(g_type_class_peek_parent (klass)); + + klass->get_uri = hyper_link_get_uri; + klass->get_object = hyper_link_get_object; + klass->get_end_index = hyper_link_get_end_index; + klass->get_start_index = hyper_link_get_start_index; + klass->is_valid = hyper_link_is_valid; + klass->get_n_anchors = hyper_link_get_n_anchors; + klass->link_state = hyper_link_link_state; + klass->is_selected_link = hyper_link_is_selected_link; +} + +static GType +hyper_link_get_type() +{ + static GType type = 0; + + if (!type) { + static const GTypeInfo tinfo = { + sizeof (AtkHyperlinkClass), + nullptr, /* base init */ + nullptr, /* base finalize */ + reinterpret_cast(hyper_link_class_init), + nullptr, /* class finalize */ + nullptr, /* class data */ + sizeof (HyperLink), /* instance size */ + 0, /* nb preallocs */ + nullptr, /* instance init */ + nullptr /* value table */ + }; + + static const GInterfaceInfo atk_action_info = { + reinterpret_cast(actionIfaceInit), + nullptr, + nullptr + }; + + type = g_type_register_static (ATK_TYPE_HYPERLINK, + "OOoAtkObjHyperLink", &tinfo, + GTypeFlags(0)); + g_type_add_interface_static (type, ATK_TYPE_ACTION, + &atk_action_info); + } + + return type; +} + +// ---------------------- AtkHyperText ---------------------- + +/// @throws uno::RuntimeException +static css::uno::Reference + getHypertext( AtkHypertext *pHypertext ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pHypertext ); + if( pWrap ) + { + if( !pWrap->mpHypertext.is() ) + { + pWrap->mpHypertext.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpHypertext; + } + + return css::uno::Reference(); +} + +static AtkHyperlink * +hypertext_get_link( AtkHypertext *hypertext, + gint link_index) +{ + try { + css::uno::Reference pHypertext + = getHypertext( hypertext ); + if( pHypertext.is() ) + { + HyperLink *pLink = static_cast(g_object_new( hyper_link_get_type(), nullptr )); + pLink->xLink = pHypertext->getHyperLink( link_index ); + if( !pLink->xLink.is() ) { + g_object_unref( G_OBJECT( pLink ) ); + pLink = nullptr; + } + return ATK_HYPERLINK( pLink ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getHyperLink()" ); + } + + return nullptr; +} + +static gint +hypertext_get_n_links( AtkHypertext *hypertext ) +{ + try { + css::uno::Reference pHypertext + = getHypertext( hypertext ); + if( pHypertext.is() ) + return pHypertext->getHyperLinkCount(); + } + catch(const uno::Exception&) { + g_warning( "Exception in getHyperLinkCount()" ); + } + + return 0; +} + +static gint +hypertext_get_link_index( AtkHypertext *hypertext, + gint index) +{ + try { + css::uno::Reference pHypertext + = getHypertext( hypertext ); + if( pHypertext.is() ) + return pHypertext->getHyperLinkIndex( index ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getHyperLinkIndex()" ); + } + + return 0; +} + +} // extern "C" + +void +hypertextIfaceInit (AtkHypertextIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->get_link = hypertext_get_link; + iface->get_n_links = hypertext_get_n_links; + iface->get_link_index = hypertext_get_link_index; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkimage.cxx b/vcl/unx/gtk3/a11y/atkimage.cxx new file mode 100644 index 0000000000..da4965b868 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkimage.cxx @@ -0,0 +1,135 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include + +#include "atkwrapper.hxx" + +#include + +using namespace ::com::sun::star; + +// FIXME +static const gchar * +getAsConst( std::u16string_view rString ) +{ + static const int nMax = 10; + static OString aUgly[nMax]; + static int nIdx = 0; + nIdx = (nIdx + 1) % nMax; + aUgly[nIdx] = OUStringToOString( rString, RTL_TEXTENCODING_UTF8 ); + return aUgly[ nIdx ].getStr(); +} + +/// @throws uno::RuntimeException +static css::uno::Reference + getImage( AtkImage *pImage ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pImage ); + if( pWrap ) + { + if( !pWrap->mpImage.is() ) + { + pWrap->mpImage.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpImage; + } + + return css::uno::Reference(); +} + +extern "C" { + +static const gchar * +image_get_image_description( AtkImage *image ) +{ + try { + css::uno::Reference pImage + = getImage( image ); + if( pImage.is() ) + return getAsConst( pImage->getAccessibleImageDescription() ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleImageDescription()" ); + } + + return nullptr; +} + +static void +image_get_image_position( AtkImage *image, + gint *x, + gint *y, + AtkCoordType coord_type ) +{ + *x = *y = -1; + if( ATK_IS_COMPONENT( image ) ) + { + gint nWidth = -1; + gint nHeight = -1; + atk_component_get_extents(ATK_COMPONENT(image), x, y, &nWidth, &nHeight, coord_type); + } + else + g_warning( "FIXME: no image position information" ); +} + +static void +image_get_image_size( AtkImage *image, + gint *width, + gint *height ) +{ + *width = *height = -1; + try { + css::uno::Reference pImage + = getImage( image ); + if( pImage.is() ) + { + *width = pImage->getAccessibleImageWidth(); + *height = pImage->getAccessibleImageHeight(); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleImageHeight() or Width" ); + } +} + +static gboolean +image_set_image_description( AtkImage *, const gchar * ) +{ + g_warning ("FIXME: no set image description"); + return FALSE; +} + +} // extern "C" + +void +imageIfaceInit (AtkImageIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->set_image_description = image_set_image_description; + iface->get_image_description = image_get_image_description; + iface->get_image_position = image_get_image_position; + iface->get_image_size = image_get_image_size; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atklistener.cxx b/vcl/unx/gtk3/a11y/atklistener.cxx new file mode 100644 index 0000000000..b826ea0306 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atklistener.cxx @@ -0,0 +1,783 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "atklistener.hxx" +#include "atkwrapper.hxx" +#include +#include + +#include + +#define DEBUG_ATK_LISTENER 0 + +#if DEBUG_ATK_LISTENER +#include +#include +#endif + +using namespace com::sun::star; + +AtkListener::AtkListener( AtkObjectWrapper* pWrapper ) : mpWrapper( pWrapper ) +{ + if( mpWrapper ) + { + g_object_ref( mpWrapper ); + updateChildList( mpWrapper->mpContext ); + } +} + +AtkListener::~AtkListener() +{ + if( mpWrapper ) + g_object_unref( mpWrapper ); +} + +/*****************************************************************************/ + +static AtkStateType mapState( const uno::Any &rAny ) +{ + sal_Int64 nState = accessibility::AccessibleStateType::INVALID; + rAny >>= nState; + return mapAtkState( nState ); +} + +/*****************************************************************************/ + +extern "C" { + // rhbz#1001768 - down to horrific problems releasing the solar mutex + // while destroying a Window - which occurs inside these notifications. + static gboolean + idle_defunc_state_change( AtkObject *atk_obj ) + { + SolarMutexGuard aGuard; + + // This is an equivalent to a state change to DEFUNC(T). + atk_object_notify_state_change( atk_obj, ATK_STATE_DEFUNCT, true ); + if( atk_get_focus_object() == atk_obj ) + { + SAL_WNODEPRECATED_DECLARATIONS_PUSH + atk_focus_tracker_notify( nullptr ); + SAL_WNODEPRECATED_DECLARATIONS_POP + } + g_object_unref( G_OBJECT( atk_obj ) ); + return false; + } +} + +// XEventListener implementation +void AtkListener::disposing( const lang::EventObject& ) +{ + if( !mpWrapper ) + return; + + AtkObject *atk_obj = ATK_OBJECT( mpWrapper ); + + // Release all interface references to avoid shutdown problems with + // global mutex + atk_object_wrapper_dispose( mpWrapper ); + + g_idle_add( reinterpret_cast(idle_defunc_state_change), + g_object_ref( G_OBJECT( atk_obj ) ) ); + + // Release the wrapper object so that it can vanish .. + g_object_unref( mpWrapper ); + mpWrapper = nullptr; +} + +/*****************************************************************************/ + +static AtkObject *getObjFromAny( const uno::Any &rAny ) +{ + uno::Reference< accessibility::XAccessible > xAccessible; + rAny >>= xAccessible; + return xAccessible.is() ? atk_object_wrapper_ref( xAccessible ) : nullptr; +} + +/*****************************************************************************/ + +// Updates the child list held to provide the old IndexInParent on children_changed::remove +void AtkListener::updateChildList( + css::uno::Reference const & + pContext) +{ + m_aChildList.clear(); + + sal_Int64 nStateSet = pContext->getAccessibleStateSet(); + if( (nStateSet & accessibility::AccessibleStateType::DEFUNC) + || (nStateSet & accessibility::AccessibleStateType::MANAGES_DESCENDANTS) ) + return; + + css::uno::Reference xContext3(pContext, css::uno::UNO_QUERY); + if (xContext3.is()) + { + m_aChildList = comphelper::sequenceToContainer>>(xContext3->getAccessibleChildren()); + } + else + { + sal_Int64 nChildren = pContext->getAccessibleChildCount(); + assert(o3tl::make_unsigned(nChildren) < m_aChildList.max_size()); + m_aChildList.resize(nChildren); + for(sal_Int64 n = 0; n < nChildren; n++) + { + try + { + m_aChildList[n] = pContext->getAccessibleChild(n); + } + catch (lang::IndexOutOfBoundsException const&) + { + sal_Int64 nChildren2 = pContext->getAccessibleChildCount(); + assert(nChildren2 <= n && "consistency?"); + m_aChildList.resize(std::min(nChildren2, n)); + break; + } + } + } +} + +/*****************************************************************************/ + +void AtkListener::handleChildAdded( + const uno::Reference< accessibility::XAccessibleContext >& rxParent, + const uno::Reference< accessibility::XAccessible>& rxAccessible, + sal_Int32 nIndexHint) +{ + AtkObject * pChild = rxAccessible.is() ? atk_object_wrapper_ref( rxAccessible ) : nullptr; + + if( !pChild ) + return; + + if (nIndexHint != -1 && (nIndexHint < 0 || nIndexHint >= static_cast(m_aChildList.size()))) + { + SAL_WARN("vcl", "index hint out of range, ignoring"); + nIndexHint = -1; + } + + bool bNeedToFullFullChildList = true; + if (nIndexHint != -1) + { + bNeedToFullFullChildList = false; + sal_Int64 nStateSet = rxParent->getAccessibleStateSet(); + if( !(nStateSet & accessibility::AccessibleStateType::DEFUNC) + || (nStateSet & accessibility::AccessibleStateType::MANAGES_DESCENDANTS) ) + { + m_aChildList.insert(m_aChildList.begin() + nIndexHint, rxAccessible); + if (m_aChildList[nIndexHint] != rxParent->getAccessibleChild(nIndexHint)) + { + SAL_WARN("vcl", "wrong index hint, falling back to updating full child list"); + bNeedToFullFullChildList = true; + } + } + } + if (bNeedToFullFullChildList) + updateChildList(rxParent); + + atk_object_wrapper_add_child( mpWrapper, pChild, + atk_object_get_index_in_parent( pChild )); + + g_object_unref( pChild ); +} + +/*****************************************************************************/ + +void AtkListener::handleChildRemoved( + const uno::Reference< accessibility::XAccessibleContext >& rxParent, + const uno::Reference< accessibility::XAccessible>& rxChild, + sal_Int32 nChildIndexHint) +{ + sal_Int32 nIndex = nChildIndexHint; + if (nIndex != -1 && (nIndex < 0 || nIndex >= static_cast(m_aChildList.size()))) + { + SAL_WARN("vcl", "index hint out of range, ignoring"); + nIndex = -1; + } + if (nIndex != -1 && rxChild != m_aChildList[nIndex]) + { + SAL_WARN("vcl", "index hint points to wrong child, somebody forgot to send accessibility update event"); + nIndex = -1; + } + + // if the hint did not work, search + const size_t nmax = m_aChildList.size(); + if (nIndex == -1) + // Locate the child in the children list + for( size_t n = 0; n < nmax; ++n ) + { + // Comparing via uno::Reference::operator== is expensive + // with lots of objects, so assume we can find it the cheap way + // first, which works most of the time. + if( rxChild.get() == m_aChildList[n].get() ) + { + nIndex = n; + break; + } + } + // The cheap way failed, find it via the more expensive path + if (nIndex == -1) + for( size_t n = 0; n < nmax; ++n ) + { + if( rxChild == m_aChildList[n] ) + { + nIndex = n; + break; + } + } + + // FIXME: two problems here: + // a) we get child-removed events for objects that are no real children + // in the accessibility hierarchy or have been removed before due to + // some child removing batch. + // b) spi_atk_bridge_signal_listener ignores the given parameters + // for children_changed events and always asks the parent for the + // 0. child, which breaks somehow on vanishing list boxes. + // Ignoring "remove" events for objects not in the m_aChildList + // for now. + if( nIndex < 0 ) + return; + + uno::Reference xBroadcaster( + rxChild->getAccessibleContext(), uno::UNO_QUERY); + + if (xBroadcaster.is()) + { + uno::Reference xListener(this); + xBroadcaster->removeAccessibleEventListener(xListener); + } + + // update child list + sal_Int64 nStateSet = rxParent->getAccessibleStateSet(); + if(!( (nStateSet & accessibility::AccessibleStateType::DEFUNC) + || (nStateSet & accessibility::AccessibleStateType::MANAGES_DESCENDANTS) )) + { + m_aChildList.erase(m_aChildList.begin() + nIndex); + } + + AtkObject * pChild = atk_object_wrapper_ref( rxChild, false ); + if( pChild ) + { + atk_object_wrapper_remove_child( mpWrapper, pChild, nIndex ); + g_object_unref( pChild ); + } +} + +/*****************************************************************************/ + +void AtkListener::handleInvalidateChildren( + const uno::Reference< accessibility::XAccessibleContext >& rxParent) +{ + // Send notifications for all previous children + size_t n = m_aChildList.size(); + while( n-- > 0 ) + { + if( m_aChildList[n].is() ) + { + AtkObject * pChild = atk_object_wrapper_ref( m_aChildList[n], false ); + if( pChild ) + { + atk_object_wrapper_remove_child( mpWrapper, pChild, n ); + g_object_unref( pChild ); + } + } + } + + updateChildList(rxParent); + + // Send notifications for all new children + size_t nmax = m_aChildList.size(); + for( n = 0; n < nmax; ++n ) + { + if( m_aChildList[n].is() ) + { + AtkObject * pChild = atk_object_wrapper_ref( m_aChildList[n] ); + + if( pChild ) + { + atk_object_wrapper_add_child( mpWrapper, pChild, n ); + g_object_unref( pChild ); + } + } + } +} + +/*****************************************************************************/ + +static uno::Reference< accessibility::XAccessibleContext > +getAccessibleContextFromSource( const uno::Reference< uno::XInterface >& rxSource ) +{ + uno::Reference< accessibility::XAccessibleContext > xContext(rxSource, uno::UNO_QUERY); + if( ! xContext.is() ) + { + g_warning( "ERROR: Event source does not implement XAccessibleContext" ); + + // Second try - query for XAccessible, which should give us access to + // XAccessibleContext. + uno::Reference< accessibility::XAccessible > xAccessible(rxSource, uno::UNO_QUERY); + if( xAccessible.is() ) + xContext = xAccessible->getAccessibleContext(); + } + + return xContext; +} + +#if DEBUG_ATK_LISTENER + +namespace { + +void printNotifyEvent( const accessibility::AccessibleEventObject& rEvent ) +{ + static std::vector aLabels = { + 0, + "NAME_CHANGED", // 01 + "DESCRIPTION_CHANGED", // 02 + "ACTION_CHANGED", // 03 + "STATE_CHANGED", // 04 + "ACTIVE_DESCENDANT_CHANGED", // 05 + "BOUNDRECT_CHANGED", // 06 + "CHILD", // 07 + "INVALIDATE_ALL_CHILDREN", // 08 + "SELECTION_CHANGED", // 09 + "VISIBLE_DATA_CHANGED", // 10 + "VALUE_CHANGED", // 11 + "CONTENT_FLOWS_FROM_RELATION_CHANGED", // 12 + "CONTENT_FLOWS_TO_RELATION_CHANGED", // 13 + "CONTROLLED_BY_RELATION_CHANGED", // 14 + "CONTROLLER_FOR_RELATION_CHANGED", // 15 + "LABEL_FOR_RELATION_CHANGED", // 16 + "LABELED_BY_RELATION_CHANGED", // 17 + "MEMBER_OF_RELATION_CHANGED", // 18 + "SUB_WINDOW_OF_RELATION_CHANGED", // 19 + "CARET_CHANGED", // 20 + "TEXT_SELECTION_CHANGED", // 21 + "TEXT_CHANGED", // 22 + "TEXT_ATTRIBUTE_CHANGED", // 23 + "HYPERTEXT_CHANGED", // 24 + "TABLE_CAPTION_CHANGED", // 25 + "TABLE_COLUMN_DESCRIPTION_CHANGED", // 26 + "TABLE_COLUMN_HEADER_CHANGED", // 27 + "TABLE_MODEL_CHANGED", // 28 + "TABLE_ROW_DESCRIPTION_CHANGED", // 29 + "TABLE_ROW_HEADER_CHANGED", // 30 + "TABLE_SUMMARY_CHANGED", // 31 + "LISTBOX_ENTRY_EXPANDED", // 32 + "LISTBOX_ENTRY_COLLAPSED", // 33 + "ACTIVE_DESCENDANT_CHANGED_NOFOCUS", // 34 + "SELECTION_CHANGED_ADD", // 35 + "SELECTION_CHANGED_REMOVE", // 36 + "SELECTION_CHANGED_WITHIN", // 37 + "PAGE_CHANGED", // 38 + "SECTION_CHANGED", // 39 + "COLUMN_CHANGED", // 40 + "ROLE_CHANGED", // 41 + }; + + static std::vector aStates = { + "INVALID", // 00 + "ACTIVE", // 01 + "ARMED", // 02 + "BUSY", // 03 + "CHECKED", // 04 + "DEFUNC", // 05 + "EDITABLE", // 06 + "ENABLED", // 07 + "EXPANDABLE", // 08 + "EXPANDED", // 09 + "FOCUSABLE", // 10 + "FOCUSED", // 11 + "HORIZONTAL", // 12 + "ICONIFIED", // 13 + "INDETERMINATE", // 14 + "MANAGES_DESCENDANTS", // 15 + "MODAL", // 16 + "MULTI_LINE", // 17 + "MULTI_SELECTABLE", // 18 + "OPAQUE", // 19 + "PRESSED", // 20 + "RESIZABLE", // 21 + "SELECTABLE", // 22 + "SELECTED", // 23 + "SENSITIVE", // 24 + "SHOWING", // 25 + "SINGLE_LINE", // 26 + "STALE", // 27 + "TRANSIENT", // 28 + "VERTICAL", // 29 + "VISIBLE", // 30 + "MOVEABLE", // 31 + "DEFAULT", // 32 + "OFFSCREEN", // 33 + "COLLAPSE", // 34 + }; + + auto getOrUnknown = [](const std::vector& rCont, size_t nIndex) -> std::string + { + return (nIndex < rCont.size()) ? rCont[nIndex] : ""; + }; + + std::ostringstream os; + os << "--" << std::endl; + os << "* event = " << getOrUnknown(aLabels, rEvent.EventId) << std::endl; + + switch (rEvent.EventId) + { + case accessibility::AccessibleEventId::STATE_CHANGED: + { + sal_Int64 nState; + if (rEvent.OldValue >>= nState) + os << " * old state = " << getOrUnknown(aStates, nState); + if (rEvent.NewValue >>= nState) + os << " * new state = " << getOrUnknown(aStates, nState); + + os << std::endl; + break; + } + default: + ; + } + + std::cout << os.str(); +} + +} + +#endif + +void AtkListener::notifyEvent( const accessibility::AccessibleEventObject& aEvent ) +{ + if( !mpWrapper ) + return; + + AtkObject *atk_obj = ATK_OBJECT( mpWrapper ); + + switch( aEvent.EventId ) + { + // AtkObject signals: + // Hierarchy signals + case accessibility::AccessibleEventId::CHILD: + { + uno::Reference< accessibility::XAccessibleContext > xParent; + uno::Reference< accessibility::XAccessible > xChild; + + xParent = getAccessibleContextFromSource(aEvent.Source); + g_return_if_fail( xParent.is() ); + + if( aEvent.OldValue >>= xChild ) + handleChildRemoved(xParent, xChild, aEvent.IndexHint); + + if( aEvent.NewValue >>= xChild ) + handleChildAdded(xParent, xChild, aEvent.IndexHint); + break; + } + + case accessibility::AccessibleEventId::INVALIDATE_ALL_CHILDREN: + { + uno::Reference< accessibility::XAccessibleContext > xParent = getAccessibleContextFromSource(aEvent.Source); + g_return_if_fail( xParent.is() ); + + handleInvalidateChildren(xParent); + break; + } + + case accessibility::AccessibleEventId::NAME_CHANGED: + { + OUString aName; + if( aEvent.NewValue >>= aName ) + { + atk_object_set_name(atk_obj, + OUStringToOString(aName, RTL_TEXTENCODING_UTF8).getStr()); + } + break; + } + + case accessibility::AccessibleEventId::DESCRIPTION_CHANGED: + { + OUString aDescription; + if( aEvent.NewValue >>= aDescription ) + { + atk_object_set_description(atk_obj, + OUStringToOString(aDescription, RTL_TEXTENCODING_UTF8).getStr()); + } + break; + } + + case accessibility::AccessibleEventId::STATE_CHANGED: + { + AtkStateType eOldState = mapState( aEvent.OldValue ); + AtkStateType eNewState = mapState( aEvent.NewValue ); + + bool bState = eNewState != ATK_STATE_INVALID; + AtkStateType eRealState = bState ? eNewState : eOldState; + + atk_object_notify_state_change( atk_obj, eRealState, bState ); + break; + } + + case accessibility::AccessibleEventId::BOUNDRECT_CHANGED: + + if( ATK_IS_COMPONENT( atk_obj ) ) + { + AtkRectangle rect; + + atk_component_get_extents( ATK_COMPONENT( atk_obj ), + &rect.x, + &rect.y, + &rect.width, + &rect.height, + ATK_XY_SCREEN ); + + g_signal_emit_by_name( atk_obj, "bounds-changed", &rect ); + } + else + g_warning( "bounds-changed event for object not implementing AtkComponent\n"); + break; + + case accessibility::AccessibleEventId::VISIBLE_DATA_CHANGED: + g_signal_emit_by_name( atk_obj, "visible-data-changed" ); + break; + + case accessibility::AccessibleEventId::ACTIVE_DESCENDANT_CHANGED: + { + AtkObject *pChild = getObjFromAny( aEvent.NewValue ); + if( pChild ) + { + g_signal_emit_by_name( atk_obj, "active-descendant-changed", pChild ); + g_object_unref( pChild ); + } + break; + } + + //ACTIVE_DESCENDANT_CHANGED_NOFOCUS (sic) appears to have been added + //as a workaround or an aid for the ia2 winaccessibility implementation + //so ignore it silently without warning here + case accessibility::AccessibleEventId::ACTIVE_DESCENDANT_CHANGED_NOFOCUS: + break; + + // #i92103# + case accessibility::AccessibleEventId::LISTBOX_ENTRY_EXPANDED: + { + AtkObject *pChild = getObjFromAny( aEvent.NewValue ); + if( pChild ) + { + atk_object_notify_state_change( pChild, ATK_STATE_EXPANDED, true ); + g_object_unref( pChild ); + } + break; + } + + case accessibility::AccessibleEventId::LISTBOX_ENTRY_COLLAPSED: + { + AtkObject *pChild = getObjFromAny( aEvent.NewValue ); + if( pChild ) + { + atk_object_notify_state_change( pChild, ATK_STATE_EXPANDED, false ); + g_object_unref( pChild ); + } + break; + } + + // AtkAction signals ... + case accessibility::AccessibleEventId::ACTION_CHANGED: + g_signal_emit_by_name( G_OBJECT( atk_obj ), "property_change::accessible-actions"); + break; + + // AtkText + case accessibility::AccessibleEventId::CARET_CHANGED: + { + sal_Int32 nPos=0; + aEvent.NewValue >>= nPos; + g_signal_emit_by_name( atk_obj, "text_caret_moved", nPos ); + break; + } + case accessibility::AccessibleEventId::TEXT_CHANGED: + { + // cf. comphelper/source/misc/accessibletexthelper.cxx (implInitTextChangedEvent) + accessibility::TextSegment aDeletedText; + accessibility::TextSegment aInsertedText; + + if( aEvent.OldValue >>= aDeletedText ) + { + const OString aDeletedTextUtf8 = OUStringToOString(aDeletedText.SegmentText, RTL_TEXTENCODING_UTF8); + g_signal_emit_by_name( atk_obj, "text-remove", + static_cast(aDeletedText.SegmentStart), + static_cast(aDeletedText.SegmentEnd - aDeletedText.SegmentStart), + g_strdup(aDeletedTextUtf8.getStr())); + + } + if( aEvent.NewValue >>= aInsertedText ) + { + const OString aInsertedTextUtf8 = OUStringToOString(aInsertedText.SegmentText, RTL_TEXTENCODING_UTF8); + g_signal_emit_by_name( atk_obj, "text-insert", + static_cast(aInsertedText.SegmentStart), + static_cast(aInsertedText.SegmentEnd - aInsertedText.SegmentStart), + g_strdup(aInsertedTextUtf8.getStr())); + } + break; + } + + case accessibility::AccessibleEventId::TEXT_SELECTION_CHANGED: + { + g_signal_emit_by_name( atk_obj, "text-selection-changed" ); + break; + } + + case accessibility::AccessibleEventId::TEXT_ATTRIBUTE_CHANGED: + g_signal_emit_by_name( atk_obj, "text-attributes-changed" ); + break; + + // AtkValue + case accessibility::AccessibleEventId::VALUE_CHANGED: + g_object_notify( G_OBJECT( atk_obj ), "accessible-value" ); + break; + + case accessibility::AccessibleEventId::CONTENT_FLOWS_FROM_RELATION_CHANGED: + case accessibility::AccessibleEventId::CONTENT_FLOWS_TO_RELATION_CHANGED: + case accessibility::AccessibleEventId::CONTROLLED_BY_RELATION_CHANGED: + case accessibility::AccessibleEventId::CONTROLLER_FOR_RELATION_CHANGED: + case accessibility::AccessibleEventId::LABEL_FOR_RELATION_CHANGED: + case accessibility::AccessibleEventId::LABELED_BY_RELATION_CHANGED: + case accessibility::AccessibleEventId::MEMBER_OF_RELATION_CHANGED: + case accessibility::AccessibleEventId::SUB_WINDOW_OF_RELATION_CHANGED: + // FIXME: ask Bill how Atk copes with this little lot ... + break; + + // AtkTable + case accessibility::AccessibleEventId::TABLE_MODEL_CHANGED: + { + accessibility::AccessibleTableModelChange aChange; + aEvent.NewValue >>= aChange; + + sal_Int32 nRowsChanged = aChange.LastRow - aChange.FirstRow + 1; + sal_Int32 nColumnsChanged = aChange.LastColumn - aChange.FirstColumn + 1; + + switch( aChange.Type ) + { + case accessibility::AccessibleTableModelChangeType::COLUMNS_INSERTED: + g_signal_emit_by_name(G_OBJECT(atk_obj), "column-inserted", + aChange.FirstColumn, nColumnsChanged); + break; + case accessibility::AccessibleTableModelChangeType::COLUMNS_REMOVED: + g_signal_emit_by_name(G_OBJECT(atk_obj), "column-deleted", + aChange.FirstColumn, nColumnsChanged); + break; + case accessibility::AccessibleTableModelChangeType::ROWS_INSERTED: + g_signal_emit_by_name(G_OBJECT(atk_obj), "row-inserted", + aChange.FirstRow, nRowsChanged); + break; + case accessibility::AccessibleTableModelChangeType::ROWS_REMOVED: + g_signal_emit_by_name(G_OBJECT(atk_obj), "row-deleted", + aChange.FirstRow, nRowsChanged); + break; + case accessibility::AccessibleTableModelChangeType::UPDATE: + // This is not really a model change, is it ? + break; + default: + g_warning( "TESTME: unusual table model change %d\n", aChange.Type ); + break; + } + g_signal_emit_by_name( G_OBJECT( atk_obj ), "model-changed" ); + break; + } + + case accessibility::AccessibleEventId::TABLE_COLUMN_HEADER_CHANGED: + { + accessibility::AccessibleTableModelChange aChange; + aEvent.NewValue >>= aChange; + + AtkPropertyValues values; + memset(&values, 0, sizeof(AtkPropertyValues)); + g_value_init (&values.new_value, G_TYPE_INT); + values.property_name = "accessible-table-column-header"; + + for (sal_Int32 nChangedColumn = aChange.FirstColumn; nChangedColumn <= aChange.LastColumn; ++nChangedColumn) + { + g_value_set_int (&values.new_value, nChangedColumn); + g_signal_emit_by_name(G_OBJECT(atk_obj), "property_change::accessible-table-column-header", &values, nullptr); + } + break; + } + + case accessibility::AccessibleEventId::TABLE_CAPTION_CHANGED: + g_signal_emit_by_name( G_OBJECT( atk_obj ), "property_change::accessible-table-caption"); + break; + + case accessibility::AccessibleEventId::TABLE_COLUMN_DESCRIPTION_CHANGED: + g_signal_emit_by_name( G_OBJECT( atk_obj ), "property_change::accessible-table-column-description"); + break; + + case accessibility::AccessibleEventId::TABLE_ROW_DESCRIPTION_CHANGED: + g_signal_emit_by_name( G_OBJECT( atk_obj ), "property_change::accessible-table-row-description"); + break; + + case accessibility::AccessibleEventId::TABLE_ROW_HEADER_CHANGED: + g_signal_emit_by_name( G_OBJECT( atk_obj ), "property_change::accessible-table-row-header"); + break; + + case accessibility::AccessibleEventId::TABLE_SUMMARY_CHANGED: + g_signal_emit_by_name( G_OBJECT( atk_obj ), "property_change::accessible-table-summary"); + break; + + case accessibility::AccessibleEventId::SELECTION_CHANGED: + case accessibility::AccessibleEventId::SELECTION_CHANGED_ADD: + case accessibility::AccessibleEventId::SELECTION_CHANGED_REMOVE: + case accessibility::AccessibleEventId::SELECTION_CHANGED_WITHIN: + if (ATK_IS_SELECTION(atk_obj)) + g_signal_emit_by_name(G_OBJECT(atk_obj), "selection_changed"); + else + { + // e.g. tdf#122353, when such dialogs become native the problem will go away anyway + SAL_INFO("vcl.gtk", "selection change from obj which doesn't support XAccessibleSelection"); + } + break; + + case accessibility::AccessibleEventId::HYPERTEXT_CHANGED: + g_signal_emit_by_name( G_OBJECT( atk_obj ), "property_change::accessible-hypertext-offset"); + break; + + case accessibility::AccessibleEventId::ROLE_CHANGED: + { + uno::Reference< accessibility::XAccessibleContext > xContext = getAccessibleContextFromSource( aEvent.Source ); + atk_object_wrapper_set_role(mpWrapper, xContext->getAccessibleRole(), xContext->getAccessibleStateSet()); + break; + } + + case accessibility::AccessibleEventId::PAGE_CHANGED: + { + /* // If we implemented AtkDocument then I imagine this is what this + // handler should look like + sal_Int32 nPos=0; + aEvent.NewValue >>= nPos; + g_signal_emit_by_name( G_OBJECT( atk_obj ), "page_changed", nPos ); + */ + break; + } + + default: + SAL_WARN("vcl.gtk", "Unknown event notification: " << aEvent.EventId); + break; + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atklistener.hxx b/vcl/unx/gtk3/a11y/atklistener.hxx new file mode 100644 index 0000000000..e286f40e5a --- /dev/null +++ b/vcl/unx/gtk3/a11y/atklistener.hxx @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include +#include + +#include + +#include "atkwrapper.hxx" + +class AtkListener : public ::cppu::WeakImplHelper< css::accessibility::XAccessibleEventListener > +{ +public: + explicit AtkListener(AtkObjectWrapper * pWrapper); + + // XEventListener + virtual void SAL_CALL disposing( const css::lang::EventObject& Source ) override; + + // XAccessibleEventListener + virtual void SAL_CALL notifyEvent( const css::accessibility::AccessibleEventObject& aEvent ) override; + +private: + + AtkObjectWrapper *mpWrapper; + std::vector< css::uno::Reference< css::accessibility::XAccessible > > + m_aChildList; + + virtual ~AtkListener() override; + + // Updates the child list held to provide the old IndexInParent on children_changed::remove + void updateChildList( + css::uno::Reference const & + pContext); + + // Process CHILD_EVENT notifications with a new child added + void handleChildAdded( + const css::uno::Reference< css::accessibility::XAccessibleContext >& rxParent, + const css::uno::Reference< css::accessibility::XAccessible>& rxChild, + sal_Int32 nIndexHint); + + // Process CHILD_EVENT notifications with a child removed + void handleChildRemoved( + const css::uno::Reference< css::accessibility::XAccessibleContext >& rxParent, + const css::uno::Reference< css::accessibility::XAccessible>& rxChild, + sal_Int32 nIndexHint); + + // Process INVALIDATE_ALL_CHILDREN notification + void handleInvalidateChildren( + const css::uno::Reference< css::accessibility::XAccessibleContext >& rxParent); +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkregistry.cxx b/vcl/unx/gtk3/a11y/atkregistry.cxx new file mode 100644 index 0000000000..ff96378c41 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkregistry.cxx @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkregistry.hxx" + +using namespace ::com::sun::star::accessibility; +using namespace ::com::sun::star::uno; + +static GHashTable *uno_to_gobject = nullptr; + +/*****************************************************************************/ + +AtkObject * +ooo_wrapper_registry_get(const Reference< XAccessible >& rxAccessible) +{ + if( uno_to_gobject ) + { + gpointer cached = + g_hash_table_lookup(uno_to_gobject, static_cast(rxAccessible.get())); + + if( cached ) + return ATK_OBJECT( cached ); + } + + return nullptr; +} + +/*****************************************************************************/ + +void +ooo_wrapper_registry_add(const Reference< XAccessible >& rxAccessible, AtkObject *obj) +{ + if( !uno_to_gobject ) + uno_to_gobject = g_hash_table_new (nullptr, nullptr); + + g_hash_table_insert( uno_to_gobject, static_cast(rxAccessible.get()), obj ); +} + +/*****************************************************************************/ + +void +ooo_wrapper_registry_remove( + css::uno::Reference const & pAccessible) +{ + if( uno_to_gobject ) + g_hash_table_remove( + uno_to_gobject, static_cast(pAccessible.get()) ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkregistry.hxx b/vcl/unx/gtk3/a11y/atkregistry.hxx new file mode 100644 index 0000000000..b26b838407 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkregistry.hxx @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include +#include + +AtkObject* +ooo_wrapper_registry_get(const css::uno::Reference& rxAccessible); + +void ooo_wrapper_registry_add( + const css::uno::Reference& rxAccessible, AtkObject* obj); + +void ooo_wrapper_registry_remove( + css::uno::Reference const& pAccessible); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkselection.cxx b/vcl/unx/gtk3/a11y/atkselection.cxx new file mode 100644 index 0000000000..0e74f1c295 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkselection.cxx @@ -0,0 +1,206 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkwrapper.hxx" + +#include +#include + +using namespace ::com::sun::star; + +/// @throws uno::RuntimeException +static css::uno::Reference + getSelection( AtkSelection *pSelection ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pSelection ); + if( pWrap ) + { + if( !pWrap->mpSelection.is() ) + { + pWrap->mpSelection.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpSelection; + } + + return css::uno::Reference(); +} + +extern "C" { + +static gboolean +selection_add_selection( AtkSelection *selection, + gint i ) +{ + try { + css::uno::Reference pSelection + = getSelection( selection ); + if( pSelection.is() ) + { + pSelection->selectAccessibleChild( i ); + return true; + } + } + catch(const uno::Exception&) { + g_warning( "Exception in selectAccessibleChild()" ); + } + + return FALSE; +} + +static gboolean +selection_clear_selection( AtkSelection *selection ) +{ + try { + css::uno::Reference pSelection + = getSelection( selection ); + if( pSelection.is() ) + { + pSelection->clearAccessibleSelection(); + return true; + } + } + catch(const uno::Exception&) { + g_warning( "Exception in clearAccessibleSelection()" ); + } + + return FALSE; +} + +static AtkObject* +selection_ref_selection( AtkSelection *selection, + gint i ) +{ + try { + css::uno::Reference pSelection + = getSelection( selection ); + if( pSelection.is() ) + return atk_object_wrapper_ref( pSelection->getSelectedAccessibleChild( i ) ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getSelectedAccessibleChild()" ); + } + + return nullptr; +} + +static gint +selection_get_selection_count( AtkSelection *selection) +{ + try { + css::uno::Reference pSelection + = getSelection( selection ); + if( pSelection.is() ) + { + sal_Int64 nSelected = pSelection->getSelectedAccessibleChildCount(); + if (nSelected > std::numeric_limits::max()) + { + SAL_WARN("vcl.gtk", "selection_get_selection_count: Count exceeds maximum gint value, " + "using max gint."); + nSelected = std::numeric_limits::max(); + } + return nSelected; + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getSelectedAccessibleChildCount()" ); + } + + return -1; +} + +static gboolean +selection_is_child_selected( AtkSelection *selection, + gint i) +{ + try { + css::uno::Reference pSelection + = getSelection( selection ); + if( pSelection.is() ) + return pSelection->isAccessibleChildSelected( i ); + } + catch(const uno::Exception&) { + g_warning( "Exception in isAccessibleChildSelected()" ); + } + + return FALSE; +} + +static gboolean +selection_remove_selection( AtkSelection *selection, + gint i ) +{ + try { + css::uno::Reference pSelection + = getSelection( selection ); + if( pSelection.is() ) + { + css::uno::Reference xAcc = pSelection->getSelectedAccessibleChild(i); + if (!xAcc.is()) + return false; + + css::uno::Reference xAccContext = xAcc->getAccessibleContext(); + const sal_Int64 nChildIndex = xAccContext->getAccessibleIndexInParent(); + pSelection->deselectAccessibleChild(nChildIndex); + return true; + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getSelectedAccessibleChild(), getAccessibleIndexInParent() or deselectAccessibleChild()" ); + } + + return FALSE; +} + +static gboolean +selection_select_all_selection( AtkSelection *selection) +{ + try { + css::uno::Reference pSelection + = getSelection( selection ); + if( pSelection.is() ) + { + pSelection->selectAllAccessibleChildren(); + return true; + } + } + catch(const uno::Exception&) { + g_warning( "Exception in selectAllAccessibleChildren()" ); + } + + return FALSE; +} + +} // extern "C" + +void +selectionIfaceInit( AtkSelectionIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->add_selection = selection_add_selection; + iface->clear_selection = selection_clear_selection; + iface->ref_selection = selection_ref_selection; + iface->get_selection_count = selection_get_selection_count; + iface->is_child_selected = selection_is_child_selected; + iface->remove_selection = selection_remove_selection; + iface->select_all_selection = selection_select_all_selection; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atktable.cxx b/vcl/unx/gtk3/a11y/atktable.cxx new file mode 100644 index 0000000000..021f3c19c4 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atktable.cxx @@ -0,0 +1,647 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include + +#include "atkwrapper.hxx" + +#include +#include +#include +#include + +using namespace ::com::sun::star; + +static AtkObject * +atk_object_wrapper_conditional_ref( const uno::Reference< accessibility::XAccessible >& rxAccessible ) +{ + if( rxAccessible.is() ) + return atk_object_wrapper_ref( rxAccessible ); + + return nullptr; +} + +/*****************************************************************************/ + +// FIXME +static const gchar * +getAsConst( std::u16string_view rString ) +{ + static const int nMax = 10; + static OString aUgly[nMax]; + static int nIdx = 0; + nIdx = (nIdx + 1) % nMax; + aUgly[nIdx] = OUStringToOString( rString, RTL_TEXTENCODING_UTF8 ); + return aUgly[ nIdx ].getStr(); +} + +/*****************************************************************************/ + +/// @throws uno::RuntimeException +static css::uno::Reference + getTable( AtkTable *pTable ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pTable ); + if( pWrap ) + { + if( !pWrap->mpTable.is() ) + { + pWrap->mpTable.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpTable; + } + + return css::uno::Reference(); +} + +static css::uno::Reference + getTableSelection(AtkTable *pTable) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER(pTable); + if (pWrap) + { + if (!pWrap->mpTableSelection.is()) + { + pWrap->mpTableSelection.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpTableSelection; + } + + return css::uno::Reference(); +} + +/*****************************************************************************/ + +extern "C" { + +static AtkObject* +table_wrapper_ref_at (AtkTable *table, + gint row, + gint column) +{ + try { + css::uno::Reference pTable = getTable( table ); + if( pTable.is() ) + return atk_object_wrapper_conditional_ref( pTable->getAccessibleCellAt( row, column ) ); + } + + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleCellAt()" ); + } + + return nullptr; +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_index_at (AtkTable *table, + gint row, + gint column) +{ + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + { + sal_Int64 nIndex = pTable->getAccessibleIndex( row, column ); + if (nIndex > std::numeric_limits::max()) + { + // use -2 when the child index is too large to fit into 32 bit to neither use the + // valid index of another cell nor -1, which might easily be interpreted as the cell + // not/no longer being valid + SAL_WARN("vcl.gtk", "table_wrapper_get_index_at: Child index exceeds maximum gint value, " + "returning -2."); + nIndex = -2; + } + return nIndex; + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleIndex()" ); + } + + return -1; +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_column_at_index (AtkTable *table, + gint nIndex) +{ + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + return pTable->getAccessibleColumn( nIndex ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleColumn()" ); + } + + return -1; +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_row_at_index( AtkTable *table, + gint nIndex ) +{ + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + return pTable->getAccessibleRow( nIndex ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleRow()" ); + } + + return -1; +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_n_columns( AtkTable *table ) +{ + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + return pTable->getAccessibleColumnCount(); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleColumnCount()" ); + } + + return -1; +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_n_rows( AtkTable *table ) +{ + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + return pTable->getAccessibleRowCount(); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleRowCount()" ); + } + + return -1; +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_column_extent_at( AtkTable *table, + gint row, + gint column ) +{ + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + return pTable->getAccessibleColumnExtentAt( row, column ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleColumnExtentAt()" ); + } + + return -1; +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_row_extent_at( AtkTable *table, + gint row, + gint column ) +{ + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + return pTable->getAccessibleRowExtentAt( row, column ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleRowExtentAt()" ); + } + + return -1; +} + +/*****************************************************************************/ + +static AtkObject * +table_wrapper_get_caption( AtkTable *table ) +{ + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + return atk_object_wrapper_conditional_ref( pTable->getAccessibleCaption() ); + } + + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleCaption()" ); + } + + return nullptr; +} + +/*****************************************************************************/ + +static const gchar * +table_wrapper_get_row_description( AtkTable *table, + gint row ) +{ + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + return getAsConst( pTable->getAccessibleRowDescription( row ) ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleRowDescription()" ); + } + + return nullptr; +} + +/*****************************************************************************/ + +static const gchar * +table_wrapper_get_column_description( AtkTable *table, + gint column ) +{ + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + return getAsConst( pTable->getAccessibleColumnDescription( column ) ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleColumnDescription()" ); + } + + return nullptr; +} + +/*****************************************************************************/ + +static AtkObject * +table_wrapper_get_row_header( AtkTable *table, + gint row ) +{ + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + { + uno::Reference< accessibility::XAccessibleTable > xRowHeaders( pTable->getAccessibleRowHeaders() ); + if( xRowHeaders.is() ) + return atk_object_wrapper_conditional_ref( xRowHeaders->getAccessibleCellAt( row, 0 ) ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleRowHeaders()" ); + } + + return nullptr; +} + +/*****************************************************************************/ + +static AtkObject * +table_wrapper_get_column_header( AtkTable *table, + gint column ) +{ + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + { + uno::Reference< accessibility::XAccessibleTable > xColumnHeaders( pTable->getAccessibleColumnHeaders() ); + if( xColumnHeaders.is() ) + return atk_object_wrapper_conditional_ref( xColumnHeaders->getAccessibleCellAt( 0, column ) ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleColumnHeaders()" ); + } + + return nullptr; +} + +/*****************************************************************************/ + +static AtkObject * +table_wrapper_get_summary( AtkTable *table ) +{ + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + { + return atk_object_wrapper_conditional_ref( pTable->getAccessibleSummary() ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleSummary()" ); + } + + return nullptr; +} + +/*****************************************************************************/ + +static gint +convertToGIntArray( const uno::Sequence< ::sal_Int32 >& aSequence, gint **pSelected ) +{ + if( aSequence.hasElements() ) + { + *pSelected = g_new( gint, aSequence.getLength() ); + + *pSelected = comphelper::sequenceToArray(*pSelected, aSequence); + } + + return aSequence.getLength(); +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_selected_columns( AtkTable *table, + gint **pSelected ) +{ + *pSelected = nullptr; + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + return convertToGIntArray( pTable->getSelectedAccessibleColumns(), pSelected ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getSelectedAccessibleColumns()" ); + } + + return 0; +} + +/*****************************************************************************/ + +static gint +table_wrapper_get_selected_rows( AtkTable *table, + gint **pSelected ) +{ + *pSelected = nullptr; + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + return convertToGIntArray( pTable->getSelectedAccessibleRows(), pSelected ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getSelectedAccessibleRows()" ); + } + + return 0; +} + +/*****************************************************************************/ + +static gboolean +table_wrapper_is_column_selected( AtkTable *table, + gint column ) +{ + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + return pTable->isAccessibleColumnSelected( column ); + } + catch(const uno::Exception&) { + g_warning( "Exception in isAccessibleColumnSelected()" ); + } + + return 0; +} + +/*****************************************************************************/ + +static gboolean +table_wrapper_is_row_selected( AtkTable *table, + gint row ) +{ + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + return pTable->isAccessibleRowSelected( row ); + } + catch(const uno::Exception&) { + g_warning( "Exception in isAccessibleRowSelected()" ); + } + + return FALSE; +} + +/*****************************************************************************/ + +static gboolean +table_wrapper_is_selected( AtkTable *table, + gint row, + gint column ) +{ + try { + css::uno::Reference pTable + = getTable( table ); + if( pTable.is() ) + return pTable->isAccessibleSelected( row, column ); + } + catch(const uno::Exception&) { + g_warning( "Exception in isAccessibleSelected()" ); + } + + return FALSE; +} + +/*****************************************************************************/ + +static gboolean +table_wrapper_add_row_selection(AtkTable *pTable, gint row) +{ + try { + css::uno::Reference xTableSelection = getTableSelection(pTable); + if (xTableSelection.is()) + return xTableSelection->selectRow(row); + } + catch(const uno::Exception&) { + g_warning( "Exception in selectRow()" ); + } + + return false; +} + +/*****************************************************************************/ + +static gboolean +table_wrapper_remove_row_selection(AtkTable *pTable, gint row) +{ + try { + css::uno::Reference xTableSelection = getTableSelection(pTable); + if (xTableSelection.is()) + return xTableSelection->unselectRow(row); + } + catch(const uno::Exception&) { + g_warning( "Exception in unselectRow()" ); + } + + return false; +} + +/*****************************************************************************/ + +static gboolean +table_wrapper_add_column_selection(AtkTable *pTable, gint column) +{ + try { + css::uno::Reference xTableSelection = getTableSelection(pTable); + if (xTableSelection.is()) + return xTableSelection->selectColumn(column); + } + catch(const uno::Exception&) { + g_warning( "Exception in selectColumn()" ); + } + + return false; +} + +/*****************************************************************************/ + +static gboolean +table_wrapper_remove_column_selection(AtkTable *pTable, gint column) +{ + try { + css::uno::Reference xTableSelection = getTableSelection(pTable); + if (xTableSelection.is()) + return xTableSelection->unselectColumn(column); + } + catch(const uno::Exception&) { + g_warning( "Exception in unselectColumn()" ); + } + + return false; +} + +/*****************************************************************************/ + +static void +table_wrapper_set_caption( AtkTable *, AtkObject * ) +{ // meaningless helper +} + +/*****************************************************************************/ + +static void +table_wrapper_set_column_description( AtkTable *, gint, const gchar * ) +{ // meaningless helper +} + +/*****************************************************************************/ + +static void +table_wrapper_set_column_header( AtkTable *, gint, AtkObject * ) +{ // meaningless helper +} + +/*****************************************************************************/ + +static void +table_wrapper_set_row_description( AtkTable *, gint, const gchar * ) +{ // meaningless helper +} + +/*****************************************************************************/ + +static void +table_wrapper_set_row_header( AtkTable *, gint, AtkObject * ) +{ // meaningless helper +} + +/*****************************************************************************/ + +static void +table_wrapper_set_summary( AtkTable *, AtkObject * ) +{ // meaningless helper +} + +/*****************************************************************************/ + +} // extern "C" + +void +tableIfaceInit (AtkTableIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->ref_at = table_wrapper_ref_at; + iface->get_n_rows = table_wrapper_get_n_rows; + iface->get_n_columns = table_wrapper_get_n_columns; + iface->get_index_at = table_wrapper_get_index_at; + iface->get_column_at_index = table_wrapper_get_column_at_index; + iface->get_row_at_index = table_wrapper_get_row_at_index; + iface->is_row_selected = table_wrapper_is_row_selected; + iface->is_selected = table_wrapper_is_selected; + iface->get_selected_rows = table_wrapper_get_selected_rows; + iface->add_row_selection = table_wrapper_add_row_selection; + iface->remove_row_selection = table_wrapper_remove_row_selection; + iface->add_column_selection = table_wrapper_add_column_selection; + iface->remove_column_selection = table_wrapper_remove_column_selection; + iface->get_selected_columns = table_wrapper_get_selected_columns; + iface->is_column_selected = table_wrapper_is_column_selected; + iface->get_column_extent_at = table_wrapper_get_column_extent_at; + iface->get_row_extent_at = table_wrapper_get_row_extent_at; + iface->get_row_header = table_wrapper_get_row_header; + iface->set_row_header = table_wrapper_set_row_header; + iface->get_column_header = table_wrapper_get_column_header; + iface->set_column_header = table_wrapper_set_column_header; + iface->get_caption = table_wrapper_get_caption; + iface->set_caption = table_wrapper_set_caption; + iface->get_summary = table_wrapper_get_summary; + iface->set_summary = table_wrapper_set_summary; + iface->get_row_description = table_wrapper_get_row_description; + iface->set_row_description = table_wrapper_set_row_description; + iface->get_column_description = table_wrapper_get_column_description; + iface->set_column_description = table_wrapper_set_column_description; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atktablecell.cxx b/vcl/unx/gtk3/a11y/atktablecell.cxx new file mode 100644 index 0000000000..35d681b062 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atktablecell.cxx @@ -0,0 +1,269 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "atkwrapper.hxx" + +#include +#include +#include + +static css::uno::Reference +getContext(AtkTableCell* pTableCell) +{ + AtkObjectWrapper* pWrap = ATK_OBJECT_WRAPPER(pTableCell); + if (pWrap) + { + return pWrap->mpContext; + } + + return css::uno::Reference(); +} + +static css::uno::Reference +getTableParent(AtkTableCell* pTableCell) +{ + AtkObject* pParent = atk_object_get_parent(ATK_OBJECT(pTableCell)); + if (!pParent) + return css::uno::Reference(); + + AtkObjectWrapper* pWrap = ATK_OBJECT_WRAPPER(pParent); + if (pWrap) + { + if (!pWrap->mpTable.is()) + { + pWrap->mpTable.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpTable; + } + + return css::uno::Reference(); +} + +extern "C" { + +static int tablecell_wrapper_get_column_span(AtkTableCell* cell) +{ + int nColumnExtent = -1; + try + { + css::uno::Reference xContext = getContext(cell); + if (!xContext.is()) + return -1; + + css::uno::Reference xTable = getTableParent(cell); + if (xTable.is()) + { + const sal_Int64 nChildIndex = xContext->getAccessibleIndexInParent(); + const sal_Int32 nRow = xTable->getAccessibleRow(nChildIndex); + const sal_Int32 nColumn = xTable->getAccessibleColumn(nChildIndex); + nColumnExtent = xTable->getAccessibleColumnExtentAt(nRow, nColumn); + } + } + catch (const css::uno::Exception&) + { + g_warning("Exception in tablecell_wrapper_get_column_span"); + } + + return nColumnExtent; +} + +static GPtrArray* tablecell_wrapper_get_column_header_cells(AtkTableCell* cell) +{ + GPtrArray* pHeaderCells = g_ptr_array_new(); + try + { + css::uno::Reference xContext = getContext(cell); + if (!xContext.is()) + return pHeaderCells; + + css::uno::Reference xTable = getTableParent(cell); + if (xTable.is()) + { + const sal_Int64 nChildIndex = xContext->getAccessibleIndexInParent(); + const sal_Int32 nCol = xTable->getAccessibleColumn(nChildIndex); + css::uno::Reference xHeaders + = xTable->getAccessibleColumnHeaders(); + if (!xHeaders.is()) + return pHeaderCells; + + for (sal_Int32 nRow = 0; nRow < xHeaders->getAccessibleRowCount(); nRow++) + { + css::uno::Reference xCell + = xHeaders->getAccessibleCellAt(nRow, nCol); + AtkObject* pCell = atk_object_wrapper_ref(xCell); + g_ptr_array_add(pHeaderCells, pCell); + } + } + } + catch (const css::uno::Exception&) + { + g_warning("Exception in tablecell_wrapper_get_column_header_cells"); + } + + return pHeaderCells; +} + +static gboolean tablecell_wrapper_get_position(AtkTableCell* cell, gint* row, gint* column) +{ + try + { + css::uno::Reference xContext = getContext(cell); + if (!xContext.is()) + return false; + + css::uno::Reference xTable = getTableParent(cell); + if (xTable.is()) + { + const sal_Int64 nChildIndex = xContext->getAccessibleIndexInParent(); + *row = xTable->getAccessibleRow(nChildIndex); + *column = xTable->getAccessibleColumn(nChildIndex); + return true; + } + } + catch (const css::uno::Exception&) + { + g_warning("Exception in tablecell_wrapper_get_position()"); + } + + return false; +} + +static gint tablecell_wrapper_get_row_span(AtkTableCell* cell) +{ + int nRowExtent = -1; + try + { + css::uno::Reference xContext = getContext(cell); + if (!xContext.is()) + return -1; + + css::uno::Reference xTable = getTableParent(cell); + if (xTable.is()) + { + const sal_Int64 nChildIndex = xContext->getAccessibleIndexInParent(); + const sal_Int32 nRow = xTable->getAccessibleRow(nChildIndex); + const sal_Int32 nColumn = xTable->getAccessibleColumn(nChildIndex); + nRowExtent = xTable->getAccessibleRowExtentAt(nRow, nColumn); + } + } + catch (const css::uno::Exception&) + { + g_warning("Exception in tablecell_wrapper_get_row_span"); + } + + return nRowExtent; +} + +static GPtrArray* tablecell_wrapper_get_row_header_cells(AtkTableCell* cell) +{ + GPtrArray* pHeaderCells = g_ptr_array_new(); + try + { + css::uno::Reference xContext = getContext(cell); + if (!xContext.is()) + return pHeaderCells; + + css::uno::Reference xTable = getTableParent(cell); + if (xTable.is()) + { + const sal_Int64 nChildIndex = xContext->getAccessibleIndexInParent(); + const sal_Int32 nRow = xTable->getAccessibleRow(nChildIndex); + css::uno::Reference xHeaders + = xTable->getAccessibleRowHeaders(); + if (!xHeaders.is()) + return pHeaderCells; + + for (sal_Int32 nCol = 0; nCol < xHeaders->getAccessibleColumnCount(); nCol++) + { + css::uno::Reference xCell + = xHeaders->getAccessibleCellAt(nRow, nCol); + AtkObject* pCell = atk_object_wrapper_ref(xCell); + g_ptr_array_add(pHeaderCells, pCell); + } + } + } + catch (const css::uno::Exception&) + { + g_warning("Exception in tablecell_wrapper_get_row_header_cells"); + } + + return pHeaderCells; +} + +static gboolean tablecell_wrapper_get_row_column_span(AtkTableCell* cell, gint* row, gint* column, + gint* row_span, gint* column_span) +{ + try + { + css::uno::Reference xContext = getContext(cell); + if (!xContext.is()) + return -1; + + css::uno::Reference xTable = getTableParent(cell); + if (xTable.is()) + { + const sal_Int64 nChildIndex = xContext->getAccessibleIndexInParent(); + const sal_Int32 nRow = xTable->getAccessibleRow(nChildIndex); + const sal_Int32 nColumn = xTable->getAccessibleColumn(nChildIndex); + *row = nRow; + *column = nColumn; + *row_span = xTable->getAccessibleRowExtentAt(nRow, nColumn); + *column_span = xTable->getAccessibleColumnExtentAt(nRow, nColumn); + return true; + } + } + catch (const css::uno::Exception&) + { + g_warning("Exception in tablecell_wrapper_get_row_column_span"); + } + + return false; +} + +static AtkObject* tablecell_wrapper_get_table(AtkTableCell* cell) +{ + try + { + css::uno::Reference xContext = getContext(cell); + if (!xContext.is()) + return nullptr; + + css::uno::Reference xParent + = getContext(cell)->getAccessibleParent(); + if (!xParent.is()) + return nullptr; + + return atk_object_wrapper_ref(xParent); + } + + catch (const css::uno::Exception&) + { + g_warning("Exception in tablecell_wrapper_get_table()"); + } + + return nullptr; +} + +} // extern "C" + +void tablecellIfaceInit(AtkTableCellIface* iface) +{ + g_return_if_fail(iface != nullptr); + + iface->get_column_span = tablecell_wrapper_get_column_span; + iface->get_column_header_cells = tablecell_wrapper_get_column_header_cells; + iface->get_position = tablecell_wrapper_get_position; + iface->get_row_span = tablecell_wrapper_get_row_span; + iface->get_row_header_cells = tablecell_wrapper_get_row_header_cells; + iface->get_row_column_span = tablecell_wrapper_get_row_column_span; + iface->get_table = tablecell_wrapper_get_table; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atktext.cxx b/vcl/unx/gtk3/a11y/atktext.cxx new file mode 100644 index 0000000000..8b1ab67422 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atktext.cxx @@ -0,0 +1,881 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkwrapper.hxx" +#include "atktextattributes.hxx" +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ::com::sun::star; + +static sal_Int16 +text_type_from_boundary(AtkTextBoundary boundary_type) +{ + switch(boundary_type) + { + case ATK_TEXT_BOUNDARY_CHAR: + return accessibility::AccessibleTextType::CHARACTER; + case ATK_TEXT_BOUNDARY_WORD_START: + case ATK_TEXT_BOUNDARY_WORD_END: + return accessibility::AccessibleTextType::WORD; + case ATK_TEXT_BOUNDARY_SENTENCE_START: + case ATK_TEXT_BOUNDARY_SENTENCE_END: + return accessibility::AccessibleTextType::SENTENCE; + case ATK_TEXT_BOUNDARY_LINE_START: + case ATK_TEXT_BOUNDARY_LINE_END: + return accessibility::AccessibleTextType::LINE; + default: + return -1; + } +} + +/*****************************************************************************/ + +#if ATK_CHECK_VERSION(2,32,0) +static accessibility::AccessibleScrollType +scroll_type_from_scroll_type(AtkScrollType type) +{ + switch(type) + { + case ATK_SCROLL_TOP_LEFT: + return accessibility::AccessibleScrollType_SCROLL_TOP_LEFT; + case ATK_SCROLL_BOTTOM_RIGHT: + return accessibility::AccessibleScrollType_SCROLL_BOTTOM_RIGHT; + case ATK_SCROLL_TOP_EDGE: + return accessibility::AccessibleScrollType_SCROLL_TOP_EDGE; + case ATK_SCROLL_BOTTOM_EDGE: + return accessibility::AccessibleScrollType_SCROLL_BOTTOM_EDGE; + case ATK_SCROLL_LEFT_EDGE: + return accessibility::AccessibleScrollType_SCROLL_LEFT_EDGE; + case ATK_SCROLL_RIGHT_EDGE: + return accessibility::AccessibleScrollType_SCROLL_RIGHT_EDGE; + case ATK_SCROLL_ANYWHERE: + return accessibility::AccessibleScrollType_SCROLL_ANYWHERE; + default: + throw lang::NoSupportException(); + } +} +#endif + +/*****************************************************************************/ + +static gchar * +adjust_boundaries( css::uno::Reference const & pText, + accessibility::TextSegment const & rTextSegment, + AtkTextBoundary boundary_type, + gint * start_offset, gint * end_offset ) +{ + accessibility::TextSegment aTextSegment; + OUString aString; + gint start = 0, end = 0; + + if( !rTextSegment.SegmentText.isEmpty() ) + { + switch(boundary_type) + { + case ATK_TEXT_BOUNDARY_CHAR: + if ((rTextSegment.SegmentEnd - rTextSegment.SegmentStart) == 1 + && rtl::isSurrogate(rTextSegment.SegmentText[0])) + return nullptr; + [[fallthrough]]; + case ATK_TEXT_BOUNDARY_LINE_START: + case ATK_TEXT_BOUNDARY_LINE_END: + case ATK_TEXT_BOUNDARY_SENTENCE_START: + start = rTextSegment.SegmentStart; + end = rTextSegment.SegmentEnd; + aString = rTextSegment.SegmentText; + break; + + // the OOo break iterator behaves as SENTENCE_START + case ATK_TEXT_BOUNDARY_SENTENCE_END: + start = rTextSegment.SegmentStart; + end = rTextSegment.SegmentEnd; + + if( start > 0 ) + --start; + if( end > 0 && end < pText->getCharacterCount() - 1 ) + --end; + + aString = pText->getTextRange(start, end); + break; + + case ATK_TEXT_BOUNDARY_WORD_START: + start = rTextSegment.SegmentStart; + + // Determine the start index of the next segment + aTextSegment = pText->getTextBehindIndex(rTextSegment.SegmentEnd, + text_type_from_boundary(boundary_type)); + if( !aTextSegment.SegmentText.isEmpty() ) + end = aTextSegment.SegmentStart; + else + end = pText->getCharacterCount(); + + aString = pText->getTextRange(start, end); + break; + + case ATK_TEXT_BOUNDARY_WORD_END: + end = rTextSegment.SegmentEnd; + + // Determine the end index of the previous segment + aTextSegment = pText->getTextBeforeIndex(rTextSegment.SegmentStart, + text_type_from_boundary(boundary_type)); + if( !aTextSegment.SegmentText.isEmpty() ) + start = aTextSegment.SegmentEnd; + else + start = 0; + + aString = pText->getTextRange(start, end); + break; + + default: + return nullptr; + } + } + + *start_offset = start; + *end_offset = end; + + return OUStringToGChar(aString); +} + +/*****************************************************************************/ + +/// @throws uno::RuntimeException +static css::uno::Reference + getText( AtkText *pText ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pText ); + if( pWrap ) + { + if( !pWrap->mpText.is() ) + { + pWrap->mpText.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpText; + } + + return css::uno::Reference(); +} + +/*****************************************************************************/ + +/// @throws uno::RuntimeException +static css::uno::Reference + getTextMarkup( AtkText *pText ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pText ); + if( pWrap ) + { + if( !pWrap->mpTextMarkup.is() ) + { + pWrap->mpTextMarkup.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpTextMarkup; + } + + return css::uno::Reference(); +} + +/*****************************************************************************/ + +/// @throws uno::RuntimeException +static css::uno::Reference + getTextAttributes( AtkText *pText ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pText ); + if( pWrap ) + { + if( !pWrap->mpTextAttributes.is() ) + { + pWrap->mpTextAttributes.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpTextAttributes; + } + + return css::uno::Reference(); +} + +/*****************************************************************************/ + +/// @throws uno::RuntimeException +static css::uno::Reference + getMultiLineText( AtkText *pText ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pText ); + if( pWrap ) + { + if( !pWrap->mpMultiLineText.is() ) + { + pWrap->mpMultiLineText.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpMultiLineText; + } + + return css::uno::Reference(); +} + +/*****************************************************************************/ + +extern "C" { + +static gchar * +text_wrapper_get_text (AtkText *text, + gint start_offset, + gint end_offset) +{ + gchar * ret = nullptr; + + g_return_val_if_fail( (end_offset == -1) || (end_offset >= start_offset), nullptr ); + + try { + css::uno::Reference pText + = getText( text ); + if( pText.is() ) + { + OUString aText; + sal_Int32 n = pText->getCharacterCount(); + + if( start_offset < n ) + { + if( -1 == end_offset ) + aText = pText->getTextRange(start_offset, n - start_offset); + else + aText = pText->getTextRange(start_offset, end_offset); + } + + ret = g_strdup( OUStringToOString(aText, RTL_TEXTENCODING_UTF8 ).getStr() ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getText()" ); + } + + return ret; +} + +static gchar * +text_wrapper_get_text_after_offset (AtkText *text, + gint offset, + AtkTextBoundary boundary_type, + gint *start_offset, + gint *end_offset) +{ + try { + css::uno::Reference pText + = getText( text ); + if( pText.is() ) + { + accessibility::TextSegment aTextSegment = pText->getTextBehindIndex(offset, text_type_from_boundary(boundary_type)); + return adjust_boundaries(pText, aTextSegment, boundary_type, start_offset, end_offset); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in get_text_after_offset()" ); + } + + return nullptr; +} + +static gchar * +text_wrapper_get_text_at_offset (AtkText *text, + gint offset, + AtkTextBoundary boundary_type, + gint *start_offset, + gint *end_offset) +{ + try { + css::uno::Reference pText + = getText( text ); + if( pText.is() ) + { + /* If the user presses the 'End' key, the caret will be placed behind the last character, + * which is the same index as the first character of the next line. In atk the magic offset + * '-2' is used to cover this special case. + */ + if ( + -2 == offset && + (ATK_TEXT_BOUNDARY_LINE_START == boundary_type || + ATK_TEXT_BOUNDARY_LINE_END == boundary_type) + ) + { + css::uno::Reference< + css::accessibility::XAccessibleMultiLineText> pMultiLineText + = getMultiLineText( text ); + if( pMultiLineText.is() ) + { + accessibility::TextSegment aTextSegment = pMultiLineText->getTextAtLineWithCaret(); + return adjust_boundaries(pText, aTextSegment, boundary_type, start_offset, end_offset); + } + } + + accessibility::TextSegment aTextSegment = pText->getTextAtIndex(offset, text_type_from_boundary(boundary_type)); + return adjust_boundaries(pText, aTextSegment, boundary_type, start_offset, end_offset); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in get_text_at_offset()" ); + } + + return nullptr; +} + +static gunichar +text_wrapper_get_character_at_offset (AtkText *text, + gint offset) +{ + gint start, end; + gunichar uc = 0xFFFFFFFF; + + gchar * char_as_string = + text_wrapper_get_text_at_offset(text, offset, ATK_TEXT_BOUNDARY_CHAR, + &start, &end); + if( char_as_string ) + { + uc = g_utf8_get_char( char_as_string ); + g_free( char_as_string ); + } + + return uc; +} + +static gchar * +text_wrapper_get_text_before_offset (AtkText *text, + gint offset, + AtkTextBoundary boundary_type, + gint *start_offset, + gint *end_offset) +{ + try { + css::uno::Reference pText + = getText( text ); + if( pText.is() ) + { + accessibility::TextSegment aTextSegment = pText->getTextBeforeIndex(offset, text_type_from_boundary(boundary_type)); + return adjust_boundaries(pText, aTextSegment, boundary_type, start_offset, end_offset); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in text_before_offset()" ); + } + + return nullptr; +} + +static gint +text_wrapper_get_caret_offset (AtkText *text) +{ + gint offset = -1; + + try { + css::uno::Reference pText + = getText( text ); + if( pText.is() ) + offset = pText->getCaretPosition(); + } + catch(const uno::Exception&) { + g_warning( "Exception in getCaretPosition()" ); + } + + return offset; +} + +static gboolean +text_wrapper_set_caret_offset (AtkText *text, + gint offset) +{ + try { + css::uno::Reference pText + = getText( text ); + if( pText.is() ) + return pText->setCaretPosition( offset ); + } + catch(const uno::Exception&) { + g_warning( "Exception in setCaretPosition()" ); + } + + return FALSE; +} + +// #i92232# +static AtkAttributeSet* +handle_text_markup_as_run_attribute( css::uno::Reference const & pTextMarkup, + const gint nTextMarkupType, + const gint offset, + AtkAttributeSet* pSet, + gint *start_offset, + gint *end_offset ) +{ + const gint nTextMarkupCount( pTextMarkup->getTextMarkupCount( nTextMarkupType ) ); + for ( gint nTextMarkupIndex = 0; + nTextMarkupIndex < nTextMarkupCount; + ++nTextMarkupIndex ) + { + accessibility::TextSegment aTextSegment = + pTextMarkup->getTextMarkup( nTextMarkupIndex, nTextMarkupType ); + const gint nStartOffsetTextMarkup = aTextSegment.SegmentStart; + const gint nEndOffsetTextMarkup = aTextSegment.SegmentEnd; + if ( nStartOffsetTextMarkup <= offset ) + { + if ( offset < nEndOffsetTextMarkup ) + { + // text markup at + *start_offset = ::std::max( *start_offset, + nStartOffsetTextMarkup ); + *end_offset = ::std::min( *end_offset, + nEndOffsetTextMarkup ); + switch ( nTextMarkupType ) + { + case css::text::TextMarkupType::SPELLCHECK: + { + pSet = attribute_set_prepend_misspelled( pSet ); + } + break; + case css::text::TextMarkupType::TRACK_CHANGE_INSERTION: + { + pSet = attribute_set_prepend_tracked_change_insertion( pSet ); + } + break; + case css::text::TextMarkupType::TRACK_CHANGE_DELETION: + { + pSet = attribute_set_prepend_tracked_change_deletion( pSet ); + } + break; + case css::text::TextMarkupType::TRACK_CHANGE_FORMATCHANGE: + { + pSet = attribute_set_prepend_tracked_change_formatchange( pSet ); + } + break; + default: + { + OSL_ASSERT( false ); + } + } + break; // no further iteration needed. + } + else + { + *start_offset = ::std::max( *start_offset, + nEndOffsetTextMarkup ); + // continue iteration. + } + } + else + { + *end_offset = ::std::min( *end_offset, + nStartOffsetTextMarkup ); + break; // no further iteration. + } + } + + return pSet; +} + +static AtkAttributeSet * +text_wrapper_get_run_attributes( AtkText *text, + gint offset, + gint *start_offset, + gint *end_offset) +{ + AtkAttributeSet *pSet = nullptr; + + try { + bool bOffsetsAreValid = false; + + css::uno::Reference pText + = getText( text ); + if( pText.is()) + { + uno::Sequence< beans::PropertyValue > aAttributeList; + + css::uno::Reference + pTextAttributes = getTextAttributes( text ); + if(pTextAttributes.is()) // Text attributes are available for paragraphs only + { + aAttributeList = pTextAttributes->getRunAttributes( offset, uno::Sequence< OUString > () ); + } + else // For other text objects use character attributes + { + aAttributeList = pText->getCharacterAttributes( offset, uno::Sequence< OUString > () ); + } + + pSet = attribute_set_new_from_property_values( aAttributeList, true, text ); + // #i100938# + // - always provide start_offset and end_offset + { + accessibility::TextSegment aTextSegment = + pText->getTextAtIndex(offset, accessibility::AccessibleTextType::ATTRIBUTE_RUN); + + *start_offset = aTextSegment.SegmentStart; + // #i100938# + // Do _not_ increment the end_offset provide by instance + *end_offset = aTextSegment.SegmentEnd; + bOffsetsAreValid = true; + } + } + + // Special handling for misspelled text + // #i92232# + // - add special handling for tracked changes and refactor the + // corresponding code for handling misspelled text. + css::uno::Reference + pTextMarkup = getTextMarkup( text ); + if( pTextMarkup.is() ) + { + // Get attribute run here if it hasn't been done before + if (!bOffsetsAreValid && pText.is()) + { + accessibility::TextSegment aAttributeTextSegment = + pText->getTextAtIndex(offset, accessibility::AccessibleTextType::ATTRIBUTE_RUN); + *start_offset = aAttributeTextSegment.SegmentStart; + *end_offset = aAttributeTextSegment.SegmentEnd; + } + // handle misspelled text + pSet = handle_text_markup_as_run_attribute( + pTextMarkup, + css::text::TextMarkupType::SPELLCHECK, + offset, pSet, start_offset, end_offset ); + // handle tracked changes + pSet = handle_text_markup_as_run_attribute( + pTextMarkup, + css::text::TextMarkupType::TRACK_CHANGE_INSERTION, + offset, pSet, start_offset, end_offset ); + pSet = handle_text_markup_as_run_attribute( + pTextMarkup, + css::text::TextMarkupType::TRACK_CHANGE_DELETION, + offset, pSet, start_offset, end_offset ); + pSet = handle_text_markup_as_run_attribute( + pTextMarkup, + css::text::TextMarkupType::TRACK_CHANGE_FORMATCHANGE, + offset, pSet, start_offset, end_offset ); + } + } + catch(const uno::Exception&){ + + g_warning( "Exception in get_run_attributes()" ); + + if( pSet ) + { + atk_attribute_set_free( pSet ); + pSet = nullptr; + } + } + + return pSet; +} + +/*****************************************************************************/ + +static AtkAttributeSet * +text_wrapper_get_default_attributes( AtkText *text ) +{ + AtkAttributeSet *pSet = nullptr; + + try { + css::uno::Reference + pTextAttributes = getTextAttributes( text ); + if( pTextAttributes.is() ) + { + uno::Sequence< beans::PropertyValue > aAttributeList = + pTextAttributes->getDefaultAttributes( uno::Sequence< OUString > () ); + + pSet = attribute_set_new_from_property_values( aAttributeList, false, text ); + } + } + catch(const uno::Exception&) { + + g_warning( "Exception in get_default_attributes()" ); + + if( pSet ) + { + atk_attribute_set_free( pSet ); + pSet = nullptr; + } + } + + return pSet; +} + +/*****************************************************************************/ + +static void +text_wrapper_get_character_extents( AtkText *text, + gint offset, + gint *x, + gint *y, + gint *width, + gint *height, + AtkCoordType coords ) +{ + *x = *y = *width = *height = -1; + + try { + css::uno::Reference pText + = getText( text ); + if( pText.is() ) + { + awt::Rectangle aRect = pText->getCharacterBounds( offset ); + + gint origin_x = 0; + gint origin_y = 0; + + if (coords == ATK_XY_SCREEN || coords == ATK_XY_WINDOW) + { + g_return_if_fail( ATK_IS_COMPONENT( text ) ); + gint nWidth = -1; + gint nHeight = -1; + atk_component_get_extents(ATK_COMPONENT(text), &origin_x, &origin_y, &nWidth, &nHeight, coords); + } + + *x = aRect.X + origin_x; + *y = aRect.Y + origin_y; + *width = aRect.Width; + *height = aRect.Height; + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getCharacterBounds" ); + } +} + +static gint +text_wrapper_get_character_count (AtkText *text) +{ + gint rv = 0; + + try { + css::uno::Reference pText + = getText( text ); + if( pText.is() ) + rv = pText->getCharacterCount(); + } + catch(const uno::Exception&) { + g_warning( "Exception in getCharacterCount" ); + } + + return rv; +} + +static gint +text_wrapper_get_offset_at_point (AtkText *text, + gint x, + gint y, + AtkCoordType coords) +{ + try { + css::uno::Reference pText + = getText( text ); + if( pText.is() ) + { + gint origin_x = 0; + gint origin_y = 0; + + if (coords == ATK_XY_SCREEN || coords == ATK_XY_WINDOW) + { + g_return_val_if_fail( ATK_IS_COMPONENT( text ), -1 ); + gint nWidth = -1; + gint nHeight = -1; + atk_component_get_extents(ATK_COMPONENT(text), &origin_x, &origin_y, &nWidth, &nHeight, coords); + } + + return pText->getIndexAtPoint( awt::Point(x - origin_x, y - origin_y) ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getIndexAtPoint" ); + } + + return -1; +} + +// FIXME: the whole series of selections API is problematic ... + +static gint +text_wrapper_get_n_selections (AtkText *text) +{ + gint rv = 0; + + try { + css::uno::Reference pText + = getText( text ); + if( pText.is() ) + rv = ( pText->getSelectionEnd() > pText->getSelectionStart() ) ? 1 : 0; + } + catch(const uno::Exception&) { + g_warning( "Exception in getSelectionEnd() or getSelectionStart()" ); + } + + return rv; +} + +static gchar * +text_wrapper_get_selection (AtkText *text, + gint selection_num, + gint *start_offset, + gint *end_offset) +{ + g_return_val_if_fail( selection_num == 0, FALSE ); + + try { + css::uno::Reference pText + = getText( text ); + if( pText.is() ) + { + *start_offset = pText->getSelectionStart(); + *end_offset = pText->getSelectionEnd(); + + return OUStringToGChar( pText->getSelectedText() ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getSelectionEnd(), getSelectionStart() or getSelectedText()" ); + } + + return nullptr; +} + +static gboolean +text_wrapper_add_selection (AtkText *text, + gint start_offset, + gint end_offset) +{ + // FIXME: can we try to be more compatible by expanding an + // existing adjacent selection ? + + try { + css::uno::Reference pText + = getText( text ); + if( pText.is() ) + return pText->setSelection( start_offset, end_offset ); // ? + } + catch(const uno::Exception&) { + g_warning( "Exception in setSelection()" ); + } + + return FALSE; +} + +static gboolean +text_wrapper_remove_selection (AtkText *text, + gint selection_num) +{ + g_return_val_if_fail( selection_num == 0, FALSE ); + + try { + css::uno::Reference pText + = getText( text ); + if( pText.is() ) + return pText->setSelection( 0, 0 ); // ? + } + catch(const uno::Exception&) { + g_warning( "Exception in setSelection()" ); + } + + return FALSE; +} + +static gboolean +text_wrapper_set_selection (AtkText *text, + gint selection_num, + gint start_offset, + gint end_offset) +{ + g_return_val_if_fail( selection_num == 0, FALSE ); + + try { + css::uno::Reference pText + = getText( text ); + if( pText.is() ) + return pText->setSelection( start_offset, end_offset ); + } + catch(const uno::Exception&) { + g_warning( "Exception in setSelection()" ); + } + + return FALSE; +} + +#if ATK_CHECK_VERSION(2,32,0) +static gboolean +text_wrapper_scroll_substring_to(AtkText *text, + gint start_offset, + gint end_offset, + AtkScrollType scroll_type) +{ + try { + css::uno::Reference pText + = getText( text ); + + if( pText.is() ) + return pText->scrollSubstringTo( start_offset, end_offset, + scroll_type_from_scroll_type( scroll_type ) ); + } + catch(const uno::Exception&) { + g_warning( "Exception in scrollSubstringTo()" ); + } + + return FALSE; +} +#endif + +} // extern "C" + +void +textIfaceInit (AtkTextIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->get_text = text_wrapper_get_text; + iface->get_character_at_offset = text_wrapper_get_character_at_offset; + iface->get_text_before_offset = text_wrapper_get_text_before_offset; + iface->get_text_at_offset = text_wrapper_get_text_at_offset; + iface->get_text_after_offset = text_wrapper_get_text_after_offset; + iface->get_caret_offset = text_wrapper_get_caret_offset; + iface->set_caret_offset = text_wrapper_set_caret_offset; + iface->get_character_count = text_wrapper_get_character_count; + iface->get_n_selections = text_wrapper_get_n_selections; + iface->get_selection = text_wrapper_get_selection; + iface->add_selection = text_wrapper_add_selection; + iface->remove_selection = text_wrapper_remove_selection; + iface->set_selection = text_wrapper_set_selection; + iface->get_run_attributes = text_wrapper_get_run_attributes; + iface->get_default_attributes = text_wrapper_get_default_attributes; + iface->get_character_extents = text_wrapper_get_character_extents; + iface->get_offset_at_point = text_wrapper_get_offset_at_point; +#if ATK_CHECK_VERSION(2,32,0) + iface->scroll_substring_to = text_wrapper_scroll_substring_to; +#endif +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atktextattributes.cxx b/vcl/unx/gtk3/a11y/atktextattributes.cxx new file mode 100644 index 0000000000..25b43f480a --- /dev/null +++ b/vcl/unx/gtk3/a11y/atktextattributes.cxx @@ -0,0 +1,1388 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atktextattributes.hxx" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include "atkwrapper.hxx" + +#include + +#include +#include +#include +#include + +#include +#include + +using namespace ::com::sun::star; + +typedef gchar* (* AtkTextAttrFunc) ( const uno::Any& rAny ); +typedef bool (* TextPropertyValueFunc) ( uno::Any& rAny, const gchar * value ); + +#define STRNCMP_PARAM( s ) s,sizeof( s )-1 + +/*****************************************************************************/ + +static AtkTextAttribute atk_text_attribute_paragraph_style = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_font_effect = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_decoration = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_line_height = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_rotation = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_shadow = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_tab_interval = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_tab_stops = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_writing_mode = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_vertical_align = ATK_TEXT_ATTR_INVALID; +static AtkTextAttribute atk_text_attribute_misspelled = ATK_TEXT_ATTR_INVALID; +// #i92232# +static AtkTextAttribute atk_text_attribute_tracked_change = ATK_TEXT_ATTR_INVALID; +// #i92233# +static AtkTextAttribute atk_text_attribute_mm_to_pixel_ratio = ATK_TEXT_ATTR_INVALID; + +/*****************************************************************************/ + +/** + * !! IMPORTANT NOTE !! : when adding items to this list, KEEP THE LIST SORTED + * and re-arrange the enum values accordingly. + */ + +namespace { + +enum ExportedAttribute +{ + TEXT_ATTRIBUTE_BACKGROUND_COLOR = 0, + TEXT_ATTRIBUTE_CASEMAP, + TEXT_ATTRIBUTE_FOREGROUND_COLOR, + TEXT_ATTRIBUTE_CONTOURED, + TEXT_ATTRIBUTE_CHAR_ESCAPEMENT, + TEXT_ATTRIBUTE_BLINKING, + TEXT_ATTRIBUTE_FONT_NAME, + TEXT_ATTRIBUTE_HEIGHT, + TEXT_ATTRIBUTE_HIDDEN, + TEXT_ATTRIBUTE_KERNING, + TEXT_ATTRIBUTE_LOCALE, + TEXT_ATTRIBUTE_POSTURE, + TEXT_ATTRIBUTE_RELIEF, + TEXT_ATTRIBUTE_ROTATION, + TEXT_ATTRIBUTE_SCALE, + TEXT_ATTRIBUTE_SHADOWED, + TEXT_ATTRIBUTE_STRIKETHROUGH, + TEXT_ATTRIBUTE_UNDERLINE, + TEXT_ATTRIBUTE_WEIGHT, + // #i92233# + TEXT_ATTRIBUTE_MM_TO_PIXEL_RATIO, + TEXT_ATTRIBUTE_JUSTIFICATION, + TEXT_ATTRIBUTE_BOTTOM_MARGIN, + TEXT_ATTRIBUTE_FIRST_LINE_INDENT, + TEXT_ATTRIBUTE_LEFT_MARGIN, + TEXT_ATTRIBUTE_LINE_SPACING, + TEXT_ATTRIBUTE_RIGHT_MARGIN, + TEXT_ATTRIBUTE_STYLE_NAME, + TEXT_ATTRIBUTE_TAB_STOPS, + TEXT_ATTRIBUTE_TOP_MARGIN, + TEXT_ATTRIBUTE_WRITING_MODE, + TEXT_ATTRIBUTE_LAST +}; + +} + +static const char * ExportedTextAttributes[TEXT_ATTRIBUTE_LAST] = +{ + "CharBackColor", // TEXT_ATTRIBUTE_BACKGROUND_COLOR + "CharCaseMap", // TEXT_ATTRIBUTE_CASEMAP + "CharColor", // TEXT_ATTRIBUTE_FOREGROUND_COLOR + "CharContoured", // TEXT_ATTRIBUTE_CONTOURED + "CharEscapement", // TEXT_ATTRIBUTE_CHAR_ESCAPEMENT + "CharFlash", // TEXT_ATTRIBUTE_BLINKING + "CharFontName", // TEXT_ATTRIBUTE_FONT_NAME + "CharHeight", // TEXT_ATTRIBUTE_HEIGHT + "CharHidden", // TEXT_ATTRIBUTE_HIDDEN + "CharKerning", // TEXT_ATTRIBUTE_KERNING + "CharLocale", // TEXT_ATTRIBUTE_LOCALE + "CharPosture", // TEXT_ATTRIBUTE_POSTURE + "CharRelief", // TEXT_ATTRIBUTE_RELIEF + "CharRotation", // TEXT_ATTRIBUTE_ROTATION + "CharScaleWidth", // TEXT_ATTRIBUTE_SCALE + "CharShadowed", // TEXT_ATTRIBUTE_SHADOWED + "CharStrikeout", // TEXT_ATTRIBUTE_STRIKETHROUGH + "CharUnderline", // TEXT_ATTRIBUTE_UNDERLINE + "CharWeight", // TEXT_ATTRIBUTE_WEIGHT + // #i92233# + "MMToPixelRatio", // TEXT_ATTRIBUTE_MM_TO_PIXEL_RATIO + "ParaAdjust", // TEXT_ATTRIBUTE_JUSTIFICATION + "ParaBottomMargin", // TEXT_ATTRIBUTE_BOTTOM_MARGIN + "ParaFirstLineIndent", // TEXT_ATTRIBUTE_FIRST_LINE_INDENT + "ParaLeftMargin", // TEXT_ATTRIBUTE_LEFT_MARGIN + "ParaLineSpacing", // TEXT_ATTRIBUTE_LINE_SPACING + "ParaRightMargin", // TEXT_ATTRIBUTE_RIGHT_MARGIN + "ParaStyleName", // TEXT_ATTRIBUTE_STYLE_NAME + "ParaTabStops", // TEXT_ATTRIBUTE_TAB_STOPS + "ParaTopMargin", // TEXT_ATTRIBUTE_TOP_MARGIN + "WritingMode" // TEXT_ATTRIBUTE_WRITING_MODE +}; + +/*****************************************************************************/ + +static gchar* +get_value( const uno::Sequence< beans::PropertyValue >& rAttributeList, + sal_Int32 nIndex, AtkTextAttrFunc func ) +{ + if( nIndex != -1 ) + return func(rAttributeList[nIndex].Value); + + return nullptr; +} + +#define get_bool_value( list, index ) get_value( list, index, Bool2String ) +#define get_height_value( list, index ) get_value( list, index, Float2String ) +#define get_justification_value( list, index ) get_value( list, index, Adjust2Justification ) +#define get_cmm_value( list, index ) get_value( list, index, CMM2UnitString ) +#define get_scale_width( list, index ) get_value( list, index, Scale2String ) +#define get_strikethrough_value( list, index ) get_value( list, index, Strikeout2String ) +#define get_string_value( list, index ) get_value( list, index, GetString ) +#define get_style_value( list, index ) get_value( list, index, FontSlant2Style ) +#define get_underline_value( list, index ) get_value( list, index, Underline2String ) +#define get_variant_value( list, index ) get_value( list, index, CaseMap2String ) +#define get_weight_value( list, index ) get_value( list, index, Weight2String ) +#define get_language_string( list, index ) get_value( list, index, Locale2String ) + +/*****************************************************************************/ + +static bool +InvalidValue( uno::Any&, const gchar * ) +{ + return false; +} + +/*****************************************************************************/ + +static gchar* +Float2String(const uno::Any& rAny) +{ + return g_strdup_printf( "%g", rAny.get() ); +} + +static bool +String2Float( uno::Any& rAny, const gchar * value ) +{ + float fval; + + if( 1 != sscanf( value, "%g", &fval ) ) + return false; + + rAny <<= fval; + return true; +} + +/*****************************************************************************/ + +/// @throws uno::RuntimeException +static css::uno::Reference + getComponent( AtkText *pText ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pText ); + if( pWrap ) + { + if( !pWrap->mpComponent.is() ) + { + pWrap->mpComponent.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpComponent; + } + + return css::uno::Reference(); +} + +static gchar* +get_color_value(const uno::Sequence< beans::PropertyValue >& rAttributeList, + const sal_Int32 * pIndexArray, + ExportedAttribute attr, + AtkText * text) +{ + sal_Int32 nColor = -1; // AUTOMATIC + sal_Int32 nIndex = pIndexArray[attr]; + + if( nIndex != -1 ) + nColor = rAttributeList[nIndex].Value.get(); + + /* + * Check for color value for 100% alpha white, which means + * "automatic". Grab the RGB value from XAccessibleComponent + * in this case. + */ + + if( (nColor == -1) && text ) + { + try + { + css::uno::Reference + pComponent = getComponent( text ); + if( pComponent.is() ) + { + switch( attr ) + { + case TEXT_ATTRIBUTE_BACKGROUND_COLOR: + nColor = pComponent->getBackground(); + break; + case TEXT_ATTRIBUTE_FOREGROUND_COLOR: + nColor = pComponent->getForeground(); + break; + default: + break; + } + } + } + + catch(const uno::Exception&) { + g_warning( "Exception in get[Fore|Back]groundColor()" ); + } + } + + if( nColor != -1 ) + { + sal_uInt8 blue = nColor & 0xFF; + sal_uInt8 green = (nColor >> 8) & 0xFF; + sal_uInt8 red = (nColor >> 16) & 0xFF; + + return g_strdup_printf( "%u,%u,%u", red, green, blue ); + } + + return nullptr; +} + +static bool +String2Color( uno::Any& rAny, const gchar * value ) +{ + int red, green, blue; + + if( 3 != sscanf( value, "%d,%d,%d", &red, &green, &blue ) ) + return false; + + sal_Int32 nColor = static_cast(blue) | ( static_cast(green) << 8 ) | ( static_cast(red) << 16 ); + rAny <<= nColor; + return true; +} + +/*****************************************************************************/ + +static gchar* +FontSlant2Style(const uno::Any& rAny) +{ + const gchar * value = nullptr; + + awt::FontSlant aFontSlant; + if(!(rAny >>= aFontSlant)) + return nullptr; + + switch( aFontSlant ) + { + case awt::FontSlant_NONE: + value = "normal"; + break; + + case awt::FontSlant_OBLIQUE: + value = "oblique"; + break; + + case awt::FontSlant_ITALIC: + value = "italic"; + break; + + case awt::FontSlant_REVERSE_OBLIQUE: + value = "reverse oblique"; + break; + + case awt::FontSlant_REVERSE_ITALIC: + value = "reverse italic"; + break; + + default: + break; + } + + if( value ) + return g_strdup( value ); + + return nullptr; +} + +static bool +Style2FontSlant( uno::Any& rAny, const gchar * value ) +{ + awt::FontSlant aFontSlant; + + if( strncmp( value, STRNCMP_PARAM( "normal" ) ) == 0 ) + aFontSlant = awt::FontSlant_NONE; + else if( strncmp( value, STRNCMP_PARAM( "oblique" ) ) == 0 ) + aFontSlant = awt::FontSlant_OBLIQUE; + else if( strncmp( value, STRNCMP_PARAM( "italic" ) ) == 0 ) + aFontSlant = awt::FontSlant_ITALIC; + else if( strncmp( value, STRNCMP_PARAM( "reverse oblique" ) ) == 0 ) + aFontSlant = awt::FontSlant_REVERSE_OBLIQUE; + else if( strncmp( value, STRNCMP_PARAM( "reverse italic" ) ) == 0 ) + aFontSlant = awt::FontSlant_REVERSE_ITALIC; + else + return false; + + rAny <<= aFontSlant; + return true; +} + +/*****************************************************************************/ + +static gchar* +Weight2String(const uno::Any& rAny) +{ + return g_strdup_printf( "%g", rAny.get() * 4 ); +} + +static bool +String2Weight( uno::Any& rAny, const gchar * value ) +{ + float weight; + + if( 1 != sscanf( value, "%g", &weight ) ) + return false; + + rAny <<= weight / 4; + return true; +} + +/*****************************************************************************/ + +static gchar* +Adjust2Justification(const uno::Any& rAny) +{ + const gchar * value = nullptr; + + switch( static_cast(rAny.get()) ) + { + case style::ParagraphAdjust_LEFT: + value = "left"; + break; + + case style::ParagraphAdjust_RIGHT: + value = "right"; + break; + + case style::ParagraphAdjust_BLOCK: + case style::ParagraphAdjust_STRETCH: + value = "fill"; + break; + + case style::ParagraphAdjust_CENTER: + value = "center"; + break; + + default: + break; + } + + if( value ) + return g_strdup( value ); + + return nullptr; +} + +static bool +Justification2Adjust( uno::Any& rAny, const gchar * value ) +{ + style::ParagraphAdjust nParagraphAdjust; + + if( strncmp( value, STRNCMP_PARAM( "left" ) ) == 0 ) + nParagraphAdjust = style::ParagraphAdjust_LEFT; + else if( strncmp( value, STRNCMP_PARAM( "right" ) ) == 0 ) + nParagraphAdjust = style::ParagraphAdjust_RIGHT; + else if( strncmp( value, STRNCMP_PARAM( "fill" ) ) == 0 ) + nParagraphAdjust = style::ParagraphAdjust_BLOCK; + else if( strncmp( value, STRNCMP_PARAM( "center" ) ) == 0 ) + nParagraphAdjust = style::ParagraphAdjust_CENTER; + else + return false; + + rAny <<= static_cast(nParagraphAdjust); + return true; +} + +/*****************************************************************************/ + +const gchar * const font_strikethrough[] = { + "none", // FontStrikeout::NONE + "single", // FontStrikeout::SINGLE + "double", // FontStrikeout::DOUBLE + nullptr, // FontStrikeout::DONTKNOW + "bold", // FontStrikeout::BOLD + "with /", // FontStrikeout::SLASH + "with X" // FontStrikeout::X +}; + +static gchar* +Strikeout2String(const uno::Any& rAny) +{ + sal_Int16 n = rAny.get(); + + if( n >= 0 && o3tl::make_unsigned(n) < SAL_N_ELEMENTS(font_strikethrough) ) + return g_strdup( font_strikethrough[n] ); + + return nullptr; +} + +static bool +String2Strikeout( uno::Any& rAny, const gchar * value ) +{ + for( sal_Int16 n=0; n < sal_Int16(SAL_N_ELEMENTS(font_strikethrough)); ++n ) + { + if( ( nullptr != font_strikethrough[n] ) && + 0 == strncmp( value, font_strikethrough[n], strlen( font_strikethrough[n] ) ) ) + { + rAny <<= n; + return true; + } + } + + return false; +} + +/*****************************************************************************/ + +static gchar* +Underline2String(const uno::Any& rAny) +{ + const gchar * value = nullptr; + + switch( rAny.get() ) + { + case awt::FontUnderline::NONE: + value = "none"; + break; + + case awt::FontUnderline::SINGLE: + value = "single"; + break; + + case awt::FontUnderline::DOUBLE: + value = "double"; + break; + + default: + break; + } + + if( value ) + return g_strdup( value ); + + return nullptr; +} + +static bool +String2Underline( uno::Any& rAny, const gchar * value ) +{ + short nUnderline; + + if( strncmp( value, STRNCMP_PARAM( "none" ) ) == 0 ) + nUnderline = awt::FontUnderline::NONE; + else if( strncmp( value, STRNCMP_PARAM( "single" ) ) == 0 ) + nUnderline = awt::FontUnderline::SINGLE; + else if( strncmp( value, STRNCMP_PARAM( "double" ) ) == 0 ) + nUnderline = awt::FontUnderline::DOUBLE; + else + return false; + + rAny <<= nUnderline; + return true; +} + +/*****************************************************************************/ + +static gchar* +GetString(const uno::Any& rAny) +{ + OString aFontName = OUStringToOString( rAny.get< OUString > (), RTL_TEXTENCODING_UTF8 ); + + if( !aFontName.isEmpty() ) + return g_strdup( aFontName.getStr() ); + + return nullptr; +} + +static bool +SetString( uno::Any& rAny, const gchar * value ) +{ + OString aFontName( value ); + + if( !aFontName.isEmpty() ) + { + rAny <<= OStringToOUString( aFontName, RTL_TEXTENCODING_UTF8 ); + return true; + } + + return false; +} + +/*****************************************************************************/ + +// @see http://developer.gnome.org/doc/API/2.0/atk/AtkText.html#AtkTextAttribute + +// CMM = 100th of mm +static gchar* +CMM2UnitString(const uno::Any& rAny) +{ + double fValue = rAny.get(); + fValue = fValue * 0.01; + + return g_strdup_printf( "%gmm", fValue ); +} + +static bool +UnitString2CMM( uno::Any& rAny, const gchar * value ) +{ + float fValue = 0.0; // pb: don't use double here because of warning on linux + + if( 1 != sscanf( value, "%gmm", &fValue ) ) + return false; + + fValue = fValue * 100; + + rAny <<= static_cast(fValue); + return true; +} + +/*****************************************************************************/ + +static const gchar * bool_values[] = { "true", "false" }; + +static gchar * +Bool2String( const uno::Any& rAny ) +{ + int n = 1; + + if( rAny.get() ) + n = 0; + + return g_strdup( bool_values[n] ); +} + +static bool +String2Bool( uno::Any& rAny, const gchar * value ) +{ + bool bValue; + + if( strncmp( value, STRNCMP_PARAM( "true" ) ) == 0 ) + bValue = true; + else if( strncmp( value, STRNCMP_PARAM( "false" ) ) == 0 ) + bValue = false; + else + return false; + + rAny <<= bValue; + return true; +} + +/*****************************************************************************/ + +static gchar* +Scale2String( const uno::Any& rAny ) +{ + return g_strdup_printf( "%g", static_cast(rAny.get< sal_Int16 > ()) / 100 ); +} + +static bool +String2Scale( uno::Any& rAny, const gchar * value ) +{ + double dval; + + if( 1 != sscanf( value, "%lg", &dval ) ) + return false; + + rAny <<= static_cast(dval * 100); + return true; +} + +/*****************************************************************************/ + +static gchar * +CaseMap2String( const uno::Any& rAny ) +{ + const gchar * value; + + switch( rAny.get() ) + { + case style::CaseMap::SMALLCAPS: + value = "small_caps"; + break; + + default: + value = "normal"; + break; + } + + return g_strdup(value); +} + +static bool +String2CaseMap( uno::Any& rAny, const gchar * value ) +{ + short nCaseMap; + + if( strncmp( value, STRNCMP_PARAM( "normal" ) ) == 0 ) + nCaseMap = style::CaseMap::NONE; + else if( strncmp( value, STRNCMP_PARAM( "small_caps" ) ) == 0 ) + nCaseMap = style::CaseMap::SMALLCAPS; + else + return false; + + rAny <<= nCaseMap; + return true; +} + +/*****************************************************************************/ + +const gchar * const font_stretch[] = { + "ultra_condensed", + "extra_condensed", + "condensed", + "semi_condensed", + "normal", + "semi_expanded", + "expanded", + "extra_expanded", + "ultra_expanded" +}; + +static gchar* +Kerning2Stretch(const uno::Any& rAny) +{ + sal_Int16 n = rAny.get(); + int i = 4; + + // No good idea for a mapping - just return the basic info + if( n < 0 ) + i=2; + else if( n > 0 ) + i=6; + + return g_strdup(font_stretch[i]); +} + +/*****************************************************************************/ + +static gchar* +Locale2String(const uno::Any& rAny) +{ + /* FIXME-BCP47: support language tags? And why is country lowercase? */ + lang::Locale aLocale = rAny.get (); + LanguageTag aLanguageTag( aLocale); + return g_strdup_printf( "%s-%s", + OUStringToOString( aLanguageTag.getLanguage(), RTL_TEXTENCODING_ASCII_US).getStr(), + OUStringToOString( aLanguageTag.getCountry(), RTL_TEXTENCODING_ASCII_US).toAsciiLowerCase().getStr() ); +} + +static bool +String2Locale( uno::Any& rAny, const gchar * value ) +{ + /* FIXME-BCP47: support language tags? */ + bool ret = false; + + gchar ** str_array = g_strsplit_set( value, "-.@", -1 ); + if( str_array[0] != nullptr ) + { + ret = true; + + lang::Locale aLocale; + + aLocale.Language = OUString::createFromAscii(str_array[0]); + if( str_array[1] != nullptr ) + { + gchar * country = g_ascii_strup(str_array[1], -1); + aLocale.Country = OUString::createFromAscii(country); + g_free(country); + } + + rAny <<= aLocale; + } + + g_strfreev(str_array); + return ret; +} + +/*****************************************************************************/ + +// @see http://www.w3.org/TR/2002/WD-css3-fonts-20020802/#font-effect-prop +static const gchar * relief[] = { "none", "emboss", "engrave" }; +const gchar * const outline = "outline"; + +static gchar * +get_font_effect(const uno::Sequence< beans::PropertyValue >& rAttributeList, + sal_Int32 nContourIndex, sal_Int32 nReliefIndex) +{ + if( nContourIndex != -1 ) + { + if( rAttributeList[nContourIndex].Value.get() ) + return g_strdup(outline); + } + + if( nReliefIndex != -1 ) + { + sal_Int16 n = rAttributeList[nReliefIndex].Value.get(); + if( n < 3) + return g_strdup(relief[n]); + } + + return nullptr; +} + +/*****************************************************************************/ + +// @see http://www.w3.org/TR/REC-CSS2/text.html#lining-striking-props + +enum +{ + DECORATION_NONE = 0, + DECORATION_BLINK, + DECORATION_UNDERLINE, + DECORATION_LINE_THROUGH +}; + +static const gchar * decorations[] = { "none", "blink", "underline", "line-through" }; + +static gchar * +get_text_decoration(const uno::Sequence< beans::PropertyValue >& rAttributeList, + sal_Int32 nBlinkIndex, sal_Int32 nUnderlineIndex, + sal_Int16 nStrikeoutIndex) +{ + gchar * value_list[4] = { nullptr, nullptr, nullptr, nullptr }; + gint count = 0; + + // no property value found + if( ( nBlinkIndex == -1 ) && (nUnderlineIndex == -1 ) && (nStrikeoutIndex == -1)) + return nullptr; + + if( nBlinkIndex != -1 ) + { + if( rAttributeList[nBlinkIndex].Value.get() ) + value_list[count++] = const_cast (decorations[DECORATION_BLINK]); + } + if( nUnderlineIndex != -1 ) + { + sal_Int16 n = rAttributeList[nUnderlineIndex].Value.get (); + if( n != awt::FontUnderline::NONE ) + value_list[count++] = const_cast (decorations[DECORATION_UNDERLINE]); + } + if( nStrikeoutIndex != -1 ) + { + sal_Int16 n = rAttributeList[nStrikeoutIndex].Value.get (); + if( n != awt::FontStrikeout::NONE && n != awt::FontStrikeout::DONTKNOW ) + value_list[count++] = const_cast (decorations[DECORATION_LINE_THROUGH]); + } + + if( count == 0 ) + value_list[count++] = const_cast (decorations[DECORATION_NONE]); + + return g_strjoinv(" ", value_list); +} + +/*****************************************************************************/ + +// @see http://www.w3.org/TR/REC-CSS2/text.html#propdef-text-shadow + +static const gchar * shadow_values[] = { "none", "black" }; + +static gchar * +Bool2Shadow( const uno::Any& rAny ) +{ + int n = 0; + + if( rAny.get() ) + n = 1; + + return g_strdup( shadow_values[n] ); +} + +/*****************************************************************************/ + +static gchar * +Short2Degree( const uno::Any& rAny ) +{ + float f = rAny.get() / 10.0; + return g_strdup_printf( "%g", f ); +} + +/*****************************************************************************/ + +const gchar * const directions[] = { "ltr", "rtl", "rtl", "ltr", "none" }; + +static gchar * +WritingMode2Direction( const uno::Any& rAny ) +{ + sal_Int16 n = rAny.get(); + + if( 0 <= n && n <= text::WritingMode2::PAGE ) + return g_strdup(directions[n]); + + return nullptr; +} + +// @see http://www.w3.org/TR/2001/WD-css3-text-20010517/#PrimaryTextAdvanceDirection + +const gchar * const writing_modes[] = { "lr-tb", "rl-tb", "tb-rl", "tb-lr", "none" }; +static gchar * +WritingMode2String( const uno::Any& rAny ) +{ + sal_Int16 n = rAny.get(); + + if( 0 <= n && n <= text::WritingMode2::PAGE ) + return g_strdup(writing_modes[n]); + + return nullptr; +} + +/*****************************************************************************/ + +const char * const baseline_values[] = { "baseline", "sub", "super" }; + +// @see http://www.w3.org/TR/REC-CSS2/visudet.html#propdef-vertical-align +static gchar * +Escapement2VerticalAlign( const uno::Any& rAny ) +{ + sal_Int16 n = rAny.get(); + gchar * ret = nullptr; + + // Values are in %, 101% means "automatic" + if( n == 0 ) + ret = g_strdup(baseline_values[0]); + else if( n == 101 ) + ret = g_strdup(baseline_values[2]); + else if( n == -101 ) + ret = g_strdup(baseline_values[1]); + else + ret = g_strdup_printf( "%d%%", n ); + + return ret; +} + +/*****************************************************************************/ + +// @see http://www.w3.org/TR/REC-CSS2/visudet.html#propdef-line-height +static gchar * +LineSpacing2LineHeight( const uno::Any& rAny ) +{ + style::LineSpacing ls; + gchar * ret = nullptr; + + if( rAny >>= ls ) + { + if( ls.Mode == style::LineSpacingMode::PROP ) + ret = g_strdup_printf( "%d%%", ls.Height ); + else if( ls.Mode == style::LineSpacingMode::FIX ) + ret = g_strdup_printf("%.3gpt", convertMm100ToPoint(ls.Height)); + } + + return ret; +} + +/*****************************************************************************/ + +// @see http://www.w3.org/People/howcome/t/970224HTMLERB-CSS/WD-tabs-970117.html +static gchar * +TabStopList2String( const uno::Any& rAny, bool default_tabs ) +{ + uno::Sequence< style::TabStop > theTabStops; + gchar * ret = nullptr; + + if( rAny >>= theTabStops) + { + sal_Unicode lastFillChar = ' '; + + for( const auto& rTabStop : std::as_const(theTabStops) ) + { + bool is_default_tab = (style::TabAlign_DEFAULT == rTabStop.Alignment); + + if( is_default_tab != default_tabs ) + continue; + + double fValue = rTabStop.Position; + fValue = fValue * 0.01; + + const gchar * tab_align = ""; + switch( rTabStop.Alignment ) + { + case style::TabAlign_LEFT : + tab_align = "left "; + break; + case style::TabAlign_CENTER : + tab_align = "center "; + break; + case style::TabAlign_RIGHT : + tab_align = "right "; + break; + case style::TabAlign_DECIMAL : + tab_align = "decimal "; + break; + default: + break; + } + + const gchar * lead_char = ""; + + if( rTabStop.FillChar != lastFillChar ) + { + lastFillChar = rTabStop.FillChar; + switch (lastFillChar) + { + case ' ': + lead_char = "blank "; + break; + + case '.': + lead_char = "dotted "; + break; + + case '-': + lead_char = "dashed "; + break; + + case '_': + lead_char = "lined "; + break; + + default: + lead_char = "custom "; + break; + } + } + + gchar * tab_str = g_strdup_printf( "%s%s%gmm", lead_char, tab_align, fValue ); + + if( ret ) + { + gchar * old_tab_str = ret; + ret = g_strconcat(old_tab_str, " ", tab_str, nullptr); + g_free( tab_str ); + g_free( old_tab_str ); + } + else + ret = tab_str; + } + } + + return ret; +} + +static gchar * +TabStops2String( const uno::Any& rAny ) +{ + return TabStopList2String(rAny, false); +} + +static gchar * +DefaultTabStops2String( const uno::Any& rAny ) +{ + return TabStopList2String(rAny, true); +} + +/*****************************************************************************/ + +extern "C" { + +static int +attr_compare(const void *p1,const void *p2) +{ + const rtl_uString * pustr = static_cast(p1); + const char * pc = *static_cast(p2); + + return rtl_ustr_ascii_compare_WithLength(pustr->buffer, pustr->length, pc); +} + +} + +static void +find_exported_attributes( sal_Int32 *pArray, + const css::uno::Sequence< css::beans::PropertyValue >& rAttributeList ) +{ + for( sal_Int32 i = 0; i < rAttributeList.getLength(); i++ ) + { + const char ** pAttr = static_cast(bsearch(rAttributeList[i].Name.pData, + ExportedTextAttributes, TEXT_ATTRIBUTE_LAST, sizeof(const char *), + attr_compare)); + + if( pAttr ) + { + sal_Int32 nIndex = pAttr - ExportedTextAttributes; + pArray[nIndex] = i; + } + } +} + +/*****************************************************************************/ + +static AtkAttributeSet* +attribute_set_prepend( AtkAttributeSet* attribute_set, + AtkTextAttribute attribute, + gchar * value ) +{ + if( value ) + { + AtkAttribute *at = static_cast(g_malloc( sizeof (AtkAttribute) )); + at->name = g_strdup( atk_text_attribute_get_name( attribute ) ); + at->value = value; + + return g_slist_prepend(attribute_set, at); + } + + return attribute_set; +} + +/*****************************************************************************/ + +AtkAttributeSet* +attribute_set_new_from_property_values( + const uno::Sequence< beans::PropertyValue >& rAttributeList, + bool run_attributes_only, + AtkText *text) +{ + AtkAttributeSet* attribute_set = nullptr; + + sal_Int32 aIndexList[TEXT_ATTRIBUTE_LAST] = { -1 }; + + // Initialize index array with -1 + for(sal_Int32 & rn : aIndexList) + rn = -1; + + find_exported_attributes(aIndexList, rAttributeList); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_BG_COLOR, + get_color_value(rAttributeList, aIndexList, TEXT_ATTRIBUTE_BACKGROUND_COLOR, run_attributes_only ? nullptr : text ) ); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_FG_COLOR, + get_color_value(rAttributeList, aIndexList, TEXT_ATTRIBUTE_FOREGROUND_COLOR, run_attributes_only ? nullptr : text) ); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_INVISIBLE, + get_bool_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_HIDDEN])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_UNDERLINE, + get_underline_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_UNDERLINE])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_STRIKETHROUGH, + get_strikethrough_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_STRIKETHROUGH])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_SIZE, + get_height_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_HEIGHT])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_WEIGHT, + get_weight_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_WEIGHT])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_FAMILY_NAME, + get_string_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_FONT_NAME])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_VARIANT, + get_variant_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_CASEMAP])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_STYLE, + get_style_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_POSTURE])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_SCALE, + get_scale_width(rAttributeList, aIndexList[TEXT_ATTRIBUTE_SCALE])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_LANGUAGE, + get_language_string(rAttributeList, aIndexList[TEXT_ATTRIBUTE_LOCALE])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_DIRECTION, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_WRITING_MODE], WritingMode2Direction)); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_STRETCH, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_KERNING], Kerning2Stretch)); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_font_effect ) + atk_text_attribute_font_effect = atk_text_attribute_register("font-effect"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_font_effect, + get_font_effect(rAttributeList, aIndexList[TEXT_ATTRIBUTE_CONTOURED], aIndexList[TEXT_ATTRIBUTE_RELIEF])); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_decoration ) + atk_text_attribute_decoration = atk_text_attribute_register("text-decoration"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_decoration, + get_text_decoration(rAttributeList, aIndexList[TEXT_ATTRIBUTE_BLINKING], + aIndexList[TEXT_ATTRIBUTE_UNDERLINE], aIndexList[TEXT_ATTRIBUTE_STRIKETHROUGH])); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_rotation ) + atk_text_attribute_rotation = atk_text_attribute_register("text-rotation"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_rotation, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_ROTATION], Short2Degree)); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_shadow ) + atk_text_attribute_shadow = atk_text_attribute_register("text-shadow"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_shadow, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_SHADOWED], Bool2Shadow)); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_writing_mode ) + atk_text_attribute_writing_mode = atk_text_attribute_register("writing-mode"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_writing_mode, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_WRITING_MODE], WritingMode2String)); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_vertical_align ) + atk_text_attribute_vertical_align = atk_text_attribute_register("vertical-align"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_vertical_align, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_CHAR_ESCAPEMENT], Escapement2VerticalAlign)); + + if( run_attributes_only ) + return attribute_set; + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_LEFT_MARGIN, + get_cmm_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_LEFT_MARGIN])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_RIGHT_MARGIN, + get_cmm_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_RIGHT_MARGIN])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_INDENT, + get_cmm_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_FIRST_LINE_INDENT])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_PIXELS_ABOVE_LINES, + get_cmm_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_TOP_MARGIN])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_PIXELS_BELOW_LINES, + get_cmm_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_BOTTOM_MARGIN])); + + attribute_set = attribute_set_prepend(attribute_set, ATK_TEXT_ATTR_JUSTIFICATION, + get_justification_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_JUSTIFICATION])); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_paragraph_style ) + atk_text_attribute_paragraph_style = atk_text_attribute_register("paragraph-style"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_paragraph_style, + get_string_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_STYLE_NAME])); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_line_height ) + atk_text_attribute_line_height = atk_text_attribute_register("line-height"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_line_height, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_LINE_SPACING], LineSpacing2LineHeight)); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_tab_interval ) + atk_text_attribute_tab_interval = atk_text_attribute_register("tab-interval"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_tab_interval, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_TAB_STOPS], DefaultTabStops2String)); + + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_tab_stops ) + atk_text_attribute_tab_stops = atk_text_attribute_register("tab-stops"); + + attribute_set = attribute_set_prepend(attribute_set, atk_text_attribute_tab_stops, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_TAB_STOPS], TabStops2String)); + + // #i92233# + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_mm_to_pixel_ratio ) + atk_text_attribute_mm_to_pixel_ratio = atk_text_attribute_register("mm-to-pixel-ratio"); + + attribute_set = attribute_set_prepend( attribute_set, atk_text_attribute_mm_to_pixel_ratio, + get_value(rAttributeList, aIndexList[TEXT_ATTRIBUTE_MM_TO_PIXEL_RATIO], Float2String)); + + return attribute_set; +} + +AtkAttributeSet* +attribute_set_new_from_extended_attributes( + const css::uno::Reference< css::accessibility::XAccessibleExtendedAttributes >& rExtendedAttributes ) +{ + AtkAttributeSet *pSet = nullptr; + + // extended attributes is a string of colon-separated pairs of property and value, + // with pairs separated by semicolons. Example: "heading-level:2;weight:bold;" + uno::Any anyVal = rExtendedAttributes->getExtendedAttributes(); + OUString sExtendedAttrs; + anyVal >>= sExtendedAttrs; + sal_Int32 nIndex = 0; + do + { + OUString sProperty = sExtendedAttrs.getToken( 0, ';', nIndex ); + + sal_Int32 nColonPos = 0; + OString sPropertyName = OUStringToOString( o3tl::getToken(sProperty, 0, ':', nColonPos ), + RTL_TEXTENCODING_UTF8 ); + OString sPropertyValue = OUStringToOString( o3tl::getToken(sProperty, 0, ':', nColonPos ), + RTL_TEXTENCODING_UTF8 ); + + pSet = attribute_set_prepend( pSet, + atk_text_attribute_register( sPropertyName.getStr() ), + g_strdup_printf( "%s", sPropertyValue.getStr() ) ); + } + while ( nIndex >= 0 && nIndex < sExtendedAttrs.getLength() ); + + return pSet; +} + +AtkAttributeSet* attribute_set_prepend_misspelled( AtkAttributeSet* attribute_set ) +{ + if( ATK_TEXT_ATTR_INVALID == atk_text_attribute_misspelled ) + atk_text_attribute_misspelled = atk_text_attribute_register( "text-spelling" ); + + attribute_set = attribute_set_prepend( attribute_set, atk_text_attribute_misspelled, + g_strdup_printf( "misspelled" ) ); + + return attribute_set; +} + +// #i92232# +AtkAttributeSet* attribute_set_prepend_tracked_change_insertion( AtkAttributeSet* attribute_set ) +{ + if ( ATK_TEXT_ATTR_INVALID == atk_text_attribute_tracked_change ) + { + atk_text_attribute_tracked_change = atk_text_attribute_register( "text-tracked-change" ); + } + + attribute_set = attribute_set_prepend( attribute_set, + atk_text_attribute_tracked_change, + g_strdup_printf( "insertion" ) ); + + return attribute_set; +} + +AtkAttributeSet* attribute_set_prepend_tracked_change_deletion( AtkAttributeSet* attribute_set ) +{ + if ( ATK_TEXT_ATTR_INVALID == atk_text_attribute_tracked_change ) + { + atk_text_attribute_tracked_change = atk_text_attribute_register( "text-tracked-change" ); + } + + attribute_set = attribute_set_prepend( attribute_set, + atk_text_attribute_tracked_change, + g_strdup_printf( "deletion" ) ); + + return attribute_set; +} + +AtkAttributeSet* attribute_set_prepend_tracked_change_formatchange( AtkAttributeSet* attribute_set ) +{ + if ( ATK_TEXT_ATTR_INVALID == atk_text_attribute_tracked_change ) + { + atk_text_attribute_tracked_change = atk_text_attribute_register( "text-tracked-change" ); + } + + attribute_set = attribute_set_prepend( attribute_set, + atk_text_attribute_tracked_change, + g_strdup_printf( "attribute-change" ) ); + + return attribute_set; +} + +/*****************************************************************************/ + +namespace { + +struct AtkTextAttrMapping +{ + const char * name; + TextPropertyValueFunc const toPropertyValue; +}; + +} + +const AtkTextAttrMapping g_TextAttrMap[] = +{ + { "", InvalidValue }, // ATK_TEXT_ATTR_INVALID = 0 + { "ParaLeftMargin", UnitString2CMM }, // ATK_TEXT_ATTR_LEFT_MARGIN + { "ParaRightMargin", UnitString2CMM }, // ATK_TEXT_ATTR_RIGHT_MARGIN + { "ParaFirstLineIndent", UnitString2CMM }, // ATK_TEXT_ATTR_INDENT + { "CharHidden", String2Bool }, // ATK_TEXT_ATTR_INVISIBLE + { "", InvalidValue }, // ATK_TEXT_ATTR_EDITABLE + { "ParaTopMargin", UnitString2CMM }, // ATK_TEXT_ATTR_PIXELS_ABOVE_LINES + { "ParaBottomMargin", UnitString2CMM }, // ATK_TEXT_ATTR_PIXELS_BELOW_LINES + { "", InvalidValue }, // ATK_TEXT_ATTR_PIXELS_INSIDE_WRAP + { "", InvalidValue }, // ATK_TEXT_ATTR_BG_FULL_HEIGHT + { "", InvalidValue }, // ATK_TEXT_ATTR_RISE + { "CharUnderline", String2Underline }, // ATK_TEXT_ATTR_UNDERLINE + { "CharStrikeout", String2Strikeout }, // ATK_TEXT_ATTR_STRIKETHROUGH + { "CharHeight", String2Float }, // ATK_TEXT_ATTR_SIZE + { "CharScaleWidth", String2Scale }, // ATK_TEXT_ATTR_SCALE + { "CharWeight", String2Weight }, // ATK_TEXT_ATTR_WEIGHT + { "CharLocale", String2Locale }, // ATK_TEXT_ATTR_LANGUAGE + { "CharFontName", SetString }, // ATK_TEXT_ATTR_FAMILY_NAME + { "CharBackColor", String2Color }, // ATK_TEXT_ATTR_BG_COLOR + { "CharColor", String2Color }, // ATK_TEXT_ATTR_FG_COLOR + { "", InvalidValue }, // ATK_TEXT_ATTR_BG_STIPPLE + { "", InvalidValue }, // ATK_TEXT_ATTR_FG_STIPPLE + { "", InvalidValue }, // ATK_TEXT_ATTR_WRAP_MODE + { "", InvalidValue }, // ATK_TEXT_ATTR_DIRECTION + { "ParaAdjust", Justification2Adjust }, // ATK_TEXT_ATTR_JUSTIFICATION + { "", InvalidValue }, // ATK_TEXT_ATTR_STRETCH + { "CharCaseMap", String2CaseMap }, // ATK_TEXT_ATTR_VARIANT + { "CharPosture", Style2FontSlant } // ATK_TEXT_ATTR_STYLE +}; + +/*****************************************************************************/ + +bool +attribute_set_map_to_property_values( + AtkAttributeSet* attribute_set, + uno::Sequence< beans::PropertyValue >& rValueList ) +{ + // Ensure enough space .. + uno::Sequence< beans::PropertyValue > aAttributeList (SAL_N_ELEMENTS(g_TextAttrMap)); + auto pAttributeList = aAttributeList.getArray(); + + sal_Int32 nIndex = 0; + for( GSList * item = attribute_set; item != nullptr; item = g_slist_next( item ) ) + { + AtkAttribute* attribute = reinterpret_cast(item); + + AtkTextAttribute text_attr = atk_text_attribute_for_name( attribute->name ); + if( text_attr < SAL_N_ELEMENTS(g_TextAttrMap) ) + { + if( g_TextAttrMap[text_attr].name[0] != '\0' ) + { + if( ! g_TextAttrMap[text_attr].toPropertyValue( pAttributeList[nIndex].Value, attribute->value) ) + return false; + + pAttributeList[nIndex].Name = OUString::createFromAscii( g_TextAttrMap[text_attr].name ); + pAttributeList[nIndex].State = beans::PropertyState_DIRECT_VALUE; + ++nIndex; + } + } + else + { + // Unsupported text attribute + return false; + } + } + + aAttributeList.realloc( nIndex ); + rValueList = aAttributeList; + return true; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atktextattributes.hxx b/vcl/unx/gtk3/a11y/atktextattributes.hxx new file mode 100644 index 0000000000..716ec45bd3 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atktextattributes.hxx @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include +#include +#include + +#include + +AtkAttributeSet* attribute_set_new_from_property_values( + const css::uno::Sequence& rAttributeList, bool run_attributes_only, + AtkText* text); + +AtkAttributeSet* attribute_set_new_from_extended_attributes( + const css::uno::Reference& + rExtendedAttributes); + +bool attribute_set_map_to_property_values( + AtkAttributeSet* attribute_set, css::uno::Sequence& rValueList); + +AtkAttributeSet* attribute_set_prepend_misspelled(AtkAttributeSet* attribute_set); +// #i92232# +AtkAttributeSet* attribute_set_prepend_tracked_change_insertion(AtkAttributeSet* attribute_set); +AtkAttributeSet* attribute_set_prepend_tracked_change_deletion(AtkAttributeSet* attribute_set); +AtkAttributeSet* attribute_set_prepend_tracked_change_formatchange(AtkAttributeSet* attribute_set); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkutil.cxx b/vcl/unx/gtk3/a11y/atkutil.cxx new file mode 100644 index 0000000000..fc15351374 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkutil.cxx @@ -0,0 +1,626 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include "atkwrapper.hxx" +#include "atkutil.hxx" + +#include +#include + +using namespace ::com::sun::star; + +namespace +{ + uno::WeakReference< accessibility::XAccessible > theNextFocusObject; +} + +static guint focus_notify_handler = 0; + +/*****************************************************************************/ + +extern "C" { + +static gboolean +atk_wrapper_focus_idle_handler (gpointer data) +{ + SolarMutexGuard aGuard; + + focus_notify_handler = 0; + + uno::Reference< accessibility::XAccessible > xAccessible = theNextFocusObject; + if( xAccessible.get() == static_cast < accessibility::XAccessible * > (data) ) + { + AtkObject *atk_obj = xAccessible.is() ? atk_object_wrapper_ref( xAccessible ) : nullptr; + // Gail does not notify focus changes to NULL, so do we .. + if( atk_obj ) + { + SAL_WNODEPRECATED_DECLARATIONS_PUSH + atk_focus_tracker_notify(atk_obj); + SAL_WNODEPRECATED_DECLARATIONS_POP + // #i93269# + // emit text_caret_moved event for object, + // if cursor is inside the object. + // also emit state-changed:focused event under the same condition. + { + AtkObjectWrapper* wrapper_obj = ATK_OBJECT_WRAPPER (atk_obj); + if( wrapper_obj && !wrapper_obj->mpText.is() ) + { + wrapper_obj->mpText.set(wrapper_obj->mpContext, css::uno::UNO_QUERY); + if ( wrapper_obj->mpText.is() ) + { + gint caretPos = -1; + + try { + caretPos = wrapper_obj->mpText->getCaretPosition(); + } + catch(const uno::Exception&) { + g_warning( "Exception in getCaretPosition()" ); + } + + if ( caretPos != -1 ) + { + atk_object_notify_state_change( atk_obj, ATK_STATE_FOCUSED, true ); + g_signal_emit_by_name( atk_obj, "text_caret_moved", caretPos ); + } + } + } + } + g_object_unref(atk_obj); + } + } + + return false; +} + +} // extern "C" + +/*****************************************************************************/ + +static void +atk_wrapper_focus_tracker_notify_when_idle( const uno::Reference< accessibility::XAccessible > &xAccessible ) +{ + if( focus_notify_handler ) + g_source_remove(focus_notify_handler); + + theNextFocusObject = xAccessible; + + focus_notify_handler = g_idle_add (atk_wrapper_focus_idle_handler, xAccessible.get()); +} + +/*****************************************************************************/ + +void DocumentFocusListener::disposing( const lang::EventObject& aEvent ) +{ + + // Unref the object here, but do not remove as listener since the object + // might no longer be in a state that safely allows this. + if( aEvent.Source.is() ) + m_aRefList.erase(aEvent.Source); + +} + +/*****************************************************************************/ + +void DocumentFocusListener::notifyEvent( const accessibility::AccessibleEventObject& aEvent ) +{ + try { + switch( aEvent.EventId ) + { + case accessibility::AccessibleEventId::STATE_CHANGED: + { + sal_Int64 nState = accessibility::AccessibleStateType::INVALID; + aEvent.NewValue >>= nState; + + if( accessibility::AccessibleStateType::FOCUSED == nState ) + atk_wrapper_focus_tracker_notify_when_idle( getAccessible(aEvent) ); + + break; + } + + case accessibility::AccessibleEventId::CHILD: + { + uno::Reference< accessibility::XAccessible > xChild; + if( (aEvent.OldValue >>= xChild) && xChild.is() ) + detachRecursive(xChild); + + if( (aEvent.NewValue >>= xChild) && xChild.is() ) + attachRecursive(xChild); + + break; + } + + case accessibility::AccessibleEventId::INVALIDATE_ALL_CHILDREN: + { + if (uno::Reference< accessibility::XAccessible > xAcc = getAccessible(aEvent)) + detachRecursive(xAcc); + break; + } + + default: + break; + } + } + catch( const lang::IndexOutOfBoundsException& ) + { + g_warning("DocumentFocusListener: Focused object has invalid index in parent"); + } +} + +/*****************************************************************************/ + +uno::Reference< accessibility::XAccessible > DocumentFocusListener::getAccessible(const lang::EventObject& aEvent ) +{ + uno::Reference< accessibility::XAccessible > xAccessible(aEvent.Source, uno::UNO_QUERY); + + if( xAccessible.is() ) + return xAccessible; + + uno::Reference< accessibility::XAccessibleContext > xContext(aEvent.Source, uno::UNO_QUERY); + + if( xContext.is() ) + { + uno::Reference< accessibility::XAccessible > xParent( xContext->getAccessibleParent() ); + if( xParent.is() ) + { + uno::Reference< accessibility::XAccessibleContext > xParentContext( xParent->getAccessibleContext() ); + if( xParentContext.is() ) + { + return xParentContext->getAccessibleChild( xContext->getAccessibleIndexInParent() ); + } + } + } + + return uno::Reference< accessibility::XAccessible >(); +} + +/*****************************************************************************/ + +void DocumentFocusListener::attachRecursive( + const uno::Reference< accessibility::XAccessible >& xAccessible +) +{ + uno::Reference< accessibility::XAccessibleContext > xContext = + xAccessible->getAccessibleContext(); + + if( xContext.is() ) + attachRecursive(xAccessible, xContext); +} + +/*****************************************************************************/ + +void DocumentFocusListener::attachRecursive( + const uno::Reference< accessibility::XAccessible >& xAccessible, + const uno::Reference< accessibility::XAccessibleContext >& xContext +) +{ + sal_Int64 nStateSet = xContext->getAccessibleStateSet(); + attachRecursive(xAccessible, xContext, nStateSet); +} + +/*****************************************************************************/ + +void DocumentFocusListener::attachRecursive( + const uno::Reference< accessibility::XAccessible >& xAccessible, + const uno::Reference< accessibility::XAccessibleContext >& xContext, + sal_Int64 nStateSet +) +{ + if( nStateSet & accessibility::AccessibleStateType::FOCUSED ) + atk_wrapper_focus_tracker_notify_when_idle( xAccessible ); + + uno::Reference< accessibility::XAccessibleEventBroadcaster > xBroadcaster(xContext, uno::UNO_QUERY); + + if (!xBroadcaster.is()) + return; + + // If not already done, add the broadcaster to the list and attach as listener. + const uno::Reference< uno::XInterface >& xInterface = xBroadcaster; + if( !m_aRefList.insert(xInterface).second ) + return; + + xBroadcaster->addAccessibleEventListener(static_cast< accessibility::XAccessibleEventListener *>(this)); + + if( ! (nStateSet & accessibility::AccessibleStateType::MANAGES_DESCENDANTS) ) + { + sal_Int64 n, nmax = xContext->getAccessibleChildCount(); + for( n = 0; n < nmax; n++ ) + { + uno::Reference< accessibility::XAccessible > xChild( xContext->getAccessibleChild( n ) ); + + if( xChild.is() ) + attachRecursive(xChild); + } + } +} + +/*****************************************************************************/ + +void DocumentFocusListener::detachRecursive( + const uno::Reference< accessibility::XAccessible >& xAccessible +) +{ + uno::Reference< accessibility::XAccessibleContext > xContext = + xAccessible->getAccessibleContext(); + + if( xContext.is() ) + detachRecursive(xContext); +} + +/*****************************************************************************/ + +void DocumentFocusListener::detachRecursive( + const uno::Reference< accessibility::XAccessibleContext >& xContext +) +{ + sal_Int64 nStateSet = xContext->getAccessibleStateSet(); + + detachRecursive(xContext, nStateSet); +} + +/*****************************************************************************/ + +void DocumentFocusListener::detachRecursive( + const uno::Reference< accessibility::XAccessibleContext >& xContext, + sal_Int64 nStateSet +) +{ + uno::Reference< accessibility::XAccessibleEventBroadcaster > xBroadcaster(xContext, uno::UNO_QUERY); + + if( !xBroadcaster.is() || 0 >= m_aRefList.erase(xBroadcaster) ) + return; + + xBroadcaster->removeAccessibleEventListener(static_cast< accessibility::XAccessibleEventListener *>(this)); + + if( ! (nStateSet & accessibility::AccessibleStateType::MANAGES_DESCENDANTS) ) + { + sal_Int64 n, nmax = xContext->getAccessibleChildCount(); + for( n = 0; n < nmax; n++ ) + { + uno::Reference< accessibility::XAccessible > xChild( xContext->getAccessibleChild( n ) ); + + if( xChild.is() ) + detachRecursive(xChild); + } + } +} + +/*****************************************************************************/ + +/* + * page tabs in gtk are widgets, so we need to simulate focus events for those + */ + +static void handle_tabpage_activated(vcl::Window *pWindow) +{ + uno::Reference< accessibility::XAccessible > xAccessible = + pWindow->GetAccessible(); + + if( ! xAccessible.is() ) + return; + + uno::Reference< accessibility::XAccessibleSelection > xSelection( + xAccessible->getAccessibleContext(), uno::UNO_QUERY); + + if( xSelection.is() ) + atk_wrapper_focus_tracker_notify_when_idle( xSelection->getSelectedAccessibleChild(0) ); +} + +/*****************************************************************************/ + +/* + * toolbar items in gtk are widgets, so we need to simulate focus events for those + */ + +static void notify_toolbox_item_focus(ToolBox *pToolBox) +{ + uno::Reference< accessibility::XAccessible > xAccessible = + pToolBox->GetAccessible(); + + if( ! xAccessible.is() ) + return; + + uno::Reference< accessibility::XAccessibleContext > xContext = + xAccessible->getAccessibleContext(); + + if( ! xContext.is() ) + return; + + ToolBox::ImplToolItems::size_type nPos = pToolBox->GetItemPos( pToolBox->GetHighlightItemId() ); + if( nPos != ToolBox::ITEM_NOTFOUND ) + atk_wrapper_focus_tracker_notify_when_idle( xContext->getAccessibleChild( nPos ) ); +} + +static void handle_toolbox_highlight(vcl::Window *pWindow) +{ + ToolBox *pToolBox = static_cast (pWindow); + + // Make sure either the toolbox or its parent toolbox has the focus + if ( ! pToolBox->HasFocus() ) + { + ToolBox* pToolBoxParent = dynamic_cast< ToolBox* >( pToolBox->GetParent() ); + if ( ! pToolBoxParent || ! pToolBoxParent->HasFocus() ) + return; + } + + notify_toolbox_item_focus(pToolBox); +} + +static void handle_toolbox_highlightoff(vcl::Window const *pWindow) +{ + ToolBox* pToolBoxParent = dynamic_cast< ToolBox* >( pWindow->GetParent() ); + + // Notify when leaving sub toolboxes + if( pToolBoxParent && pToolBoxParent->HasFocus() ) + notify_toolbox_item_focus( pToolBoxParent ); +} + +/*****************************************************************************/ + +static void create_wrapper_for_child( + const uno::Reference< accessibility::XAccessibleContext >& xContext, + sal_Int32 index) +{ + if( xContext.is() ) + { + uno::Reference< accessibility::XAccessible > xChild(xContext->getAccessibleChild(index)); + if( xChild.is() ) + { + // create the wrapper object - it will survive the unref unless it is a transient object + g_object_unref( atk_object_wrapper_ref( xChild ) ); + } + } +} + +/*****************************************************************************/ + +static void handle_toolbox_buttonchange(VclWindowEvent const *pEvent) +{ + vcl::Window* pWindow = pEvent->GetWindow(); + sal_Int32 index = static_cast(reinterpret_cast(pEvent->GetData())); + + if( pWindow && pWindow->IsReallyVisible() ) + { + uno::Reference< accessibility::XAccessible > xAccessible(pWindow->GetAccessible()); + if( xAccessible.is() ) + { + create_wrapper_for_child(xAccessible->getAccessibleContext(), index); + } + } +} + +/*****************************************************************************/ + +namespace { + +struct WindowList { + ~WindowList() { assert(list.empty()); }; + // needs to be empty already on DeInitVCL, but at least check it's empty + // on exit + + std::set< VclPtr > list; +}; + +WindowList g_aWindowList; + +} + +rtl::Reference GtkSalData::GetDocumentFocusListener() +{ + rtl::Reference xDFL = m_xDocumentFocusListener.get(); + if (!xDFL) + { + xDFL = new DocumentFocusListener; + m_xDocumentFocusListener = xDFL.get(); + } + return xDFL; +} + +static void handle_get_focus(::VclWindowEvent const * pEvent) +{ + GtkSalData *const pSalData(GetGtkSalData()); + assert(pSalData); + + rtl::Reference xDocumentFocusListener(pSalData->GetDocumentFocusListener()); + + vcl::Window *pWindow = pEvent->GetWindow(); + + // The menu bar is handled through VclEventId::MenuHighlightED + if( ! pWindow || !pWindow->IsReallyVisible() || pWindow->GetType() == WindowType::MENUBARWINDOW ) + return; + + // ToolBoxes are handled through VclEventId::ToolboxHighlight + if( pWindow->GetType() == WindowType::TOOLBOX ) + return; + + if( pWindow->GetType() == WindowType::TABCONTROL ) + { + handle_tabpage_activated( pWindow ); + return; + } + + uno::Reference< accessibility::XAccessible > xAccessible = + pWindow->GetAccessible(); + + if( ! xAccessible.is() ) + return; + + uno::Reference< accessibility::XAccessibleContext > xContext = + xAccessible->getAccessibleContext(); + + if( ! xContext.is() ) + return; + + sal_Int64 nStateSet = xContext->getAccessibleStateSet(); + +/* the UNO ToolBox wrapper does not (yet?) support XAccessibleSelection, so we + * need to add listeners to the children instead of re-using the tabpage stuff + */ + if( (nStateSet & accessibility::AccessibleStateType::FOCUSED) && + ( pWindow->GetType() != WindowType::TREELISTBOX ) ) + { + atk_wrapper_focus_tracker_notify_when_idle( xAccessible ); + } + else + { + if( g_aWindowList.list.insert(pWindow).second ) + { + try + { + xDocumentFocusListener->attachRecursive(xAccessible, xContext, nStateSet); + } + catch (const uno::Exception&) + { + g_warning( "Exception caught processing focus events" ); + } + } + } +} + +/*****************************************************************************/ + +static void handle_menu_highlighted(::VclMenuEvent const * pEvent) +{ + try + { + Menu* pMenu = pEvent->GetMenu(); + sal_uInt16 nPos = pEvent->GetItemPos(); + + if( pMenu && nPos != 0xFFFF) + { + uno::Reference< accessibility::XAccessible > xAccessible ( pMenu->GetAccessible() ); + + if( xAccessible.is() ) + { + uno::Reference< accessibility::XAccessibleContext > xContext ( xAccessible->getAccessibleContext() ); + + if( xContext.is() ) + atk_wrapper_focus_tracker_notify_when_idle( xContext->getAccessibleChild( nPos ) ); + } + } + } + catch (const uno::Exception&) + { + g_warning( "Exception caught processing menu highlight events" ); + } +} + +/*****************************************************************************/ + +static void WindowEventHandler(void *, VclSimpleEvent& rEvent) +{ + try + { + switch (rEvent.GetId()) + { + case VclEventId::WindowShow: + break; + case VclEventId::WindowHide: + break; + case VclEventId::WindowClose: + break; + case VclEventId::WindowGetFocus: + handle_get_focus(static_cast< ::VclWindowEvent const * >(&rEvent)); + break; + case VclEventId::WindowLoseFocus: + break; + case VclEventId::WindowMinimize: + break; + case VclEventId::WindowNormalize: + break; + case VclEventId::WindowKeyInput: + case VclEventId::WindowKeyUp: + case VclEventId::WindowCommand: + case VclEventId::WindowMouseMove: + break; + + case VclEventId::MenuHighlight: + if (const VclMenuEvent* pMenuEvent = dynamic_cast(&rEvent)) + { + handle_menu_highlighted(pMenuEvent); + } + break; + + case VclEventId::ToolboxHighlight: + handle_toolbox_highlight(static_cast< ::VclWindowEvent const * >(&rEvent)->GetWindow()); + break; + + case VclEventId::ToolboxButtonStateChanged: + handle_toolbox_buttonchange(static_cast< ::VclWindowEvent const * >(&rEvent)); + break; + + case VclEventId::ObjectDying: + g_aWindowList.list.erase( static_cast< ::VclWindowEvent const * >(&rEvent)->GetWindow() ); + [[fallthrough]]; + case VclEventId::ToolboxHighlightOff: + handle_toolbox_highlightoff(static_cast< ::VclWindowEvent const * >(&rEvent)->GetWindow()); + break; + + case VclEventId::TabpageActivate: + handle_tabpage_activated(static_cast< ::VclWindowEvent const * >(&rEvent)->GetWindow()); + break; + + case VclEventId::ComboboxSetText: + // This looks quite strange to me. Stumbled over this when fixing #i104290#. + // This kicked in when leaving the combobox in the toolbar, after that the events worked. + // I guess this was a try to work around missing combobox events, which didn't do the full job, and shouldn't be necessary anymore. + // Fix for #i104290# was done in toolkit/source/awt/vclxaccessiblecomponent, FOCUSED state for compound controls in general. + // create_wrapper_for_children(static_cast< ::VclWindowEvent const * >(pEvent)->GetWindow()); + break; + + default: + break; + } + } + catch (const lang::IndexOutOfBoundsException&) + { + g_warning("WindowEventHandler: Focused object has invalid index in parent"); + } +} + +static Link g_aEventListenerLink( nullptr, WindowEventHandler ); + +/*****************************************************************************/ + +void ooo_atk_util_ensure_event_listener() +{ + static bool bInited; + if (!bInited) + { + Application::AddEventListener( g_aEventListenerLink ); + bInited = true; + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkutil.hxx b/vcl/unx/gtk3/a11y/atkutil.hxx new file mode 100644 index 0000000000..bc3d9d73b9 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkutil.hxx @@ -0,0 +1,26 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include + +void ooo_atk_util_ensure_event_listener(); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkvalue.cxx b/vcl/unx/gtk3/a11y/atkvalue.cxx new file mode 100644 index 0000000000..014164da20 --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkvalue.cxx @@ -0,0 +1,154 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "atkwrapper.hxx" + +#include + +#include +#include + +using namespace ::com::sun::star; + +/// @throws uno::RuntimeException +static css::uno::Reference + getValue( AtkValue *pValue ) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER( pValue ); + if( pWrap ) + { + if( !pWrap->mpValue.is() ) + { + pWrap->mpValue.set(pWrap->mpContext, css::uno::UNO_QUERY); + } + + return pWrap->mpValue; + } + + return css::uno::Reference(); +} + +static void anyToGValue( const uno::Any& aAny, GValue *pValue ) +{ + // FIXME: expand to lots of types etc. + double aDouble=0; + aAny >>= aDouble; + + memset( pValue, 0, sizeof( GValue ) ); + g_value_init( pValue, G_TYPE_DOUBLE ); + g_value_set_double( pValue, aDouble ); +} + +extern "C" { + +static void +value_wrapper_get_current_value( AtkValue *value, + GValue *gval ) +{ + try { + css::uno::Reference pValue + = getValue( value ); + if( pValue.is() ) + anyToGValue( pValue->getCurrentValue(), gval ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getCurrentValue()" ); + } +} + +static void +value_wrapper_get_maximum_value( AtkValue *value, + GValue *gval ) +{ + try { + css::uno::Reference pValue + = getValue( value ); + if( pValue.is() ) + anyToGValue( pValue->getMaximumValue(), gval ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getCurrentValue()" ); + } +} + +static void +value_wrapper_get_minimum_value( AtkValue *value, + GValue *gval ) +{ + try { + css::uno::Reference pValue + = getValue( value ); + if( pValue.is() ) + anyToGValue( pValue->getMinimumValue(), gval ); + } + catch(const uno::Exception&) { + g_warning( "Exception in getCurrentValue()" ); + } +} + +static gboolean +value_wrapper_set_current_value( AtkValue *value, + const GValue *gval ) +{ + try { + css::uno::Reference pValue + = getValue( value ); + if( pValue.is() ) + { + double aDouble = g_value_get_double( gval ); + + // Different types of numerical values for XAccessibleValue are possible. + // If current value has an integer type, also use that for the new value, to make + // sure underlying implementations expecting that can handle the value properly. + const css::uno::Any aCurrentValue = pValue->getCurrentValue(); + if (aCurrentValue.getValueTypeClass() == css::uno::TypeClass::TypeClass_LONG) + { + const sal_Int32 nValue = std::round(aDouble); + return pValue->setCurrentValue(css::uno::Any(nValue)); + } + else if (aCurrentValue.getValueTypeClass() == css::uno::TypeClass::TypeClass_HYPER) + { + const sal_Int64 nValue = std::round(aDouble); + return pValue->setCurrentValue(css::uno::Any(nValue)); + } + + return pValue->setCurrentValue( uno::Any(aDouble) ); + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getCurrentValue()" ); + } + + return FALSE; +} + +} // extern "C" + +void +valueIfaceInit (AtkValueIface *iface) +{ + g_return_if_fail (iface != nullptr); + + iface->get_current_value = value_wrapper_get_current_value; + iface->get_maximum_value = value_wrapper_get_maximum_value; + iface->get_minimum_value = value_wrapper_get_minimum_value; + iface->set_current_value = value_wrapper_set_current_value; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkwrapper.cxx b/vcl/unx/gtk3/a11y/atkwrapper.cxx new file mode 100644 index 0000000000..c946a6e3da --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkwrapper.cxx @@ -0,0 +1,1103 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "atkwrapper.hxx" +#include "atkregistry.hxx" +#include "atklistener.hxx" +#include "atktextattributes.hxx" + +#include +#include + +using namespace ::com::sun::star; + +static GObjectClass *parent_class = nullptr; + +static AtkRelationType mapRelationType( sal_Int16 nRelation ) +{ + AtkRelationType type = ATK_RELATION_NULL; + + switch( nRelation ) + { + case accessibility::AccessibleRelationType::CONTENT_FLOWS_FROM: + type = ATK_RELATION_FLOWS_FROM; + break; + + case accessibility::AccessibleRelationType::CONTENT_FLOWS_TO: + type = ATK_RELATION_FLOWS_TO; + break; + + case accessibility::AccessibleRelationType::CONTROLLED_BY: + type = ATK_RELATION_CONTROLLED_BY; + break; + + case accessibility::AccessibleRelationType::CONTROLLER_FOR: + type = ATK_RELATION_CONTROLLER_FOR; + break; + + case accessibility::AccessibleRelationType::LABEL_FOR: + type = ATK_RELATION_LABEL_FOR; + break; + + case accessibility::AccessibleRelationType::LABELED_BY: + type = ATK_RELATION_LABELLED_BY; + break; + + case accessibility::AccessibleRelationType::MEMBER_OF: + type = ATK_RELATION_MEMBER_OF; + break; + + case accessibility::AccessibleRelationType::SUB_WINDOW_OF: + type = ATK_RELATION_SUBWINDOW_OF; + break; + + case accessibility::AccessibleRelationType::NODE_CHILD_OF: + type = ATK_RELATION_NODE_CHILD_OF; + break; + + default: + break; + } + + return type; +} + +AtkStateType mapAtkState( sal_Int64 nState ) +{ + AtkStateType type = ATK_STATE_INVALID; + + // A perfect / complete mapping ... + switch( nState ) + { +#define MAP_DIRECT( a ) \ + case accessibility::AccessibleStateType::a: \ + type = ATK_STATE_##a; break + + MAP_DIRECT( INVALID ); + MAP_DIRECT( ACTIVE ); + MAP_DIRECT( ARMED ); + MAP_DIRECT( BUSY ); + MAP_DIRECT( CHECKABLE ); + MAP_DIRECT( CHECKED ); + MAP_DIRECT( EDITABLE ); + MAP_DIRECT( ENABLED ); + MAP_DIRECT( EXPANDABLE ); + MAP_DIRECT( EXPANDED ); + MAP_DIRECT( FOCUSABLE ); + MAP_DIRECT( FOCUSED ); + MAP_DIRECT( HORIZONTAL ); + MAP_DIRECT( ICONIFIED ); + MAP_DIRECT( INDETERMINATE ); + MAP_DIRECT( MANAGES_DESCENDANTS ); + MAP_DIRECT( MODAL ); + MAP_DIRECT( MULTI_LINE ); + MAP_DIRECT( OPAQUE ); + MAP_DIRECT( PRESSED ); + MAP_DIRECT( RESIZABLE ); + MAP_DIRECT( SELECTABLE ); + MAP_DIRECT( SELECTED ); + MAP_DIRECT( SENSITIVE ); + MAP_DIRECT( SHOWING ); + MAP_DIRECT( SINGLE_LINE ); + MAP_DIRECT( STALE ); + MAP_DIRECT( TRANSIENT ); + MAP_DIRECT( VERTICAL ); + MAP_DIRECT( VISIBLE ); + MAP_DIRECT( DEFAULT ); + // a spelling error ... + case accessibility::AccessibleStateType::DEFUNC: + type = ATK_STATE_DEFUNCT; break; + case accessibility::AccessibleStateType::MULTI_SELECTABLE: + type = ATK_STATE_MULTISELECTABLE; break; + default: + //Mis-use ATK_STATE_LAST_DEFINED to check if a state is unmapped + //NOTE! Do not report it + type = ATK_STATE_LAST_DEFINED; + break; + } + + return type; +} + +static AtkRole mapToAtkRole(sal_Int16 nRole, sal_Int64 nStates) +{ + switch (nRole) + { + case accessibility::AccessibleRole::UNKNOWN: + return ATK_ROLE_UNKNOWN; + case accessibility::AccessibleRole::ALERT: + return ATK_ROLE_ALERT; + case accessibility::AccessibleRole::BLOCK_QUOTE: + return ATK_ROLE_BLOCK_QUOTE; + case accessibility::AccessibleRole::COLUMN_HEADER: + return ATK_ROLE_COLUMN_HEADER; + case accessibility::AccessibleRole::CANVAS: + return ATK_ROLE_CANVAS; + case accessibility::AccessibleRole::CHECK_BOX: + return ATK_ROLE_CHECK_BOX; + case accessibility::AccessibleRole::CHECK_MENU_ITEM: + return ATK_ROLE_CHECK_MENU_ITEM; + case accessibility::AccessibleRole::COLOR_CHOOSER: + return ATK_ROLE_COLOR_CHOOSER; + case accessibility::AccessibleRole::COMBO_BOX: + return ATK_ROLE_COMBO_BOX; + case accessibility::AccessibleRole::DATE_EDITOR: + return ATK_ROLE_DATE_EDITOR; + case accessibility::AccessibleRole::DESKTOP_ICON: + return ATK_ROLE_DESKTOP_ICON; + case accessibility::AccessibleRole::DESKTOP_PANE: + return ATK_ROLE_DESKTOP_FRAME; + case accessibility::AccessibleRole::DIRECTORY_PANE: + return ATK_ROLE_DIRECTORY_PANE; + case accessibility::AccessibleRole::DIALOG: + return ATK_ROLE_DIALOG; + case accessibility::AccessibleRole::DOCUMENT: + return ATK_ROLE_DOCUMENT_FRAME; + case accessibility::AccessibleRole::EMBEDDED_OBJECT: + return ATK_ROLE_EMBEDDED; + case accessibility::AccessibleRole::END_NOTE: + return ATK_ROLE_FOOTNOTE; + case accessibility::AccessibleRole::FILE_CHOOSER: + return ATK_ROLE_FILE_CHOOSER; + case accessibility::AccessibleRole::FILLER: + return ATK_ROLE_FILLER; + case accessibility::AccessibleRole::FONT_CHOOSER: + return ATK_ROLE_FONT_CHOOSER; + case accessibility::AccessibleRole::FOOTER: + return ATK_ROLE_FOOTER; + case accessibility::AccessibleRole::FOOTNOTE: + return ATK_ROLE_FOOTNOTE; + case accessibility::AccessibleRole::FRAME: + return ATK_ROLE_FRAME; + case accessibility::AccessibleRole::GLASS_PANE: + return ATK_ROLE_GLASS_PANE; + case accessibility::AccessibleRole::GRAPHIC: + return ATK_ROLE_IMAGE; + case accessibility::AccessibleRole::GROUP_BOX: + return ATK_ROLE_GROUPING; + case accessibility::AccessibleRole::HEADER: + return ATK_ROLE_HEADER; + case accessibility::AccessibleRole::HEADING: + return ATK_ROLE_HEADING; + case accessibility::AccessibleRole::HYPER_LINK: + return ATK_ROLE_LINK; + case accessibility::AccessibleRole::ICON: + return ATK_ROLE_ICON; + case accessibility::AccessibleRole::INTERNAL_FRAME: + return ATK_ROLE_INTERNAL_FRAME; + case accessibility::AccessibleRole::LABEL: + return ATK_ROLE_LABEL; + case accessibility::AccessibleRole::LAYERED_PANE: + return ATK_ROLE_LAYERED_PANE; + case accessibility::AccessibleRole::LIST: + return ATK_ROLE_LIST; + case accessibility::AccessibleRole::LIST_ITEM: + return ATK_ROLE_LIST_ITEM; + case accessibility::AccessibleRole::MENU: + return ATK_ROLE_MENU; + case accessibility::AccessibleRole::MENU_BAR: + return ATK_ROLE_MENU_BAR; + case accessibility::AccessibleRole::MENU_ITEM: + return ATK_ROLE_MENU_ITEM; + case accessibility::AccessibleRole::OPTION_PANE: + return ATK_ROLE_OPTION_PANE; + case accessibility::AccessibleRole::PAGE_TAB: + return ATK_ROLE_PAGE_TAB; + case accessibility::AccessibleRole::PAGE_TAB_LIST: + return ATK_ROLE_PAGE_TAB_LIST; + case accessibility::AccessibleRole::PANEL: + return ATK_ROLE_PANEL; + case accessibility::AccessibleRole::PARAGRAPH: + return ATK_ROLE_PARAGRAPH; + case accessibility::AccessibleRole::PASSWORD_TEXT: + return ATK_ROLE_PASSWORD_TEXT; + case accessibility::AccessibleRole::POPUP_MENU: + return ATK_ROLE_POPUP_MENU; + case accessibility::AccessibleRole::PUSH_BUTTON: + return ATK_ROLE_PUSH_BUTTON; + case accessibility::AccessibleRole::PROGRESS_BAR: + return ATK_ROLE_PROGRESS_BAR; + case accessibility::AccessibleRole::RADIO_BUTTON: + return ATK_ROLE_RADIO_BUTTON; + case accessibility::AccessibleRole::RADIO_MENU_ITEM: + return ATK_ROLE_RADIO_MENU_ITEM; + case accessibility::AccessibleRole::ROW_HEADER: + return ATK_ROLE_ROW_HEADER; + case accessibility::AccessibleRole::ROOT_PANE: + return ATK_ROLE_ROOT_PANE; + case accessibility::AccessibleRole::SCROLL_BAR: + return ATK_ROLE_SCROLL_BAR; + case accessibility::AccessibleRole::SCROLL_PANE: + return ATK_ROLE_SCROLL_PANE; + case accessibility::AccessibleRole::SHAPE: + return ATK_ROLE_PANEL; + case accessibility::AccessibleRole::SEPARATOR: + return ATK_ROLE_SEPARATOR; + case accessibility::AccessibleRole::SLIDER: + return ATK_ROLE_SLIDER; + case accessibility::AccessibleRole::SPIN_BOX: + return ATK_ROLE_SPIN_BUTTON; + case accessibility::AccessibleRole::SPLIT_PANE: + return ATK_ROLE_SPLIT_PANE; + case accessibility::AccessibleRole::STATUS_BAR: + return ATK_ROLE_STATUSBAR; + case accessibility::AccessibleRole::TABLE: + return ATK_ROLE_TABLE; + case accessibility::AccessibleRole::TABLE_CELL: + return ATK_ROLE_TABLE_CELL; + case accessibility::AccessibleRole::TEXT: + return ATK_ROLE_TEXT; + case accessibility::AccessibleRole::TEXT_FRAME: + return ATK_ROLE_PANEL; + case accessibility::AccessibleRole::TOGGLE_BUTTON: + return ATK_ROLE_TOGGLE_BUTTON; + case accessibility::AccessibleRole::TOOL_BAR: + return ATK_ROLE_TOOL_BAR; + case accessibility::AccessibleRole::TOOL_TIP: + return ATK_ROLE_TOOL_TIP; + case accessibility::AccessibleRole::TREE: + return ATK_ROLE_TREE; + case accessibility::AccessibleRole::VIEW_PORT: + return ATK_ROLE_VIEWPORT; + case accessibility::AccessibleRole::WINDOW: + return ATK_ROLE_WINDOW; + case accessibility::AccessibleRole::BUTTON_DROPDOWN: + { + if (nStates & css::accessibility::AccessibleStateType::CHECKABLE) + return ATK_ROLE_TOGGLE_BUTTON; + return ATK_ROLE_PUSH_BUTTON; + } + case accessibility::AccessibleRole::BUTTON_MENU: +#if ATK_CHECK_VERSION(2, 46, 0) + return ATK_ROLE_PUSH_BUTTON_MENU; +#else + return ATK_ROLE_PUSH_BUTTON; +#endif + case accessibility::AccessibleRole::CAPTION: + return ATK_ROLE_CAPTION; + case accessibility::AccessibleRole::CHART: + return ATK_ROLE_CHART; + case accessibility::AccessibleRole::EDIT_BAR: + return ATK_ROLE_EDITBAR; + case accessibility::AccessibleRole::FORM: + return ATK_ROLE_FORM; + case accessibility::AccessibleRole::IMAGE_MAP: + return ATK_ROLE_IMAGE_MAP; + case accessibility::AccessibleRole::NOTE: + return ATK_ROLE_COMMENT; + case accessibility::AccessibleRole::PAGE: + return ATK_ROLE_PAGE; + case accessibility::AccessibleRole::RULER: + return ATK_ROLE_RULER; + case accessibility::AccessibleRole::SECTION: + return ATK_ROLE_SECTION; + case accessibility::AccessibleRole::TREE_ITEM: + return ATK_ROLE_TREE_ITEM; + case accessibility::AccessibleRole::TREE_TABLE: + return ATK_ROLE_TREE_TABLE; + case accessibility::AccessibleRole::COMMENT: + return ATK_ROLE_COMMENT; + case accessibility::AccessibleRole::COMMENT_END: + return ATK_ROLE_UNKNOWN; + case accessibility::AccessibleRole::DOCUMENT_PRESENTATION: + return ATK_ROLE_DOCUMENT_PRESENTATION; + case accessibility::AccessibleRole::DOCUMENT_SPREADSHEET: + return ATK_ROLE_DOCUMENT_SPREADSHEET; + case accessibility::AccessibleRole::DOCUMENT_TEXT: + return ATK_ROLE_DOCUMENT_TEXT; + case accessibility::AccessibleRole::STATIC: + return ATK_ROLE_STATIC; + case accessibility::AccessibleRole::NOTIFICATION: + return ATK_ROLE_NOTIFICATION; + default: + SAL_WARN("vcl.gtk", "Unmapped accessible role: " << nRole); + return ATK_ROLE_UNKNOWN; + } +} + +/*****************************************************************************/ + +extern "C" { + +/*****************************************************************************/ + +static const gchar* +wrapper_get_name( AtkObject *atk_obj ) +{ + AtkObjectWrapper *obj = ATK_OBJECT_WRAPPER (atk_obj); + + if( obj->mpContext.is() ) + { + try { + OString aName = + OUStringToOString( + obj->mpContext->getAccessibleName(), + RTL_TEXTENCODING_UTF8); + + int nCmp = atk_obj->name ? rtl_str_compare( atk_obj->name, aName.getStr() ) : -1; + if( nCmp != 0 ) + { + if( atk_obj->name ) + g_free(atk_obj->name); + atk_obj->name = g_strdup(aName.getStr()); + + return atk_obj->name; + } + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleName()" ); + } + } + + return ATK_OBJECT_CLASS (parent_class)->get_name(atk_obj); +} + +/*****************************************************************************/ + +static const gchar* +wrapper_get_description( AtkObject *atk_obj ) +{ + AtkObjectWrapper *obj = ATK_OBJECT_WRAPPER (atk_obj); + + if( obj->mpContext.is() ) + { + try { + OString aDescription = + OUStringToOString( + obj->mpContext->getAccessibleDescription(), + RTL_TEXTENCODING_UTF8); + + g_free(atk_obj->description); + atk_obj->description = g_strdup(aDescription.getStr()); + + return atk_obj->description; + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleDescription()" ); + } + } + + return ATK_OBJECT_CLASS (parent_class)->get_description(atk_obj); + +} + +/*****************************************************************************/ + +static AtkAttributeSet * +wrapper_get_attributes( AtkObject *atk_obj ) +{ + AtkObjectWrapper *obj = ATK_OBJECT_WRAPPER( atk_obj ); + AtkAttributeSet *pSet = nullptr; + + try + { + uno::Reference< accessibility::XAccessibleExtendedAttributes > + xExtendedAttrs( obj->mpContext, uno::UNO_QUERY ); + if( xExtendedAttrs.is() ) + pSet = attribute_set_new_from_extended_attributes( xExtendedAttrs ); + } + catch(const uno::Exception&) + { + g_warning( "Exception in getAccessibleAttributes()" ); + } + + return pSet; +} + +/*****************************************************************************/ + +static gint +wrapper_get_n_children( AtkObject *atk_obj ) +{ + AtkObjectWrapper *obj = ATK_OBJECT_WRAPPER (atk_obj); + + if (obj->mpSysObjChild) + return 1; + + gint n = 0; + + if( obj->mpContext.is() ) + { + try { + sal_Int64 nChildCount = obj->mpContext->getAccessibleChildCount(); + if (nChildCount > std::numeric_limits::max()) + { + SAL_WARN("vcl.gtk", "wrapper_get_n_children: Child count exceeds maximum gint value, " + "returning max gint."); + nChildCount = std::numeric_limits::max(); + } + n = nChildCount; + } + catch(const uno::Exception&) { + TOOLS_WARN_EXCEPTION( "vcl", "Exception" ); + } + } + + return n; +} + +/*****************************************************************************/ + +static AtkObject * +wrapper_ref_child( AtkObject *atk_obj, + gint i ) +{ + SolarMutexGuard aGuard; + + AtkObjectWrapper *obj = ATK_OBJECT_WRAPPER (atk_obj); + + if (obj->mpSysObjChild) + { + g_object_ref(obj->mpSysObjChild); + return obj->mpSysObjChild; + } + + AtkObject* child = nullptr; + + // see comments above atk_object_wrapper_remove_child + if( -1 < i && obj->index_of_child_about_to_be_removed == i ) + { + g_object_ref( obj->child_about_to_be_removed ); + return obj->child_about_to_be_removed; + } + + if( obj->mpContext.is() ) + { + try { + uno::Reference< accessibility::XAccessible > xAccessible = + obj->mpContext->getAccessibleChild( i ); + + child = atk_object_wrapper_ref( xAccessible ); + } + catch(const uno::Exception&) { + TOOLS_WARN_EXCEPTION( "vcl", "getAccessibleChild"); + } + } + + return child; +} + +/*****************************************************************************/ + +static gint +wrapper_get_index_in_parent( AtkObject *atk_obj ) +{ + SolarMutexGuard aGuard; + + AtkObjectWrapper *obj = ATK_OBJECT_WRAPPER (atk_obj); + + //if we're a native GtkDrawingArea with custom a11y, use the default toolkit a11y + if (obj->mpOrig) + return atk_object_get_index_in_parent(obj->mpOrig); + + gint i = -1; + + if( obj->mpContext.is() ) + { + try { + sal_Int64 nIndex = obj->mpContext->getAccessibleIndexInParent(); + if (nIndex > std::numeric_limits::max()) + { + // use -2 when the child index is too large to fit into 32 bit to neither use the + // valid index of another child nor -1, which would e.g. make Orca interpret the + // object as being a zombie + SAL_WARN("vcl.gtk", "wrapper_get_index_in_parent: Child index exceeds maximum gint value, " + "returning -2."); + nIndex = -2; + } + i = nIndex; + } + catch(const uno::Exception&) { + g_warning( "Exception in getAccessibleIndexInParent()" ); + } + } + return i; +} + +/*****************************************************************************/ + +AtkRelation* +atk_object_wrapper_relation_new(const accessibility::AccessibleRelation& rRelation) +{ + sal_uInt32 nTargetCount = rRelation.TargetSet.getLength(); + + std::vector aTargets; + + for (const auto& rTarget : rRelation.TargetSet) + { + uno::Reference< accessibility::XAccessible > xAccessible( rTarget, uno::UNO_QUERY ); + aTargets.push_back(atk_object_wrapper_ref(xAccessible)); + } + + AtkRelation *pRel = + atk_relation_new( + aTargets.data(), nTargetCount, + mapRelationType( rRelation.RelationType ) + ); + + return pRel; +} + +static AtkRelationSet * +wrapper_ref_relation_set( AtkObject *atk_obj ) +{ + AtkObjectWrapper *obj = ATK_OBJECT_WRAPPER (atk_obj); + + //if we're a native GtkDrawingArea with custom a11y, use the default toolkit relation set impl + if (obj->mpOrig) + return atk_object_ref_relation_set(obj->mpOrig); + + AtkRelationSet *pSet = atk_relation_set_new(); + + if( obj->mpContext.is() ) + { + try { + uno::Reference< accessibility::XAccessibleRelationSet > xRelationSet( + obj->mpContext->getAccessibleRelationSet() + ); + + sal_Int32 nRelations = xRelationSet.is() ? xRelationSet->getRelationCount() : 0; + for( sal_Int32 n = 0; n < nRelations; n++ ) + { + AtkRelation *pRel = atk_object_wrapper_relation_new(xRelationSet->getRelation(n)); + atk_relation_set_add(pSet, pRel); + g_object_unref(pRel); + } + } + catch(const uno::Exception &) { + g_object_unref( G_OBJECT( pSet ) ); + pSet = nullptr; + } + } + + return pSet; +} + +static AtkStateSet * +wrapper_ref_state_set( AtkObject *atk_obj ) +{ + AtkObjectWrapper *obj = ATK_OBJECT_WRAPPER (atk_obj); + AtkStateSet *pSet = atk_state_set_new(); + + if( obj->mpContext.is() ) + { + try { + sal_Int64 nStateSet = obj->mpContext->getAccessibleStateSet(); + + if( nStateSet ) + { + for (int i=0; i<63; ++i) + { + // ATK_STATE_LAST_DEFINED is used to check if the state + // is unmapped, do not report it to Atk + sal_Int64 nState = sal_Int64(1) << i; + if ( (nStateSet & nState) && mapAtkState( nState ) != ATK_STATE_LAST_DEFINED ) + atk_state_set_add_state( pSet, mapAtkState( nState ) ); + } + + // We need to emulate FOCUS state for menus, menu-items etc. + if( atk_obj == atk_get_focus_object() ) + atk_state_set_add_state( pSet, ATK_STATE_FOCUSED ); +/* FIXME - should we do this ? + else + atk_state_set_remove_state( pSet, ATK_STATE_FOCUSED ); +*/ + } + } + + catch(const uno::Exception &) { + g_warning( "Exception in wrapper_ref_state_set" ); + atk_state_set_add_state( pSet, ATK_STATE_DEFUNCT ); + } + } + else + atk_state_set_add_state( pSet, ATK_STATE_DEFUNCT ); + + return pSet; +} + +/*****************************************************************************/ + +static void +atk_object_wrapper_finalize (GObject *obj) +{ + AtkObjectWrapper *pWrap = ATK_OBJECT_WRAPPER (obj); + + if( pWrap->mpAccessible.is() ) + { + ooo_wrapper_registry_remove( pWrap->mpAccessible ); + SolarMutexGuard aGuard; + pWrap->mpAccessible.clear(); + } + + atk_object_wrapper_dispose( pWrap ); + + parent_class->finalize( obj ); +} + +static void +atk_object_wrapper_class_init (AtkObjectWrapperClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS( klass ); + AtkObjectClass *atk_class = ATK_OBJECT_CLASS( klass ); + + parent_class = static_cast(g_type_class_peek_parent (klass)); + + // GObject methods + gobject_class->finalize = atk_object_wrapper_finalize; + + // AtkObject methods + atk_class->get_name = wrapper_get_name; + atk_class->get_description = wrapper_get_description; + atk_class->get_attributes = wrapper_get_attributes; + atk_class->get_n_children = wrapper_get_n_children; + atk_class->ref_child = wrapper_ref_child; + atk_class->get_index_in_parent = wrapper_get_index_in_parent; + atk_class->ref_relation_set = wrapper_ref_relation_set; + atk_class->ref_state_set = wrapper_ref_state_set; + + AtkObjectClass* orig_atk_klass = static_cast(g_type_class_ref(ATK_TYPE_OBJECT)); + // tdf#150496 we want to inherit from GtkAccessible because gtk assumes it can cast to GtkAccessible + // but we want the original behaviour we got from atk_object_real_get_parent when we inherited + // from AtkObject + atk_class->get_parent = orig_atk_klass->get_parent; + // and likewise for focus_event + atk_class->focus_event = orig_atk_klass->focus_event; + g_type_class_unref(orig_atk_klass); +} + +static void +atk_object_wrapper_init (AtkObjectWrapper *wrapper, + AtkObjectWrapperClass*) +{ + wrapper->mpAction = nullptr; + wrapper->mpComponent = nullptr; + wrapper->mpEditableText = nullptr; + wrapper->mpHypertext = nullptr; + wrapper->mpImage = nullptr; + wrapper->mpSelection = nullptr; + wrapper->mpTable = nullptr; + wrapper->mpTableSelection = nullptr; + wrapper->mpText = nullptr; + wrapper->mpValue = nullptr; +} + +} // extern "C" + +GType +atk_object_wrapper_get_type() +{ + static GType type = 0; + + if (!type) + { + static const GTypeInfo typeInfo = + { + sizeof (AtkObjectWrapperClass), + nullptr, + nullptr, + reinterpret_cast(atk_object_wrapper_class_init), + nullptr, + nullptr, + sizeof (AtkObjectWrapper), + 0, + reinterpret_cast(atk_object_wrapper_init), + nullptr + } ; + type = g_type_register_static (GTK_TYPE_WIDGET_ACCESSIBLE, + "OOoAtkObj", + &typeInfo, GTypeFlags(0)) ; + } + return type; +} + +static bool +isOfType( uno::XInterface *pInterface, const uno::Type & rType ) +{ + g_return_val_if_fail( pInterface != nullptr, false ); + + bool bIs = false; + try { + uno::Any aRet = pInterface->queryInterface( rType ); + + bIs = ( ( typelib_TypeClass_INTERFACE == aRet.pType->eTypeClass ) && + ( aRet.pReserved != nullptr ) ); + } catch( const uno::Exception &) { } + + return bIs; +} + +// Whether AtkTableCell can be supported for the interface. +// Returns true if the corresponding XAccessible has role TABLE_CELL +// and an XAccessibleTable as parent. +static bool isTableCell(uno::XInterface* pInterface) +{ + g_return_val_if_fail(pInterface != nullptr, false); + + try { + auto aType = cppu::UnoType::get().getTypeLibType(); + uno::Any aAcc = pInterface->queryInterface(aType); + + css::uno::Reference xAcc; + aAcc >>= xAcc; + if (!xAcc.is()) + return false; + + css::uno::Reference xContext = xAcc->getAccessibleContext(); + if (!xContext.is() || !(xContext->getAccessibleRole() == accessibility::AccessibleRole::TABLE_CELL)) + return false; + + css::uno::Reference xParent = xContext->getAccessibleParent(); + if (!xParent.is()) + return false; + css::uno::Reference xParentContext = xParent->getAccessibleContext(); + if (!xParentContext.is()) + return false; + + css::uno::Reference xTable(xParentContext, uno::UNO_QUERY); + return xTable.is(); + } + catch(const uno::Exception &) + { + g_warning("Exception in isTableCell()"); + } + + return false; +} + +extern "C" { +typedef GType (* GetGIfaceType ) (); +} +const struct { + const char *name; + GInterfaceInitFunc const aInit; + GetGIfaceType const aGetGIfaceType; + const uno::Type & (*aGetUnoType) (); +} aTypeTable[] = { +// re-location heaven: + { + "Comp", reinterpret_cast(componentIfaceInit), + atk_component_get_type, + cppu::UnoType::get + }, + { + "Act", reinterpret_cast(actionIfaceInit), + atk_action_get_type, + cppu::UnoType::get + }, + { + "Txt", reinterpret_cast(textIfaceInit), + atk_text_get_type, + cppu::UnoType::get + }, + { + "Val", reinterpret_cast(valueIfaceInit), + atk_value_get_type, + cppu::UnoType::get + }, + { + "Tab", reinterpret_cast(tableIfaceInit), + atk_table_get_type, + cppu::UnoType::get + }, + { + "Cell", reinterpret_cast(tablecellIfaceInit), + atk_table_cell_get_type, + // there is no UNO a11y interface for table cells, so this case is handled separately below + nullptr + }, + { + "Edt", reinterpret_cast(editableTextIfaceInit), + atk_editable_text_get_type, + cppu::UnoType::get + }, + { + "Img", reinterpret_cast(imageIfaceInit), + atk_image_get_type, + cppu::UnoType::get + }, + { + "Hyp", reinterpret_cast(hypertextIfaceInit), + atk_hypertext_get_type, + cppu::UnoType::get + }, + { + "Sel", reinterpret_cast(selectionIfaceInit), + atk_selection_get_type, + cppu::UnoType::get + } + // AtkDocument is a nastily broken interface (so far) + // we could impl. get_document_type perhaps though. +}; + +const int aTypeTableSize = G_N_ELEMENTS( aTypeTable ); + +static GType +ensureTypeFor( uno::XInterface *pAccessible ) +{ + int i; + bool bTypes[ aTypeTableSize ] = { false, }; + OStringBuffer aTypeNameBuf( "OOoAtkObj" ); + + for( i = 0; i < aTypeTableSize; i++ ) + { + if(!g_strcmp0(aTypeTable[i].name, "Cell")) + { + // there is no UNO interface for table cells, but AtkTableCell can be supported + // for table cells via the methods of the parent that is a table + if (isTableCell(pAccessible)) + { + aTypeNameBuf.append(aTypeTable[i].name); + bTypes[i] = true; + } + } + else if (isOfType( pAccessible, aTypeTable[i].aGetUnoType() ) ) + { + aTypeNameBuf.append(aTypeTable[i].name); + bTypes[i] = true; + } + } + + OString aTypeName = aTypeNameBuf.makeStringAndClear(); + GType nType = g_type_from_name( aTypeName.getStr() ); + if( nType == G_TYPE_INVALID ) + { + GTypeInfo aTypeInfo = { + sizeof( AtkObjectWrapperClass ), + nullptr, nullptr, nullptr, nullptr, nullptr, + sizeof( AtkObjectWrapper ), + 0, nullptr, nullptr + } ; + nType = g_type_register_static( ATK_TYPE_OBJECT_WRAPPER, + aTypeName.getStr(), &aTypeInfo, + GTypeFlags(0) ) ; + + for( int j = 0; j < aTypeTableSize; j++ ) + if( bTypes[j] ) + { + GInterfaceInfo aIfaceInfo = { nullptr, nullptr, nullptr }; + aIfaceInfo.interface_init = aTypeTable[j].aInit; + g_type_add_interface_static (nType, aTypeTable[j].aGetGIfaceType(), + &aIfaceInfo); + } + } + return nType; +} + +AtkObject * +atk_object_wrapper_ref( const uno::Reference< accessibility::XAccessible > &rxAccessible, bool create ) +{ + g_return_val_if_fail( bool(rxAccessible), nullptr ); + + AtkObject *obj = ooo_wrapper_registry_get(rxAccessible); + if( obj ) + { + g_object_ref( obj ); + return obj; + } + + if( create ) + return atk_object_wrapper_new( rxAccessible ); + + return nullptr; +} + +AtkObject * +atk_object_wrapper_new( const css::uno::Reference< css::accessibility::XAccessible >& rxAccessible, + AtkObject* parent, AtkObject* orig ) +{ + g_return_val_if_fail( bool(rxAccessible), nullptr ); + + AtkObjectWrapper *pWrap = nullptr; + + try { + uno::Reference< accessibility::XAccessibleContext > xContext(rxAccessible->getAccessibleContext()); + + g_return_val_if_fail( bool(xContext), nullptr ); + + GType nType = ensureTypeFor( xContext.get() ); + gpointer obj = g_object_new( nType, nullptr); + + pWrap = ATK_OBJECT_WRAPPER( obj ); + pWrap->mpAccessible = rxAccessible; + + pWrap->index_of_child_about_to_be_removed = -1; + pWrap->child_about_to_be_removed = nullptr; + + pWrap->mpContext = xContext; + pWrap->mpOrig = orig; + + AtkObject* atk_obj = ATK_OBJECT(pWrap); + atk_obj->role = mapToAtkRole(xContext->getAccessibleRole(), xContext->getAccessibleStateSet()); + atk_obj->accessible_parent = parent; + + ooo_wrapper_registry_add( rxAccessible, atk_obj ); + + if( parent ) + g_object_ref( atk_obj->accessible_parent ); + else + { + /* gail_focus_tracker remembers the focused object at the first + * parent in the hierarchy that is a Gtk+ widget, but at the time the + * event gets processed (at idle), it may be too late to create the + * hierarchy, so doing it now .. + */ + uno::Reference< accessibility::XAccessible > xParent( xContext->getAccessibleParent() ); + + if( xParent.is() ) + atk_obj->accessible_parent = atk_object_wrapper_ref( xParent ); + } + + // Attach a listener to the UNO object if it's not TRANSIENT + sal_Int64 nStateSet( xContext->getAccessibleStateSet() ); + if( ! (nStateSet & accessibility::AccessibleStateType::TRANSIENT ) ) + { + uno::Reference< accessibility::XAccessibleEventBroadcaster > xBroadcaster(xContext, uno::UNO_QUERY); + if( xBroadcaster.is() ) + { + uno::Reference xListener(new AtkListener(pWrap)); + xBroadcaster->addAccessibleEventListener(xListener); + } + else + OSL_ASSERT( false ); + } + + static auto func = reinterpret_cast(dlsym(nullptr, "atk_object_set_accessible_id")); + if (func) + { + css::uno::Reference xContext2(xContext, css::uno::UNO_QUERY); + if( xContext2.is() ) + { + OString aId = OUStringToOString( xContext2->getAccessibleId(), RTL_TEXTENCODING_UTF8); + (*func)(atk_obj, aId.getStr()); + } + } + + // tdf#141197 if we have a sysobj child then include that in the hierarchy + if (UnoWrapperBase* pWrapper = UnoWrapperBase::GetUnoWrapper()) + { + css::uno::Reference xAWTWindow(rxAccessible, css::uno::UNO_QUERY); + VclPtr xWindow = pWrapper->GetWindow(xAWTWindow); + if (xWindow && xWindow->GetType() == WindowType::SYSTEMCHILDWINDOW) + { + const SystemEnvData* pEnvData = static_cast(xWindow.get())->GetSystemData(); + if (GtkWidget *pSysObj = pEnvData ? static_cast(pEnvData->pWidget) : nullptr) + pWrap->mpSysObjChild = gtk_widget_get_accessible(pSysObj); + } + } + + return ATK_OBJECT( pWrap ); + } + catch (const uno::Exception &) + { + if( pWrap ) + g_object_unref( pWrap ); + + return nullptr; + } +} + +/*****************************************************************************/ + +void atk_object_wrapper_add_child(AtkObjectWrapper* wrapper, AtkObject *child, gint index) +{ + AtkObject *atk_obj = ATK_OBJECT( wrapper ); + + atk_object_set_parent( child, atk_obj ); + g_signal_emit_by_name( atk_obj, "children_changed::add", index, child, nullptr ); +} + +/*****************************************************************************/ + +void atk_object_wrapper_remove_child(AtkObjectWrapper* wrapper, AtkObject *child, gint index) +{ + /* + * the atk-bridge GTK+ module gets back to the event source to ref the child just + * vanishing, so we keep this reference because the semantic on OOo side is different. + */ + wrapper->child_about_to_be_removed = child; + wrapper->index_of_child_about_to_be_removed = index; + + g_signal_emit_by_name( ATK_OBJECT( wrapper ), "children_changed::remove", index, child, nullptr ); + + wrapper->index_of_child_about_to_be_removed = -1; + wrapper->child_about_to_be_removed = nullptr; +} + +/*****************************************************************************/ + +void atk_object_wrapper_set_role(AtkObjectWrapper* wrapper, sal_Int16 role, sal_Int64 nStates) +{ + AtkObject *atk_obj = ATK_OBJECT( wrapper ); + atk_object_set_role(atk_obj, mapToAtkRole(role, nStates)); +} + +/*****************************************************************************/ + +void atk_object_wrapper_dispose(AtkObjectWrapper* wrapper) +{ + wrapper->mpContext.clear(); + wrapper->mpAction.clear(); + wrapper->mpComponent.clear(); + wrapper->mpEditableText.clear(); + wrapper->mpHypertext.clear(); + wrapper->mpImage.clear(); + wrapper->mpSelection.clear(); + wrapper->mpMultiLineText.clear(); + wrapper->mpTable.clear(); + wrapper->mpTableSelection.clear(); + wrapper->mpText.clear(); + wrapper->mpTextMarkup.clear(); + wrapper->mpTextAttributes.clear(); + wrapper->mpValue.clear(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/a11y/atkwrapper.hxx b/vcl/unx/gtk3/a11y/atkwrapper.hxx new file mode 100644 index 0000000000..8d1aeb359b --- /dev/null +++ b/vcl/unx/gtk3/a11y/atkwrapper.hxx @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include + +#include + +#include +#include +#include +#include + +extern "C" { + +namespace com::sun::star::accessibility { + class XAccessibleAction; + class XAccessibleComponent; + class XAccessibleEditableText; + class XAccessibleHypertext; + class XAccessibleImage; + class XAccessibleMultiLineText; + class XAccessibleSelection; + class XAccessibleTable; + class XAccessibleTableSelection; + class XAccessibleText; + class XAccessibleTextMarkup; + class XAccessibleTextAttributes; + class XAccessibleValue; +} + +struct AtkObjectWrapper +{ + GtkWidgetAccessible aParent; + + AtkObject* mpOrig; //if we're a GtkDrawingArea acting as a custom LibreOffice widget, this is the toolkit default impl + AtkObject* mpSysObjChild; //if we're a container for a sysobj, then this is the sysobj native gtk AtkObject + + css::uno::Reference mpAccessible; + css::uno::Reference mpContext; + css::uno::Reference mpAction; + css::uno::Reference mpComponent; + css::uno::Reference + mpEditableText; + css::uno::Reference mpHypertext; + css::uno::Reference mpImage; + css::uno::Reference + mpMultiLineText; + css::uno::Reference mpSelection; + css::uno::Reference mpTable; + css::uno::Reference mpTableSelection; + css::uno::Reference mpText; + css::uno::Reference mpTextMarkup; + css::uno::Reference + mpTextAttributes; + css::uno::Reference mpValue; + + AtkObject *child_about_to_be_removed; + gint index_of_child_about_to_be_removed; +// OString * m_pKeyBindings +}; + +struct AtkObjectWrapperClass +{ + GtkWidgetAccessibleClass aParentClass; +}; + +GType atk_object_wrapper_get_type() G_GNUC_CONST; +AtkObject * atk_object_wrapper_ref( + const css::uno::Reference< css::accessibility::XAccessible >& rxAccessible, + bool create = true ); + +AtkObject * atk_object_wrapper_new( + const css::uno::Reference< css::accessibility::XAccessible >& rxAccessible, + AtkObject* parent = nullptr, AtkObject* orig = nullptr ); + +void atk_object_wrapper_add_child(AtkObjectWrapper* wrapper, AtkObject *child, gint index); +void atk_object_wrapper_remove_child(AtkObjectWrapper* wrapper, AtkObject *child, gint index); +void atk_object_wrapper_set_role(AtkObjectWrapper* wrapper, sal_Int16 role, sal_Int64 nStates); + +void atk_object_wrapper_dispose(AtkObjectWrapper* wrapper); + +AtkStateType mapAtkState( sal_Int64 nState ); + +AtkRelation* atk_object_wrapper_relation_new(const css::accessibility::AccessibleRelation& rRelation); + +void actionIfaceInit(AtkActionIface *iface); +void componentIfaceInit(AtkComponentIface *iface); +void editableTextIfaceInit(AtkEditableTextIface *iface); +void hypertextIfaceInit(AtkHypertextIface *iface); +void imageIfaceInit(AtkImageIface *iface); +void selectionIfaceInit(AtkSelectionIface *iface); +void tableIfaceInit(AtkTableIface *iface); +void tablecellIfaceInit(AtkTableCellIface *iface); +void textIfaceInit(AtkTextIface *iface); +void valueIfaceInit(AtkValueIface *iface); + +} // extern "C" + +#define ATK_TYPE_OBJECT_WRAPPER atk_object_wrapper_get_type() +#define ATK_OBJECT_WRAPPER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST ((obj), ATK_TYPE_OBJECT_WRAPPER, AtkObjectWrapper)) +#define ATK_IS_OBJECT_WRAPPER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), ATK_TYPE_OBJECT_WRAPPER)) + +static inline gchar * +OUStringToGChar(std::u16string_view rString ) +{ + OString aUtf8 = OUStringToOString( rString, RTL_TEXTENCODING_UTF8 ); + return g_strdup( aUtf8.getStr() ); +} + +#define OUStringToConstGChar( string ) OUStringToOString( string, RTL_TEXTENCODING_UTF8 ).getStr() + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/customcellrenderer.cxx b/vcl/unx/gtk3/customcellrenderer.cxx new file mode 100644 index 0000000000..a6559f99ce --- /dev/null +++ b/vcl/unx/gtk3/customcellrenderer.cxx @@ -0,0 +1,316 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include +#include "customcellrenderer.hxx" +#if !GTK_CHECK_VERSION(4, 0, 0) +#include +#endif + +namespace +{ +struct _CustomCellRendererClass : public GtkCellRendererTextClass +{ +}; + +enum +{ + PROP_ID = 10000, + PROP_INSTANCE_TREE_VIEW = 10001 +}; +} + +#if defined __clang__ && GLIB_MAJOR_VERSION == 2 && GLIB_MINOR_VERSION < 68 +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-volatile" +#endif +G_DEFINE_TYPE(CustomCellRenderer, custom_cell_renderer, GTK_TYPE_CELL_RENDERER_TEXT) +#if defined __clang__ && GLIB_MAJOR_VERSION == 2 && GLIB_MINOR_VERSION < 68 +#pragma clang diagnostic pop +#endif + +static void custom_cell_renderer_init(CustomCellRenderer* self) +{ + { + SolarMutexGuard aGuard; + new (&self->device) VclPtr; + } + + // prevent loplugin:unreffun firing on macro generated function + (void)custom_cell_renderer_get_instance_private(self); +} + +static void custom_cell_renderer_get_property(GObject* object, guint param_id, GValue* value, + GParamSpec* pspec) +{ + CustomCellRenderer* cellsurface = CUSTOM_CELL_RENDERER(object); + + switch (param_id) + { + case PROP_ID: + g_value_set_string(value, cellsurface->id); + break; + case PROP_INSTANCE_TREE_VIEW: + g_value_set_pointer(value, cellsurface->instance); + break; + default: + G_OBJECT_CLASS(custom_cell_renderer_parent_class) + ->get_property(object, param_id, value, pspec); + break; + } +} + +static void custom_cell_renderer_set_property(GObject* object, guint param_id, const GValue* value, + GParamSpec* pspec) +{ + CustomCellRenderer* cellsurface = CUSTOM_CELL_RENDERER(object); + + switch (param_id) + { + case PROP_ID: + g_free(cellsurface->id); + cellsurface->id = g_value_dup_string(value); + break; + case PROP_INSTANCE_TREE_VIEW: + cellsurface->instance = g_value_get_pointer(value); + break; + default: + G_OBJECT_CLASS(custom_cell_renderer_parent_class) + ->set_property(object, param_id, value, pspec); + break; + } +} + +static bool custom_cell_renderer_get_preferred_size(GtkCellRenderer* cell, + GtkOrientation orientation, gint* minimum_size, + gint* natural_size); + +#if GTK_CHECK_VERSION(4, 0, 0) +static void custom_cell_renderer_snapshot(GtkCellRenderer* cell, GtkSnapshot* snapshot, + GtkWidget* widget, const GdkRectangle* background_area, + const GdkRectangle* cell_area, + GtkCellRendererState flags); +#endif + +static void custom_cell_renderer_render(GtkCellRenderer* cell, cairo_t* cr, GtkWidget* widget, + const GdkRectangle* background_area, + const GdkRectangle* cell_area, GtkCellRendererState flags); + +static void custom_cell_renderer_finalize(GObject* object) +{ + CustomCellRenderer* cellsurface = CUSTOM_CELL_RENDERER(object); + + g_free(cellsurface->id); + + { + SolarMutexGuard aGuard; + cellsurface->device.disposeAndClear(); + cellsurface->device.~VclPtr(); + } + + G_OBJECT_CLASS(custom_cell_renderer_parent_class)->finalize(object); +} + +static void custom_cell_renderer_get_preferred_width(GtkCellRenderer* cell, GtkWidget* widget, + gint* minimum_size, gint* natural_size) +{ + if (!custom_cell_renderer_get_preferred_size(cell, GTK_ORIENTATION_HORIZONTAL, minimum_size, + natural_size)) + { + // fallback to parent if we're empty + GTK_CELL_RENDERER_CLASS(custom_cell_renderer_parent_class) + ->get_preferred_width(cell, widget, minimum_size, natural_size); + } +} + +static void custom_cell_renderer_get_preferred_height(GtkCellRenderer* cell, GtkWidget* widget, + gint* minimum_size, gint* natural_size) +{ + if (!custom_cell_renderer_get_preferred_size(cell, GTK_ORIENTATION_VERTICAL, minimum_size, + natural_size)) + { + // fallback to parent if we're empty + GTK_CELL_RENDERER_CLASS(custom_cell_renderer_parent_class) + ->get_preferred_height(cell, widget, minimum_size, natural_size); + } +} + +static void custom_cell_renderer_get_preferred_height_for_width(GtkCellRenderer* cell, + GtkWidget* widget, gint /*width*/, + gint* minimum_height, + gint* natural_height) +{ + gtk_cell_renderer_get_preferred_height(cell, widget, minimum_height, natural_height); +} + +static void custom_cell_renderer_get_preferred_width_for_height(GtkCellRenderer* cell, + GtkWidget* widget, gint /*height*/, + gint* minimum_width, + gint* natural_width) +{ + gtk_cell_renderer_get_preferred_width(cell, widget, minimum_width, natural_width); +} + +void custom_cell_renderer_class_init(CustomCellRendererClass* klass) +{ + GtkCellRendererClass* cell_class = GTK_CELL_RENDERER_CLASS(klass); + GObjectClass* object_class = G_OBJECT_CLASS(klass); + + /* Hook up functions to set and get our custom cell renderer properties */ + object_class->get_property = custom_cell_renderer_get_property; + object_class->set_property = custom_cell_renderer_set_property; + + custom_cell_renderer_parent_class = g_type_class_peek_parent(klass); + object_class->finalize = custom_cell_renderer_finalize; + + cell_class->get_preferred_width = custom_cell_renderer_get_preferred_width; + cell_class->get_preferred_height = custom_cell_renderer_get_preferred_height; + cell_class->get_preferred_width_for_height + = custom_cell_renderer_get_preferred_width_for_height; + cell_class->get_preferred_height_for_width + = custom_cell_renderer_get_preferred_height_for_width; + +#if GTK_CHECK_VERSION(4, 0, 0) + cell_class->snapshot = custom_cell_renderer_snapshot; +#else + cell_class->render = custom_cell_renderer_render; +#endif + + g_object_class_install_property( + object_class, PROP_ID, + g_param_spec_string("id", "ID", "The ID of the custom data", nullptr, G_PARAM_READWRITE)); + + g_object_class_install_property( + object_class, PROP_INSTANCE_TREE_VIEW, + g_param_spec_pointer("instance", "Instance", "The GtkInstanceTreeView", G_PARAM_READWRITE)); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_cell_renderer_class_set_accessible_type(cell_class, GTK_TYPE_TEXT_CELL_ACCESSIBLE); +#endif +} + +GtkCellRenderer* custom_cell_renderer_new() +{ + return GTK_CELL_RENDERER(g_object_new(CUSTOM_TYPE_CELL_RENDERER, nullptr)); +} + +bool custom_cell_renderer_get_preferred_size(GtkCellRenderer* cell, GtkOrientation orientation, + gint* minimum_size, gint* natural_size) +{ + GValue value = G_VALUE_INIT; + g_value_init(&value, G_TYPE_STRING); + g_object_get_property(G_OBJECT(cell), "id", &value); + + const char* pStr = g_value_get_string(&value); + + OUString sId(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + + value = G_VALUE_INIT; + g_value_init(&value, G_TYPE_POINTER); + g_object_get_property(G_OBJECT(cell), "instance", &value); + + CustomCellRenderer* cellsurface = CUSTOM_CELL_RENDERER(cell); + + Size aSize; + + gpointer pWidget = g_value_get_pointer(&value); + if (pWidget) + { + SolarMutexGuard aGuard; + custom_cell_renderer_ensure_device(cellsurface, pWidget); + aSize = custom_cell_renderer_get_size(*cellsurface->device, sId, pWidget); + } + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + { + if (minimum_size) + *minimum_size = aSize.Width(); + + if (natural_size) + *natural_size = aSize.Width(); + } + else + { + if (minimum_size) + *minimum_size = aSize.Height(); + + if (natural_size) + *natural_size = aSize.Height(); + } + + return true; +} + +void custom_cell_renderer_render(GtkCellRenderer* cell, cairo_t* cr, GtkWidget* /*widget*/, + const GdkRectangle* /*background_area*/, + const GdkRectangle* cell_area, GtkCellRendererState flags) +{ + GValue value = G_VALUE_INIT; + g_value_init(&value, G_TYPE_STRING); + g_object_get_property(G_OBJECT(cell), "id", &value); + + const char* pStr = g_value_get_string(&value); + OUString sId(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + + value = G_VALUE_INIT; + g_value_init(&value, G_TYPE_POINTER); + g_object_get_property(G_OBJECT(cell), "instance", &value); + + CustomCellRenderer* cellsurface = CUSTOM_CELL_RENDERER(cell); + + gpointer pWidget = g_value_get_pointer(&value); + if (!pWidget) + return; + + SolarMutexGuard aGuard; + + custom_cell_renderer_ensure_device(cellsurface, pWidget); + + Size aSize(cell_area->width, cell_area->height); + // false to not bother setting the bg on resize as we'll do that + // ourself via cairo + cellsurface->device->SetOutputSizePixel(aSize, false); + + cairo_surface_t* pSurface = get_underlying_cairo_surface(*cellsurface->device); + + // fill surface as transparent so it can be blended with the potentially + // selected background + cairo_t* tempcr = cairo_create(pSurface); + cairo_set_source_rgba(tempcr, 0, 0, 0, 0); + cairo_set_operator(tempcr, CAIRO_OPERATOR_SOURCE); + cairo_paint(tempcr); + cairo_destroy(tempcr); + cairo_surface_flush(pSurface); + + custom_cell_renderer_render(*cellsurface->device, tools::Rectangle(Point(0, 0), aSize), + static_cast(flags & GTK_CELL_RENDERER_SELECTED), sId, + pWidget); + + cairo_surface_mark_dirty(pSurface); + + cairo_set_source_surface(cr, pSurface, cell_area->x, cell_area->y); + cairo_paint(cr); +} + +#if GTK_CHECK_VERSION(4, 0, 0) +static void custom_cell_renderer_snapshot(GtkCellRenderer* cell, GtkSnapshot* snapshot, + GtkWidget* widget, const GdkRectangle* background_area, + const GdkRectangle* cell_area, GtkCellRendererState flags) +{ + graphene_rect_t rect = GRAPHENE_RECT_INIT( + static_cast(cell_area->x), static_cast(cell_area->y), + static_cast(cell_area->width), static_cast(cell_area->height)); + cairo_t* cr = gtk_snapshot_append_cairo(GTK_SNAPSHOT(snapshot), &rect); + custom_cell_renderer_render(cell, cr, widget, background_area, cell_area, flags); + cairo_destroy(cr); +} + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/customcellrenderer.hxx b/vcl/unx/gtk3/customcellrenderer.hxx new file mode 100644 index 0000000000..a4d847e7c2 --- /dev/null +++ b/vcl/unx/gtk3/customcellrenderer.hxx @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +struct _CustomCellRenderer +{ + GtkCellRendererText parent; + VclPtr device; + gchar* id; + gpointer instance; +}; + +/* + Provide a mechanism to support custom rendering of cells in a GtkTreeView/GtkComboBox +*/ + +G_DECLARE_FINAL_TYPE(CustomCellRenderer, custom_cell_renderer, CUSTOM, CELL_RENDERER, + GtkCellRendererText) + +#define CUSTOM_TYPE_CELL_RENDERER (custom_cell_renderer_get_type()) + +#define CUSTOM_CELL_RENDERER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), CUSTOM_TYPE_CELL_RENDERER, CustomCellRenderer)) + +#define CUSTOM_IS_CELL_RENDERER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), CUSTOM_TYPE_CELL_RENDERER)) + +GtkCellRenderer* custom_cell_renderer_new(); + +G_END_DECLS + +void custom_cell_renderer_ensure_device(CustomCellRenderer* cellsurface, gpointer user_data); +Size custom_cell_renderer_get_size(VirtualDevice& rDevice, const OUString& rCellId, + gpointer user_data); +void custom_cell_renderer_render(VirtualDevice& rDevice, const tools::Rectangle& rRect, + bool bSelected, const OUString& rId, gpointer user_data); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/fpicker/SalGtkFilePicker.cxx b/vcl/unx/gtk3/fpicker/SalGtkFilePicker.cxx new file mode 100644 index 0000000000..4b93d03617 --- /dev/null +++ b/vcl/unx/gtk3/fpicker/SalGtkFilePicker.cxx @@ -0,0 +1,2091 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include "SalGtkFilePicker.hxx" + +using namespace ::com::sun::star; +using namespace ::com::sun::star::ui::dialogs; +using namespace ::com::sun::star::ui::dialogs::TemplateDescription; +using namespace ::com::sun::star::ui::dialogs::ExtendedFilePickerElementIds; +using namespace ::com::sun::star::ui::dialogs::CommonFilePickerElementIds; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::beans; +using namespace ::com::sun::star::uno; + +struct FilterEntry +{ +protected: + OUString m_sTitle; + OUString m_sFilter; + + css::uno::Sequence< css::beans::StringPair > m_aSubFilters; + +public: + FilterEntry( OUString _aTitle, OUString _aFilter ) + :m_sTitle(std::move( _aTitle )) + ,m_sFilter(std::move( _aFilter )) + { + } + + const OUString& getTitle() const { return m_sTitle; } + const OUString& getFilter() const { return m_sFilter; } + + /// determines if the filter has sub filter (i.e., the filter is a filter group in real) + bool hasSubFilters( ) const; + + /** retrieves the filters belonging to the entry + */ + void getSubFilters( css::uno::Sequence< css::beans::StringPair >& _rSubFilterList ); + + // helpers for iterating the sub filters + const css::beans::StringPair* beginSubFilters() const { return m_aSubFilters.begin(); } + const css::beans::StringPair* endSubFilters() const { return m_aSubFilters.end(); } +}; + +bool FilterEntry::hasSubFilters() const +{ + return m_aSubFilters.hasElements(); +} + +void FilterEntry::getSubFilters( css::uno::Sequence< css::beans::StringPair >& _rSubFilterList ) +{ + _rSubFilterList = m_aSubFilters; +} + +void SalGtkFilePicker::dialog_mapped_cb(GtkWidget *, SalGtkFilePicker *pobjFP) +{ + pobjFP->InitialMapping(); +} + +void SalGtkFilePicker::InitialMapping() +{ + if (!mbPreviewState ) + { + gtk_widget_hide( m_pPreview ); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_file_chooser_set_preview_widget_active( GTK_FILE_CHOOSER( m_pDialog ), false); +#endif + } + gtk_widget_set_size_request (m_pPreview, -1, -1); +} + +SalGtkFilePicker::SalGtkFilePicker( const uno::Reference< uno::XComponentContext >& xContext ) : + SalGtkPicker( xContext ), + SalGtkFilePicker_Base( m_rbHelperMtx ), + m_pVBox ( nullptr ), + mnHID_FolderChange( 0 ), + mnHID_SelectionChange( 0 ), + bVersionWidthUnset( false ), + mbPreviewState( false ), + mbInitialized(false), + mHID_Preview( 0 ), + m_pPreview( nullptr ), + m_pPseudoFilter( nullptr ) +{ + int i; + + for( i = 0; i < TOGGLE_LAST; i++ ) + { + m_pToggles[i] = nullptr; + mbToggleVisibility[i] = false; + } + + for( i = 0; i < BUTTON_LAST; i++ ) + { + m_pButtons[i] = nullptr; + mbButtonVisibility[i] = false; + } + + for( i = 0; i < LIST_LAST; i++ ) + { + m_pHBoxs[i] = nullptr; + m_pLists[i] = nullptr; + m_pListLabels[i] = nullptr; + mbListVisibility[i] = false; + } + + OUString aFilePickerTitle = getResString( FILE_PICKER_TITLE_OPEN ); + + m_pDialog = GTK_WIDGET(g_object_new(GTK_TYPE_FILE_CHOOSER_DIALOG, + "title", OUStringToOString(aFilePickerTitle, RTL_TEXTENCODING_UTF8).getStr(), + "action", GTK_FILE_CHOOSER_ACTION_OPEN, + nullptr)); + gtk_window_set_modal(GTK_WINDOW(m_pDialog), true); + gtk_dialog_set_default_response( GTK_DIALOG (m_pDialog), GTK_RESPONSE_ACCEPT ); + +#if !GTK_CHECK_VERSION(4, 0, 0) +#if ENABLE_GIO + gtk_file_chooser_set_local_only( GTK_FILE_CHOOSER( m_pDialog ), false ); +#endif +#endif + + gtk_file_chooser_set_select_multiple( GTK_FILE_CHOOSER( m_pDialog ), false ); + + m_pVBox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + + // We don't want clickable items to have a huge hit-area + GtkWidget *pHBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + GtkWidget *pThinVBox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_box_pack_end (GTK_BOX( m_pVBox ), pHBox, false, false, 0); + gtk_box_pack_start (GTK_BOX( pHBox ), pThinVBox, false, false, 0); +#else + gtk_box_append(GTK_BOX(m_pVBox), pHBox); + gtk_box_prepend(GTK_BOX(m_pVBox), pThinVBox); +#endif + gtk_widget_show( pHBox ); + gtk_widget_show( pThinVBox ); + + OUString aLabel; + + for( i = 0; i < TOGGLE_LAST; i++ ) + { + m_pToggles[i] = gtk_check_button_new(); + +#define LABEL_TOGGLE( elem ) \ + case elem : \ + aLabel = getResString( CHECKBOX_##elem ); \ + setLabel( CHECKBOX_##elem, aLabel ); \ + break + + switch( i ) { + LABEL_TOGGLE( AUTOEXTENSION ); + LABEL_TOGGLE( PASSWORD ); + LABEL_TOGGLE( GPGENCRYPTION ); + LABEL_TOGGLE( FILTEROPTIONS ); + LABEL_TOGGLE( READONLY ); + LABEL_TOGGLE( LINK ); + LABEL_TOGGLE( PREVIEW ); + LABEL_TOGGLE( SELECTION ); + default: + SAL_WARN( "vcl.gtk", "Handle unknown control " << i); + break; + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_box_pack_end( GTK_BOX( pThinVBox ), m_pToggles[i], false, false, 0 ); +#else + gtk_box_append(GTK_BOX(pThinVBox), m_pToggles[i]); +#endif + } + + for( i = 0; i < LIST_LAST; i++ ) + { + m_pHBoxs[i] = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + + GtkListStore *pListStores[ LIST_LAST ]; + pListStores[i] = gtk_list_store_new (1, G_TYPE_STRING); + m_pLists[i] = gtk_combo_box_new_with_model(GTK_TREE_MODEL(pListStores[i])); + g_object_unref (pListStores[i]); // owned by the widget. + GtkCellRenderer *pCell = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start( + GTK_CELL_LAYOUT(m_pLists[i]), pCell, true); + gtk_cell_layout_set_attributes( + GTK_CELL_LAYOUT (m_pLists[i]), pCell, "text", 0, nullptr); + + m_pListLabels[i] = gtk_label_new( "" ); + +#define LABEL_LIST( elem ) \ + case elem : \ + aLabel = getResString( LISTBOX_##elem##_LABEL ); \ + setLabel( LISTBOX_##elem##_LABEL, aLabel ); \ + break + + switch( i ) + { + LABEL_LIST( VERSION ); + LABEL_LIST( TEMPLATE ); + LABEL_LIST( IMAGE_TEMPLATE ); + LABEL_LIST( IMAGE_ANCHOR ); + default: + SAL_WARN( "vcl.gtk", "Handle unknown control " << i); + break; + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_box_pack_end( GTK_BOX( m_pHBoxs[i] ), m_pLists[i], false, false, 0 ); + gtk_box_pack_end( GTK_BOX( m_pHBoxs[i] ), m_pListLabels[i], false, false, 0 ); +#else + gtk_box_append(GTK_BOX(m_pHBoxs[i]), m_pLists[i]); + gtk_box_append(GTK_BOX(m_pHBoxs[i]), m_pListLabels[i]); +#endif + gtk_label_set_mnemonic_widget( GTK_LABEL(m_pListLabels[i]), m_pLists[i] ); + gtk_box_set_spacing( GTK_BOX( m_pHBoxs[i] ), 12 ); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_box_pack_end( GTK_BOX( m_pVBox ), m_pHBoxs[i], false, false, 0 ); +#else + gtk_box_append(GTK_BOX(m_pVBox), m_pHBoxs[i]); +#endif + } + + aLabel = getResString( FILE_PICKER_FILE_TYPE ); + m_pFilterExpander = gtk_expander_new_with_mnemonic( + OUStringToOString( aLabel, RTL_TEXTENCODING_UTF8 ).getStr()); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_box_pack_end( GTK_BOX( m_pVBox ), m_pFilterExpander, false, true, 0 ); +#else + gtk_box_append(GTK_BOX(m_pVBox), m_pFilterExpander); +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget *scrolled_window = gtk_scrolled_window_new (nullptr, nullptr); + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window), + GTK_SHADOW_IN); +#else + GtkWidget *scrolled_window = gtk_scrolled_window_new(); + gtk_scrolled_window_set_has_frame(GTK_SCROLLED_WINDOW(scrolled_window), true); +#endif + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_add (GTK_CONTAINER (m_pFilterExpander), scrolled_window); +#else + gtk_expander_set_child(GTK_EXPANDER(m_pFilterExpander), scrolled_window); +#endif + gtk_widget_show (scrolled_window); + + m_pFilterStore = gtk_list_store_new (4, G_TYPE_STRING, G_TYPE_STRING, + G_TYPE_STRING, G_TYPE_STRING); + m_pFilterView = gtk_tree_view_new_with_model (GTK_TREE_MODEL(m_pFilterStore)); + gtk_tree_view_set_headers_visible (GTK_TREE_VIEW(m_pFilterView), false); + + GtkCellRenderer *cell = nullptr; + + for (i = 0; i < 2; ++i) + { + GtkTreeViewColumn *column = gtk_tree_view_column_new (); + cell = gtk_cell_renderer_text_new (); + gtk_tree_view_column_set_expand (column, true); + gtk_tree_view_column_pack_start (column, cell, false); + gtk_tree_view_column_set_attributes (column, cell, "text", i, nullptr); + gtk_tree_view_append_column (GTK_TREE_VIEW(m_pFilterView), column); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_add (GTK_CONTAINER (scrolled_window), m_pFilterView); +#else + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scrolled_window), m_pFilterView); +#endif + gtk_widget_show (m_pFilterView); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_file_chooser_set_extra_widget( GTK_FILE_CHOOSER( m_pDialog ), m_pVBox ); +#endif + + m_pPreview = gtk_image_new(); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_file_chooser_set_preview_widget( GTK_FILE_CHOOSER( m_pDialog ), m_pPreview ); +#endif + + g_signal_connect( G_OBJECT( m_pToggles[PREVIEW] ), "toggled", + G_CALLBACK( preview_toggled_cb ), this ); + g_signal_connect (gtk_tree_view_get_selection (GTK_TREE_VIEW(m_pFilterView)), "changed", + G_CALLBACK ( type_changed_cb ), this); + g_signal_connect( G_OBJECT( m_pDialog ), "notify::filter", + G_CALLBACK( filter_changed_cb ), this); + g_signal_connect( G_OBJECT( m_pFilterExpander ), "activate", + G_CALLBACK( expander_changed_cb ), this); + g_signal_connect (G_OBJECT( m_pDialog ), "map", + G_CALLBACK (dialog_mapped_cb), this); + + gtk_widget_show( m_pVBox ); + + PangoLayout *layout = gtk_widget_create_pango_layout (m_pFilterView, nullptr); + guint ypad; + PangoRectangle row_height; + pango_layout_set_markup (layout, "All Files", -1); + pango_layout_get_pixel_extents (layout, nullptr, &row_height); + g_object_unref (layout); + + g_object_get (cell, "ypad", &ypad, nullptr); + guint height = (row_height.height + 2*ypad) * 5; + gtk_widget_set_size_request (m_pFilterView, -1, height); + gtk_widget_set_size_request (m_pPreview, 1, height); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_file_chooser_set_preview_widget_active( GTK_FILE_CHOOSER( m_pDialog ), true); +#endif +} + +// XFilePickerNotifier + +void SAL_CALL SalGtkFilePicker::addFilePickerListener( const uno::Reference& xListener ) +{ + SolarMutexGuard g; + + OSL_ENSURE(!m_xListener.is(), + "SalGtkFilePicker only talks with one listener at a time..."); + m_xListener = xListener; +} + +void SAL_CALL SalGtkFilePicker::removeFilePickerListener( const uno::Reference& ) +{ + SolarMutexGuard g; + + m_xListener.clear(); +} + +// FilePicker Event functions + +void SalGtkFilePicker::impl_fileSelectionChanged( const FilePickerEvent& aEvent ) +{ + if (m_xListener.is()) m_xListener->fileSelectionChanged( aEvent ); +} + +void SalGtkFilePicker::impl_directoryChanged( const FilePickerEvent& aEvent ) +{ + if (m_xListener.is()) m_xListener->directoryChanged( aEvent ); +} + +void SalGtkFilePicker::impl_controlStateChanged( const FilePickerEvent& aEvent ) +{ + if (m_xListener.is()) m_xListener->controlStateChanged( aEvent ); +} + +static bool +isFilterString( std::u16string_view rFilterString, const char *pMatch ) +{ + sal_Int32 nIndex = 0; + bool bIsFilter = true; + + OUString aMatch(OUString::createFromAscii(pMatch)); + + do + { + std::u16string_view aToken = o3tl::getToken(rFilterString, 0, ';', nIndex ); + if( !o3tl::starts_with(aToken, aMatch) ) + { + bIsFilter = false; + break; + } + } + while( nIndex >= 0 ); + + return bIsFilter; +} + +static OUString +shrinkFilterName( const OUString &rFilterName, bool bAllowNoStar = false ) +{ + int i; + int nBracketLen = -1; + int nBracketEnd = -1; + const sal_Unicode *pStr = rFilterName.getStr(); + OUString aRealName = rFilterName; + + for( i = aRealName.getLength() - 1; i > 0; i-- ) + { + if( pStr[i] == ')' ) + nBracketEnd = i; + else if( pStr[i] == '(' ) + { + nBracketLen = nBracketEnd - i; + if( nBracketEnd <= 0 ) + continue; + if( isFilterString( rFilterName.subView( i + 1, nBracketLen - 1 ), "*." ) ) + aRealName = aRealName.replaceAt( i, nBracketLen + 1, u"" ); + else if (bAllowNoStar) + { + if( isFilterString( rFilterName.subView( i + 1, nBracketLen - 1 ), ".") ) + aRealName = aRealName.replaceAt( i, nBracketLen + 1, u"" ); + } + } + } + + return aRealName; +} + +namespace { + + struct FilterTitleMatch + { + protected: + const OUString& rTitle; + + public: + explicit FilterTitleMatch( const OUString& _rTitle ) : rTitle( _rTitle ) { } + + bool operator () ( const FilterEntry& _rEntry ) + { + bool bMatch; + if( !_rEntry.hasSubFilters() ) + // a real filter + bMatch = (_rEntry.getTitle() == rTitle) + || (shrinkFilterName(_rEntry.getTitle()) == rTitle); + else + // a filter group -> search the sub filters + bMatch = + ::std::any_of( + _rEntry.beginSubFilters(), + _rEntry.endSubFilters(), + *this + ); + + return bMatch; + } + bool operator () ( const css::beans::StringPair& _rEntry ) + { + OUString aShrunkName = shrinkFilterName( _rEntry.First ); + return aShrunkName == rTitle; + } + }; +} + +bool SalGtkFilePicker::FilterNameExists( const OUString& rTitle ) +{ + bool bRet = false; + + if( m_pFilterVector ) + bRet = + ::std::any_of( + m_pFilterVector->begin(), + m_pFilterVector->end(), + FilterTitleMatch( rTitle ) + ); + + return bRet; +} + +bool SalGtkFilePicker::FilterNameExists( const css::uno::Sequence< css::beans::StringPair >& _rGroupedFilters ) +{ + bool bRet = false; + + if( m_pFilterVector ) + { + bRet = std::any_of(_rGroupedFilters.begin(), _rGroupedFilters.end(), + [&](const css::beans::StringPair& rFilter) { + return ::std::any_of( m_pFilterVector->begin(), m_pFilterVector->end(), FilterTitleMatch( rFilter.First ) ); }); + } + + return bRet; +} + +void SalGtkFilePicker::ensureFilterVector( const OUString& _rInitialCurrentFilter ) +{ + if( !m_pFilterVector ) + { + m_pFilterVector.reset( new std::vector ); + + // set the first filter to the current filter + if ( m_aCurrentFilter.isEmpty() ) + m_aCurrentFilter = _rInitialCurrentFilter; + } +} + +void SAL_CALL SalGtkFilePicker::appendFilter( const OUString& aTitle, const OUString& aFilter ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + if( FilterNameExists( aTitle ) ) + throw IllegalArgumentException(); + + // ensure that we have a filter list + ensureFilterVector( aTitle ); + + // append the filter + m_pFilterVector->insert( m_pFilterVector->end(), FilterEntry( aTitle, aFilter ) ); +} + +void SAL_CALL SalGtkFilePicker::setCurrentFilter( const OUString& aTitle ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + if( aTitle != m_aCurrentFilter ) + { + m_aCurrentFilter = aTitle; + SetCurFilter( m_aCurrentFilter ); + } + + // TODO m_pImpl->setCurrentFilter( aTitle ); +} + +void SalGtkFilePicker::updateCurrentFilterFromName(const gchar* filtername) +{ + OUString aFilterName(filtername, strlen(filtername), RTL_TEXTENCODING_UTF8); + if (m_pFilterVector) + { + for (auto const& filter : *m_pFilterVector) + { + if (aFilterName == shrinkFilterName(filter.getTitle())) + { + m_aCurrentFilter = filter.getTitle(); + break; + } + } + } +} + +void SalGtkFilePicker::UpdateFilterfromUI() +{ + // Update the filtername from the users selection if they have had a chance to do so. + // If the user explicitly sets a type then use that, if not then take the implicit type + // from the filter of the files glob on which he is currently searching + if (!mnHID_FolderChange || !mnHID_SelectionChange) + return; + + GtkTreeSelection* selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(m_pFilterView)); + GtkTreeIter iter; + GtkTreeModel *model; + if (gtk_tree_selection_get_selected (selection, &model, &iter)) + { + gchar *title; + gtk_tree_model_get (model, &iter, 2, &title, -1); + updateCurrentFilterFromName(title); + g_free (title); + } + else if( GtkFileFilter *filter = gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(m_pDialog))) + { + if (m_pPseudoFilter != filter) + updateCurrentFilterFromName(gtk_file_filter_get_name( filter )); + else + updateCurrentFilterFromName(OUStringToOString( m_aInitialFilter, RTL_TEXTENCODING_UTF8 ).getStr()); + } +} + +OUString SAL_CALL SalGtkFilePicker::getCurrentFilter() +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + UpdateFilterfromUI(); + + return m_aCurrentFilter; +} + +// XFilterGroupManager functions + +void SAL_CALL SalGtkFilePicker::appendFilterGroup( const OUString& /*sGroupTitle*/, const uno::Sequence& aFilters ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + // TODO m_pImpl->appendFilterGroup( sGroupTitle, aFilters ); + // check the names + if( FilterNameExists( aFilters ) ) + // TODO: a more precise exception message + throw IllegalArgumentException(); + + // ensure that we have a filter list + OUString sInitialCurrentFilter; + if( aFilters.hasElements() ) + sInitialCurrentFilter = aFilters[0].First; + + ensureFilterVector( sInitialCurrentFilter ); + + // append the filter + for( const auto& rSubFilter : aFilters ) + m_pFilterVector->insert( m_pFilterVector->end(), FilterEntry( rSubFilter.First, rSubFilter.Second ) ); + +} + +// XFilePicker functions + +void SAL_CALL SalGtkFilePicker::setMultiSelectionMode( sal_Bool bMode ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + gtk_file_chooser_set_select_multiple( GTK_FILE_CHOOSER(m_pDialog), bMode ); +} + +void SAL_CALL SalGtkFilePicker::setDefaultName( const OUString& aName ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + OString aStr = OUStringToOString( aName, RTL_TEXTENCODING_UTF8 ); + GtkFileChooserAction eAction = gtk_file_chooser_get_action( GTK_FILE_CHOOSER( m_pDialog ) ); + + // set_current_name launches a Gtk critical error if called for other than save + if( GTK_FILE_CHOOSER_ACTION_SAVE == eAction ) + gtk_file_chooser_set_current_name( GTK_FILE_CHOOSER( m_pDialog ), aStr.getStr() ); +} + +void SAL_CALL SalGtkFilePicker::setDisplayDirectory( const OUString& rDirectory ) +{ + SolarMutexGuard g; + + implsetDisplayDirectory(rDirectory); +} + +OUString SAL_CALL SalGtkFilePicker::getDisplayDirectory() +{ + SolarMutexGuard g; + + return implgetDisplayDirectory(); +} + +uno::Sequence SAL_CALL SalGtkFilePicker::getFiles() +{ + // no member access => no mutex needed + + uno::Sequence< OUString > aFiles = getSelectedFiles(); + /* + The previous multiselection API design was completely broken + and unimplementable for some heterogeneous pseudo-URIs eg. search: + Thus crop unconditionally to a single selection. + */ + aFiles.realloc (1); + return aFiles; +} + +namespace +{ + +bool lcl_matchFilter( std::u16string_view rFilter, std::u16string_view rExt ) +{ + const sal_Unicode cSep {';'}; + size_t nIdx {0}; + + for (;;) + { + const size_t nBegin = rFilter.find(rExt, nIdx); + + if (nBegin == std::u16string_view::npos) // not found + break; + + // Let nIdx point to end of matched string, useful in order to + // check string boundaries and also for a possible next iteration + nIdx = nBegin + rExt.size(); + + // Check if the found occurrence is an exact match: right side + if (nIdx < rFilter.size() && rFilter[nIdx]!=cSep) + continue; + + // Check if the found occurrence is an exact match: left side + if (nBegin>0 && rFilter[nBegin-1]!=cSep) + continue; + + return true; + } + + return false; +} + +} + +uno::Sequence SAL_CALL SalGtkFilePicker::getSelectedFiles() +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + +#if !GTK_CHECK_VERSION(4, 0, 0) + GSList* pPathList = gtk_file_chooser_get_uris( GTK_FILE_CHOOSER(m_pDialog) ); + int nCount = g_slist_length( pPathList ); +#else + GListModel* pPathList = gtk_file_chooser_get_files(GTK_FILE_CHOOSER(m_pDialog)); + int nCount = g_list_model_get_n_items(pPathList); +#endif + + int nIndex = 0; + + // get the current action setting + GtkFileChooserAction eAction = gtk_file_chooser_get_action( + GTK_FILE_CHOOSER( m_pDialog )); + + uno::Sequence< OUString > aSelectedFiles(nCount); + auto aSelectedFilesRange = asNonConstRange(aSelectedFiles); + + // Convert to OOo +#if !GTK_CHECK_VERSION(4, 0, 0) + for( GSList *pElem = pPathList; pElem; pElem = pElem->next) + { + gchar *pURI = static_cast(pElem->data); +#else + while (gpointer pElem = g_list_model_get_item(pPathList, nIndex)) + { + gchar *pURI = g_file_get_uri(static_cast(pElem)); +#endif + + aSelectedFilesRange[ nIndex ] = uritounicode(pURI); + + if( GTK_FILE_CHOOSER_ACTION_SAVE == eAction ) + { + OUString sFilterName; + sal_Int32 nTokenIndex = 0; + bool bExtensionTypedIn = false; + + GtkTreeSelection* selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(m_pFilterView)); + GtkTreeIter iter; + GtkTreeModel *model; + if (gtk_tree_selection_get_selected (selection, &model, &iter)) + { + gchar *title = nullptr; + gtk_tree_model_get (model, &iter, 2, &title, -1); + if (title) + sFilterName = OUString( title, strlen( title ), RTL_TEXTENCODING_UTF8 ); + else + sFilterName = OUString(); + g_free (title); + } + else + { + if( aSelectedFiles[nIndex].indexOf('.') > 0 ) + { + std::u16string_view sExtension; + nTokenIndex = 0; + do + sExtension = o3tl::getToken(aSelectedFiles[nIndex], 0, '.', nTokenIndex ); + while( nTokenIndex >= 0 ); + + if( sExtension.size() >= 3 ) // 3 = typical/minimum extension length + { + OUString aNewFilter; + OUString aOldFilter = getCurrentFilter(); + bool bChangeFilter = true; + if ( m_pFilterVector) + for (auto const& filter : *m_pFilterVector) + { + if( lcl_matchFilter( filter.getFilter(), Concat2View(OUString::Concat("*.") + sExtension) ) ) + { + if( aNewFilter.isEmpty() ) + aNewFilter = filter.getTitle(); + + if( aOldFilter == filter.getTitle() ) + bChangeFilter = false; + + bExtensionTypedIn = true; + } + } + if( bChangeFilter && bExtensionTypedIn ) + { + gchar* pCurrentName = gtk_file_chooser_get_current_name(GTK_FILE_CHOOSER(m_pDialog)); + setCurrentFilter( aNewFilter ); + gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(m_pDialog), pCurrentName); + g_free(pCurrentName); + } + } + } + + GtkFileFilter *filter = gtk_file_chooser_get_filter(GTK_FILE_CHOOSER(m_pDialog)); + if (m_pPseudoFilter != filter) + { + const gchar* filtername = filter ? gtk_file_filter_get_name(filter) : nullptr; + if (filtername) + sFilterName = OUString(filtername, strlen( filtername ), RTL_TEXTENCODING_UTF8); + else + sFilterName.clear(); + } + else + sFilterName = m_aInitialFilter; + } + + if (m_pFilterVector) + { + auto aVectorIter = ::std::find_if( + m_pFilterVector->begin(), m_pFilterVector->end(), FilterTitleMatch(sFilterName) ); + + OUString aFilter; + if (aVectorIter != m_pFilterVector->end()) + aFilter = aVectorIter->getFilter(); + + nTokenIndex = 0; + OUString sToken; + do + { + sToken = aFilter.getToken( 0, '.', nTokenIndex ); + + if ( sToken.lastIndexOf( ';' ) != -1 ) + { + sToken = sToken.getToken(0, ';'); + break; + } + } + while( nTokenIndex >= 0 ); + + if( !bExtensionTypedIn && ( sToken != "*" ) ) + { + //if the filename does not already have the auto extension, stick it on + OUString sExtension = "." + sToken; + OUString &rBase = aSelectedFilesRange[nIndex]; + sal_Int32 nExtensionIdx = rBase.getLength() - sExtension.getLength(); + SAL_INFO( + "vcl.gtk", + "idx are " << rBase.lastIndexOf(sExtension) << " " + << nExtensionIdx); + + if( rBase.lastIndexOf( sExtension ) != nExtensionIdx ) + rBase += sExtension; + } + } + + } + + nIndex++; + g_free( pURI ); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + g_slist_free( pPathList ); +#else + g_object_unref(pPathList); +#endif + + return aSelectedFiles; +} + +// XExecutableDialog functions + +void SAL_CALL SalGtkFilePicker::setTitle( const OUString& rTitle ) +{ + SolarMutexGuard g; + + implsetTitle(rTitle); +} + +sal_Int16 SAL_CALL SalGtkFilePicker::execute() +{ + SolarMutexGuard g; + + if (!mbInitialized) + { + // tdf#144084 if not initialized default to FILEOPEN_SIMPLE + impl_initialize(nullptr, FILEOPEN_SIMPLE); + assert(mbInitialized); + } + + OSL_ASSERT( m_pDialog != nullptr ); + + sal_Int16 retVal = 0; + + SetFilters(); + + // tdf#84431 - set the filter after the corresponding widget is created + if ( !m_aCurrentFilter.isEmpty() ) + SetCurFilter(m_aCurrentFilter); + +#if !GTK_CHECK_VERSION(4, 0, 0) + mnHID_FolderChange = + g_signal_connect( GTK_FILE_CHOOSER( m_pDialog ), "current-folder-changed", + G_CALLBACK( folder_changed_cb ), static_cast(this) ); +#else + // no replacement in 4-0 that I can see :-( + mnHID_FolderChange = 0; +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + mnHID_SelectionChange = + g_signal_connect( GTK_FILE_CHOOSER( m_pDialog ), "selection-changed", + G_CALLBACK( selection_changed_cb ), static_cast(this) ); +#else + // no replacement in 4-0 that I can see :-( + mnHID_SelectionChange = 0; +#endif + + int btn = GTK_RESPONSE_NO; + + uno::Reference< awt::XExtendedToolkit > xToolkit( + awt::Toolkit::create(m_xContext), + UNO_QUERY_THROW ); + + uno::Reference< frame::XDesktop > xDesktop( + frame::Desktop::create(m_xContext), + UNO_QUERY_THROW ); + + GtkWindow *pParent = GTK_WINDOW(m_pParentWidget); + if (!pParent) + { + SAL_WARN( "vcl.gtk", "no parent widget set"); + pParent = RunDialog::GetTransientFor(); + } + if (pParent) + gtk_window_set_transient_for(GTK_WINDOW(m_pDialog), pParent); + rtl::Reference pRunDialog = new RunDialog(m_pDialog, xToolkit, xDesktop); + while( GTK_RESPONSE_NO == btn ) + { + btn = GTK_RESPONSE_YES; // we don't want to repeat unless user clicks NO for file save. + + gint nStatus = pRunDialog->run(); + switch( nStatus ) + { + case GTK_RESPONSE_ACCEPT: + if( GTK_FILE_CHOOSER_ACTION_SAVE == gtk_file_chooser_get_action( GTK_FILE_CHOOSER( m_pDialog ) ) ) + { + Sequence < OUString > aPathSeq = getFiles(); + if( aPathSeq.getLength() == 1 ) + { + OUString sFileName = aPathSeq[0]; + if (::utl::UCBContentHelper::Exists(sFileName)) + { + INetURLObject aFileObj(sFileName); + + OString baseName( + OUStringToOString( + aFileObj.getName( + INetURLObject::LAST_SEGMENT, + true, + INetURLObject::DecodeMechanism::WithCharset + ), + RTL_TEXTENCODING_UTF8 + ) + ); + OString aMsg( + OUStringToOString( + getResString( FILE_PICKER_OVERWRITE_PRIMARY ), + RTL_TEXTENCODING_UTF8 + ) + ); + OString toReplace("$filename$"_ostr); + + aMsg = aMsg.replaceAt( + aMsg.indexOf( toReplace ), + toReplace.getLength(), + baseName + ); + + GtkWidget *dlg = gtk_message_dialog_new( nullptr, + GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_YES_NO, + "%s", + aMsg.getStr() + ); + + GtkWidget* pOkButton = gtk_dialog_get_widget_for_response(GTK_DIALOG(dlg), GTK_RESPONSE_YES); + GtkStyleContext* pStyleContext = gtk_widget_get_style_context(pOkButton); + gtk_style_context_add_class(pStyleContext, "destructive-action"); + + sal_Int32 nSegmentCount = aFileObj.getSegmentCount(); + if (nSegmentCount >= 2) + { + OString dirName( + OUStringToOString( + aFileObj.getName( + nSegmentCount-2, + true, + INetURLObject::DecodeMechanism::WithCharset + ), + RTL_TEXTENCODING_UTF8 + ) + ); + + aMsg = + OUStringToOString( + getResString( FILE_PICKER_OVERWRITE_SECONDARY ), + RTL_TEXTENCODING_UTF8 + ); + + toReplace = "$dirname$"_ostr; + + aMsg = aMsg.replaceAt( + aMsg.indexOf( toReplace ), + toReplace.getLength(), + dirName + ); + + gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG( dlg ), "%s", aMsg.getStr() ); + } + + gtk_window_set_title( GTK_WINDOW( dlg ), + OUStringToOString(getResString(FILE_PICKER_TITLE_SAVE ), + RTL_TEXTENCODING_UTF8 ).getStr() ); + gtk_window_set_transient_for(GTK_WINDOW(dlg), GTK_WINDOW(m_pDialog)); + rtl::Reference pAnotherDialog = new RunDialog(dlg, xToolkit, xDesktop); + btn = pAnotherDialog->run(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_destroy(dlg); +#else + gtk_window_destroy(GTK_WINDOW(dlg)); +#endif + } + + if( btn == GTK_RESPONSE_YES ) + retVal = ExecutableDialogResults::OK; + } + } + else + retVal = ExecutableDialogResults::OK; + break; + + case GTK_RESPONSE_CANCEL: + retVal = ExecutableDialogResults::CANCEL; + break; + + case 1: //PLAY + { + FilePickerEvent evt; + evt.ElementId = PUSHBUTTON_PLAY; + impl_controlStateChanged( evt ); + btn = GTK_RESPONSE_NO; + } + break; + + default: + retVal = 0; + break; + } + } + gtk_widget_hide(m_pDialog); + + if (mnHID_FolderChange) + g_signal_handler_disconnect(GTK_FILE_CHOOSER( m_pDialog ), mnHID_FolderChange); + if (mnHID_SelectionChange) + g_signal_handler_disconnect(GTK_FILE_CHOOSER( m_pDialog ), mnHID_SelectionChange); + + return retVal; +} + +// cf. offapi/com/sun/star/ui/dialogs/ExtendedFilePickerElementIds.idl +GtkWidget *SalGtkFilePicker::getWidget( sal_Int16 nControlId, GType *pType ) +{ + GType tType = GTK_TYPE_CHECK_BUTTON; //prevent warning by initializing + GtkWidget *pWidget = nullptr; + +#define MAP_TOGGLE( elem ) \ + case ExtendedFilePickerElementIds::CHECKBOX_##elem: \ + pWidget = m_pToggles[elem]; tType = GTK_TYPE_CHECK_BUTTON; \ + break +#define MAP_BUTTON( elem ) \ + case CommonFilePickerElementIds::PUSHBUTTON_##elem: \ + pWidget = m_pButtons[elem]; tType = GTK_TYPE_BUTTON; \ + break +#define MAP_EXT_BUTTON( elem ) \ + case ExtendedFilePickerElementIds::PUSHBUTTON_##elem: \ + pWidget = m_pButtons[elem]; tType = GTK_TYPE_BUTTON; \ + break +#define MAP_LIST( elem ) \ + case ExtendedFilePickerElementIds::LISTBOX_##elem: \ + pWidget = m_pLists[elem]; tType = GTK_TYPE_COMBO_BOX; \ + break +#define MAP_LIST_LABEL( elem ) \ + case ExtendedFilePickerElementIds::LISTBOX_##elem##_LABEL: \ + pWidget = m_pListLabels[elem]; tType = GTK_TYPE_LABEL; \ + break + + switch( nControlId ) + { + MAP_TOGGLE( AUTOEXTENSION ); + MAP_TOGGLE( PASSWORD ); + MAP_TOGGLE( GPGENCRYPTION ); + MAP_TOGGLE( FILTEROPTIONS ); + MAP_TOGGLE( READONLY ); + MAP_TOGGLE( LINK ); + MAP_TOGGLE( PREVIEW ); + MAP_TOGGLE( SELECTION ); + MAP_BUTTON( OK ); + MAP_BUTTON( CANCEL ); + MAP_EXT_BUTTON( PLAY ); + MAP_LIST( VERSION ); + MAP_LIST( TEMPLATE ); + MAP_LIST( IMAGE_TEMPLATE ); + MAP_LIST( IMAGE_ANCHOR ); + MAP_LIST_LABEL( VERSION ); + MAP_LIST_LABEL( TEMPLATE ); + MAP_LIST_LABEL( IMAGE_TEMPLATE ); + MAP_LIST_LABEL( IMAGE_ANCHOR ); + case CommonFilePickerElementIds::LISTBOX_FILTER_LABEL: + // the filter list in gtk typically is not labeled, but has a built-in + // tooltip to indicate what it does + break; + default: + SAL_WARN( "vcl.gtk", "Handle unknown control " << nControlId); + break; + } +#undef MAP + + if( pType ) + *pType = tType; + return pWidget; +} + +// XFilePickerControlAccess functions + +static void HackWidthToFirst(GtkComboBox *pWidget) +{ + GtkRequisition requisition; +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_size_request(GTK_WIDGET(pWidget), &requisition); +#else + gtk_widget_get_preferred_size(GTK_WIDGET(pWidget), &requisition, nullptr); +#endif + gtk_widget_set_size_request(GTK_WIDGET(pWidget), requisition.width, -1); +} + +static void ComboBoxAppendText(GtkComboBox *pCombo, std::u16string_view rStr) +{ + GtkTreeIter aIter; + GtkListStore *pStore = GTK_LIST_STORE(gtk_combo_box_get_model(pCombo)); + OString aStr = OUStringToOString(rStr, RTL_TEXTENCODING_UTF8); + gtk_list_store_append(pStore, &aIter); + gtk_list_store_set(pStore, &aIter, 0, aStr.getStr(), -1); +} + +void SalGtkFilePicker::HandleSetListValue(GtkComboBox *pWidget, sal_Int16 nControlAction, const uno::Any& rValue) +{ + switch (nControlAction) + { + case ControlActions::ADD_ITEM: + { + OUString sItem; + rValue >>= sItem; + ComboBoxAppendText(pWidget, sItem); + if (!bVersionWidthUnset) + { + HackWidthToFirst(pWidget); + bVersionWidthUnset = true; + } + } + break; + case ControlActions::ADD_ITEMS: + { + Sequence< OUString > aStringList; + rValue >>= aStringList; + for (const auto& rString : std::as_const(aStringList)) + { + ComboBoxAppendText(pWidget, rString); + if (!bVersionWidthUnset) + { + HackWidthToFirst(pWidget); + bVersionWidthUnset = true; + } + } + } + break; + case ControlActions::DELETE_ITEM: + { + sal_Int32 nPos=0; + rValue >>= nPos; + + GtkTreeIter aIter; + GtkListStore *pStore = GTK_LIST_STORE( + gtk_combo_box_get_model(GTK_COMBO_BOX(pWidget))); + if(gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(pStore), &aIter, nullptr, nPos)) + gtk_list_store_remove(pStore, &aIter); + } + break; + case ControlActions::DELETE_ITEMS: + { + gtk_combo_box_set_active(pWidget, -1); + GtkListStore *pStore = GTK_LIST_STORE( + gtk_combo_box_get_model(GTK_COMBO_BOX(pWidget))); + gtk_list_store_clear(pStore); + } + break; + case ControlActions::SET_SELECT_ITEM: + { + sal_Int32 nPos=0; + rValue >>= nPos; + gtk_combo_box_set_active(pWidget, nPos); + } + break; + default: + SAL_WARN( "vcl.gtk", "undocumented/unimplemented ControlAction for a list " << nControlAction); + break; + } + + //I think its best to make it insensitive unless there is the chance to + //actually select something from the list. + gint nItems = gtk_tree_model_iter_n_children( + gtk_combo_box_get_model(pWidget), nullptr); + gtk_widget_set_sensitive(GTK_WIDGET(pWidget), nItems > 1); +} + +uno::Any SalGtkFilePicker::HandleGetListValue(GtkComboBox *pWidget, sal_Int16 nControlAction) +{ + uno::Any aAny; + switch (nControlAction) + { + case ControlActions::GET_ITEMS: + { + Sequence< OUString > aItemList; + + GtkTreeModel *pTree = gtk_combo_box_get_model(pWidget); + GtkTreeIter iter; + if (gtk_tree_model_get_iter_first(pTree, &iter)) + { + sal_Int32 nSize = gtk_tree_model_iter_n_children( + pTree, nullptr); + + aItemList.realloc(nSize); + auto pItemList = aItemList.getArray(); + for (sal_Int32 i=0; i < nSize; ++i) + { + gchar *item; + gtk_tree_model_get(gtk_combo_box_get_model(pWidget), + &iter, 0, &item, -1); + pItemList[i] = OUString(item, strlen(item), RTL_TEXTENCODING_UTF8); + g_free(item); + (void)gtk_tree_model_iter_next(pTree, &iter); + } + } + aAny <<= aItemList; + } + break; + case ControlActions::GET_SELECTED_ITEM: + { + GtkTreeIter iter; + if (gtk_combo_box_get_active_iter(pWidget, &iter)) + { + gchar *item; + gtk_tree_model_get(gtk_combo_box_get_model(pWidget), + &iter, 0, &item, -1); + OUString sItem(item, strlen(item), RTL_TEXTENCODING_UTF8); + aAny <<= sItem; + g_free(item); + } + } + break; + case ControlActions::GET_SELECTED_ITEM_INDEX: + { + gint nActive = gtk_combo_box_get_active(pWidget); + aAny <<= static_cast< sal_Int32 >(nActive); + } + break; + default: + SAL_WARN( "vcl.gtk", "undocumented/unimplemented ControlAction for a list " << nControlAction); + break; + } + return aAny; +} + +void SAL_CALL SalGtkFilePicker::setValue( sal_Int16 nControlId, sal_Int16 nControlAction, const uno::Any& rValue ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + GType tType; + GtkWidget *pWidget; + + if( !( pWidget = getWidget( nControlId, &tType ) ) ) + SAL_WARN( "vcl.gtk", "enable unknown control " << nControlId); + else if( tType == GTK_TYPE_CHECK_BUTTON) + { + bool bChecked = false; + rValue >>= bChecked; +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_check_button_set_active(GTK_CHECK_BUTTON(pWidget), bChecked); +#else + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(pWidget), bChecked); +#endif + } + else if( tType == GTK_TYPE_COMBO_BOX ) + HandleSetListValue(GTK_COMBO_BOX(pWidget), nControlAction, rValue); + else + { + SAL_WARN( "vcl.gtk", "Can't set value on button / list " << nControlId << " " << nControlAction ); + } +} + +uno::Any SAL_CALL SalGtkFilePicker::getValue( sal_Int16 nControlId, sal_Int16 nControlAction ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + uno::Any aRetval; + + GType tType; + GtkWidget *pWidget; + + if( !( pWidget = getWidget( nControlId, &tType ) ) ) + SAL_WARN( "vcl.gtk", "enable unknown control " << nControlId); + else if( tType == GTK_TYPE_CHECK_BUTTON) + { +#if GTK_CHECK_VERSION(4, 0, 0) + aRetval <<= bool(gtk_check_button_get_active(GTK_CHECK_BUTTON(pWidget))); +#else + aRetval <<= bool(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pWidget))); +#endif + } + else if( tType == GTK_TYPE_COMBO_BOX ) + aRetval = HandleGetListValue(GTK_COMBO_BOX(pWidget), nControlAction); + else + SAL_WARN( "vcl.gtk", "Can't get value on button / list " << nControlId << " " << nControlAction ); + + return aRetval; +} + +void SAL_CALL SalGtkFilePicker::enableControl( sal_Int16 nControlId, sal_Bool bEnable ) +{ + // skip this built-in one which is Enabled by default + if (nControlId == ExtendedFilePickerElementIds::LISTBOX_FILTER_SELECTOR && bEnable) + return; + + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + GtkWidget *pWidget; + + if ( ( pWidget = getWidget( nControlId ) ) ) + { + if( bEnable ) + { + gtk_widget_set_sensitive( pWidget, true ); + } + else + { + gtk_widget_set_sensitive( pWidget, false ); + } + } + else + SAL_WARN( "vcl.gtk", "enable unknown control " << nControlId ); +} + +void SAL_CALL SalGtkFilePicker::setLabel( sal_Int16 nControlId, const OUString& rLabel ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + GType tType; + GtkWidget *pWidget; + + if( !( pWidget = getWidget( nControlId, &tType ) ) ) + { + SAL_WARN_IF(nControlId != CommonFilePickerElementIds::LISTBOX_FILTER_LABEL, + "vcl.gtk", "Set label '" << rLabel << "' on unknown control " << nControlId); + return; + } + + OString aTxt = OUStringToOString( rLabel.replace('~', '_'), RTL_TEXTENCODING_UTF8 ); + if( tType == GTK_TYPE_CHECK_BUTTON || tType == GTK_TYPE_BUTTON || tType == GTK_TYPE_LABEL ) + g_object_set( pWidget, "label", aTxt.getStr(), + "use_underline", true, nullptr ); + else + SAL_WARN( "vcl.gtk", "Can't set label on list"); +} + +OUString SAL_CALL SalGtkFilePicker::getLabel( sal_Int16 nControlId ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + GType tType; + OString aTxt; + GtkWidget *pWidget; + + if( !( pWidget = getWidget( nControlId, &tType ) ) ) + SAL_WARN( "vcl.gtk", "Get label on unknown control " << nControlId); + else if( tType == GTK_TYPE_CHECK_BUTTON || tType == GTK_TYPE_BUTTON || tType == GTK_TYPE_LABEL ) + aTxt = gtk_button_get_label( GTK_BUTTON( pWidget ) ); + else + SAL_WARN( "vcl.gtk", "Can't get label on list"); + + return OStringToOUString( aTxt, RTL_TEXTENCODING_UTF8 ); +} + +// XFilePreview functions + +uno::Sequence SAL_CALL SalGtkFilePicker::getSupportedImageFormats() +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + // TODO return m_pImpl->getSupportedImageFormats(); + return uno::Sequence(); +} + +sal_Int32 SAL_CALL SalGtkFilePicker::getTargetColorDepth() +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + // TODO return m_pImpl->getTargetColorDepth(); + return 0; +} + +sal_Int32 SAL_CALL SalGtkFilePicker::getAvailableWidth() +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + return g_PreviewImageWidth; +} + +sal_Int32 SAL_CALL SalGtkFilePicker::getAvailableHeight() +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + return g_PreviewImageHeight; +} + +void SAL_CALL SalGtkFilePicker::setImage( sal_Int16 /*aImageFormat*/, const uno::Any& /*aImage*/ ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + // TODO m_pImpl->setImage( aImageFormat, aImage ); +} + +void SalGtkFilePicker::implChangeType( GtkTreeSelection *selection ) +{ + OUString aLabel = getResString( FILE_PICKER_FILE_TYPE ); + + GtkTreeIter iter; + GtkTreeModel *model; + if (gtk_tree_selection_get_selected (selection, &model, &iter)) + { + gchar *title; + gtk_tree_model_get (model, &iter, 2, &title, -1); + aLabel += ": " + + OUString( title, strlen(title), RTL_TEXTENCODING_UTF8 ); + g_free (title); + } + gtk_expander_set_label (GTK_EXPANDER (m_pFilterExpander), + OUStringToOString( aLabel, RTL_TEXTENCODING_UTF8 ).getStr()); + FilePickerEvent evt; + evt.ElementId = LISTBOX_FILTER; + impl_controlStateChanged( evt ); +} + +void SalGtkFilePicker::type_changed_cb( GtkTreeSelection *selection, SalGtkFilePicker *pobjFP ) +{ + pobjFP->implChangeType(selection); +} + +void SalGtkFilePicker::unselect_type() +{ + gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(GTK_TREE_VIEW(m_pFilterView))); +} + +void SalGtkFilePicker::expander_changed_cb( GtkExpander *expander, SalGtkFilePicker *pobjFP ) +{ + if (gtk_expander_get_expanded(expander)) + pobjFP->unselect_type(); +} + +void SalGtkFilePicker::filter_changed_cb( GtkFileChooser *, GParamSpec *, + SalGtkFilePicker *pobjFP ) +{ + FilePickerEvent evt; + evt.ElementId = LISTBOX_FILTER; + SAL_INFO( "vcl.gtk", "filter_changed, isn't it great " << pobjFP ); + pobjFP->impl_controlStateChanged( evt ); +} + +void SalGtkFilePicker::folder_changed_cb( GtkFileChooser *, SalGtkFilePicker *pobjFP ) +{ + FilePickerEvent evt; + SAL_INFO( "vcl.gtk", "folder_changed, isn't it great " << pobjFP ); + pobjFP->impl_directoryChanged( evt ); +} + +void SalGtkFilePicker::selection_changed_cb( GtkFileChooser *, SalGtkFilePicker *pobjFP ) +{ + FilePickerEvent evt; + SAL_INFO( "vcl.gtk", "selection_changed, isn't it great " << pobjFP ); + pobjFP->impl_fileSelectionChanged( evt ); +} + +void SalGtkFilePicker::update_preview_cb( GtkFileChooser *file_chooser, SalGtkFilePicker* pobjFP ) +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + bool have_preview = false; + + GtkWidget* preview = pobjFP->m_pPreview; + char* filename = gtk_file_chooser_get_preview_filename( file_chooser ); + + if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pobjFP->m_pToggles[PREVIEW])) && filename && g_file_test(filename, G_FILE_TEST_IS_REGULAR)) + { + GdkPixbuf *pixbuf = gdk_pixbuf_new_from_file_at_size( + filename, + g_PreviewImageWidth, + g_PreviewImageHeight, nullptr ); + + have_preview = ( pixbuf != nullptr ); + + gtk_image_set_from_pixbuf( GTK_IMAGE( preview ), pixbuf ); + if( pixbuf ) + g_object_unref( G_OBJECT( pixbuf ) ); + + } + + gtk_file_chooser_set_preview_widget_active( file_chooser, have_preview ); + + if( filename ) + g_free( filename ); +#else + (void)file_chooser; + (void)pobjFP; +#endif +} + +sal_Bool SAL_CALL SalGtkFilePicker::setShowState( sal_Bool bShowState ) +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + // TODO return m_pImpl->setShowState( bShowState ); + if( bool(bShowState) != mbPreviewState ) + { + if( bShowState ) + { + // Show + if( !mHID_Preview ) + { + mHID_Preview = g_signal_connect( + GTK_FILE_CHOOSER( m_pDialog ), "update-preview", + G_CALLBACK( update_preview_cb ), static_cast(this) ); + } + gtk_widget_show( m_pPreview ); + } + else + { + // Hide + gtk_widget_hide( m_pPreview ); + } + + // also emit the signal + g_signal_emit_by_name( G_OBJECT( m_pDialog ), "update-preview" ); + + mbPreviewState = bShowState; + } + return true; +} + +sal_Bool SAL_CALL SalGtkFilePicker::getShowState() +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + return mbPreviewState; +} + +GtkWidget* SalGtkPicker::GetParentWidget(const uno::Sequence& rArguments) +{ + GtkWidget* pParentWidget = nullptr; + + css::uno::Reference xParentWindow; + if (rArguments.getLength() > 1) + { + rArguments[1] >>= xParentWindow; + } + + if (xParentWindow.is()) + { + if (SalGtkXWindow* pGtkXWindow = dynamic_cast(xParentWindow.get())) + pParentWidget = pGtkXWindow->getGtkWidget(); + else + { + css::uno::Reference xSysDepWin(xParentWindow, css::uno::UNO_QUERY); + if (xSysDepWin.is()) + { + css::uno::Sequence aProcessIdent(16); + rtl_getGlobalProcessId(reinterpret_cast(aProcessIdent.getArray())); + uno::Any aAny = xSysDepWin->getWindowHandle(aProcessIdent, css::lang::SystemDependent::SYSTEM_XWINDOW); + css::awt::SystemDependentXWindow tmp; + aAny >>= tmp; + pParentWidget = GetGtkSalData()->GetGtkDisplay()->findGtkWidgetForNativeHandle(tmp.WindowHandle); + } + } + } + + return pParentWidget; +} + +// XInitialization + +void SAL_CALL SalGtkFilePicker::initialize( const uno::Sequence& aArguments ) +{ + // parameter checking + uno::Any aAny; + if( !aArguments.hasElements() ) + throw lang::IllegalArgumentException( + "no arguments", + static_cast( this ), 1 ); + + aAny = aArguments[0]; + + if( ( aAny.getValueType() != cppu::UnoType::get()) && + (aAny.getValueType() != cppu::UnoType::get()) ) + throw lang::IllegalArgumentException( + "invalid argument type", + static_cast( this ), 1 ); + + sal_Int16 templateId = -1; + aAny >>= templateId; + + impl_initialize(GetParentWidget(aArguments), templateId); +} + +void SalGtkFilePicker::impl_initialize(GtkWidget* pParentWidget, sal_Int16 templateId) +{ + m_pParentWidget = pParentWidget; + + GtkFileChooserAction eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + OString sOpen = getOpenText(); + OString sSave = getSaveText(); + const gchar *first_button_text; + + SolarMutexGuard g; + + // TODO: extract full semantic from + // svtools/source/filepicker/filepicker.cxx (getWinBits) + switch( templateId ) + { + case FILEOPEN_SIMPLE: + eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + first_button_text = sOpen.getStr(); + break; + case FILESAVE_SIMPLE: + eAction = GTK_FILE_CHOOSER_ACTION_SAVE; + first_button_text = sSave.getStr(); + break; + case FILESAVE_AUTOEXTENSION_PASSWORD: + eAction = GTK_FILE_CHOOSER_ACTION_SAVE; + first_button_text = sSave.getStr(); + mbToggleVisibility[PASSWORD] = true; + mbToggleVisibility[GPGENCRYPTION] = true; + // TODO + break; + case FILESAVE_AUTOEXTENSION_PASSWORD_FILTEROPTIONS: + eAction = GTK_FILE_CHOOSER_ACTION_SAVE; + first_button_text = sSave.getStr(); + mbToggleVisibility[PASSWORD] = true; + mbToggleVisibility[GPGENCRYPTION] = true; + mbToggleVisibility[FILTEROPTIONS] = true; + // TODO + break; + case FILESAVE_AUTOEXTENSION_SELECTION: + eAction = GTK_FILE_CHOOSER_ACTION_SAVE; // SELECT_FOLDER ? + first_button_text = sSave.getStr(); + mbToggleVisibility[SELECTION] = true; + // TODO + break; + case FILESAVE_AUTOEXTENSION_TEMPLATE: + eAction = GTK_FILE_CHOOSER_ACTION_SAVE; + first_button_text = sSave.getStr(); + mbListVisibility[TEMPLATE] = true; + // TODO + break; + case FILEOPEN_LINK_PREVIEW_IMAGE_TEMPLATE: + eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + first_button_text = sOpen.getStr(); + mbToggleVisibility[LINK] = true; + mbToggleVisibility[PREVIEW] = true; + mbListVisibility[IMAGE_TEMPLATE] = true; + // TODO + break; + case FILEOPEN_LINK_PREVIEW_IMAGE_ANCHOR: + eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + first_button_text = sOpen.getStr(); + mbToggleVisibility[LINK] = true; + mbToggleVisibility[PREVIEW] = true; + mbListVisibility[IMAGE_ANCHOR] = true; + // TODO + break; + case FILEOPEN_PLAY: + eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + first_button_text = sOpen.getStr(); + mbButtonVisibility[PLAY] = true; + // TODO + break; + case FILEOPEN_LINK_PLAY: + eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + first_button_text = sOpen.getStr(); + mbToggleVisibility[LINK] = true; + mbButtonVisibility[PLAY] = true; + // TODO + break; + case FILEOPEN_READONLY_VERSION: + eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + first_button_text = sOpen.getStr(); + mbToggleVisibility[READONLY] = true; + mbListVisibility[VERSION] = true; + break; + case FILEOPEN_LINK_PREVIEW: + eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + first_button_text = sOpen.getStr(); + mbToggleVisibility[LINK] = true; + mbToggleVisibility[PREVIEW] = true; + // TODO + break; + case FILESAVE_AUTOEXTENSION: + eAction = GTK_FILE_CHOOSER_ACTION_SAVE; + first_button_text = sSave.getStr(); + // TODO + break; + case FILEOPEN_PREVIEW: + eAction = GTK_FILE_CHOOSER_ACTION_OPEN; + first_button_text = sOpen.getStr(); + mbToggleVisibility[PREVIEW] = true; + // TODO + break; + default: + throw lang::IllegalArgumentException( + "Unknown template", + static_cast< XFilePicker2* >( this ), + 1 ); + } + + if( GTK_FILE_CHOOSER_ACTION_SAVE == eAction ) + { + OUString aFilePickerTitle(getResString( FILE_PICKER_TITLE_SAVE )); + gtk_window_set_title ( GTK_WINDOW( m_pDialog ), + OUStringToOString( aFilePickerTitle, RTL_TEXTENCODING_UTF8 ).getStr() ); + } + + gtk_file_chooser_set_action( GTK_FILE_CHOOSER( m_pDialog ), eAction); + m_pButtons[CANCEL] = gtk_dialog_add_button(GTK_DIALOG(m_pDialog), getCancelText().getStr(), GTK_RESPONSE_CANCEL); + mbButtonVisibility[CANCEL] = true; + + if (mbButtonVisibility[PLAY]) + { + OString aPlay = OUStringToOString(getResString(PUSHBUTTON_PLAY), RTL_TEXTENCODING_UTF8); + m_pButtons[PLAY] = gtk_dialog_add_button(GTK_DIALOG(m_pDialog), aPlay.getStr(), 1); + } + + m_pButtons[OK] = gtk_dialog_add_button(GTK_DIALOG(m_pDialog), first_button_text, GTK_RESPONSE_ACCEPT); + mbButtonVisibility[OK] = true; + + gtk_dialog_set_default_response( GTK_DIALOG (m_pDialog), GTK_RESPONSE_ACCEPT ); + + // Setup special flags + for( int nTVIndex = 0; nTVIndex < TOGGLE_LAST; nTVIndex++ ) + { + if( mbToggleVisibility[nTVIndex] ) + gtk_widget_show( m_pToggles[ nTVIndex ] ); + } + + for( int nTVIndex = 0; nTVIndex < LIST_LAST; nTVIndex++ ) + { + if( mbListVisibility[nTVIndex] ) + { + gtk_widget_set_sensitive( m_pLists[ nTVIndex ], false ); + gtk_widget_show( m_pLists[ nTVIndex ] ); + gtk_widget_show( m_pListLabels[ nTVIndex ] ); + gtk_widget_show( m_pHBoxs[ nTVIndex ] ); + } + } + + mbInitialized = true; +} + +void SalGtkFilePicker::preview_toggled_cb( GObject *cb, SalGtkFilePicker* pobjFP ) +{ + if( pobjFP->mbToggleVisibility[PREVIEW] ) + pobjFP->setShowState( gtk_toggle_button_get_active( GTK_TOGGLE_BUTTON( cb ) ) ); +} + +// XCancellable + +void SAL_CALL SalGtkFilePicker::cancel() +{ + SolarMutexGuard g; + + OSL_ASSERT( m_pDialog != nullptr ); + + // TODO m_pImpl->cancel(); +} + +// Misc + +void SalGtkFilePicker::SetCurFilter( const OUString& rFilter ) +{ + // Get all the filters already added +#if GTK_CHECK_VERSION(4, 0, 0) + GListModel *filters = gtk_file_chooser_get_filters(GTK_FILE_CHOOSER(m_pDialog)); +#else + GSList *filters = gtk_file_chooser_list_filters(GTK_FILE_CHOOSER(m_pDialog)); +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + int nIndex = 0; + while (gpointer pElem = g_list_model_get_item(filters, nIndex)) + { + GtkFileFilter* pFilter = static_cast(pElem); + ++nIndex; +#else + for( GSList *iter = filters; iter; iter = iter->next ) + { + GtkFileFilter* pFilter = static_cast( iter->data ); +#endif + const gchar * filtername = gtk_file_filter_get_name( pFilter ); + OUString sFilterName( filtername, strlen( filtername ), RTL_TEXTENCODING_UTF8 ); + + OUString aShrunkName = shrinkFilterName( rFilter ); + if( aShrunkName == sFilterName ) + { + SAL_INFO( "vcl.gtk", "actually setting " << filtername ); + gtk_file_chooser_set_filter( GTK_FILE_CHOOSER( m_pDialog ), pFilter ); + break; + } + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + g_slist_free( filters ); +#else + g_object_unref (filters); +#endif +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +extern "C" +{ +static gboolean +case_insensitive_filter (const GtkFileFilterInfo *filter_info, gpointer data) +{ + bool bRetval = false; + const char *pFilter = static_cast(data); + + g_return_val_if_fail( data != nullptr, false ); + g_return_val_if_fail( filter_info != nullptr, false ); + + if( !filter_info->uri ) + return false; + + const char *pExtn = strrchr( filter_info->uri, '.' ); + if( !pExtn ) + return false; + pExtn++; + + if( !g_ascii_strcasecmp( pFilter, pExtn ) ) + bRetval = true; + + SAL_INFO( "vcl.gtk", "'" << filter_info->uri << "' match extn '" << pExtn << "' vs '" << pFilter << "' yields " << bRetval ); + + return bRetval; +} +} +#endif + +GtkFileFilter* SalGtkFilePicker::implAddFilter( const OUString& rFilter, const OUString& rType ) +{ + GtkFileFilter *filter = gtk_file_filter_new(); + + OUString aShrunkName = shrinkFilterName( rFilter ); + OString aFilterName = OUStringToOString( aShrunkName, RTL_TEXTENCODING_UTF8 ); + gtk_file_filter_set_name( filter, aFilterName.getStr() ); + + OUStringBuffer aTokens; + + bool bAllGlob = rType == "*.*" || rType == "*"; + if (bAllGlob) + gtk_file_filter_add_pattern( filter, "*" ); + else + { + sal_Int32 nIndex = 0; + do + { + OUString aToken = rType.getToken( 0, ';', nIndex ); + // Assume all have the "*." syntax + sal_Int32 nStarDot = aToken.lastIndexOf( "*." ); + if (nStarDot >= 0) + aToken = aToken.copy( nStarDot + 2 ); + if (!aToken.isEmpty()) + { + if (!aTokens.isEmpty()) + aTokens.append(","); + aTokens.append(aToken); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_file_filter_add_suffix(filter, aToken.toUtf8().getStr()); +#else + gtk_file_filter_add_custom (filter, GTK_FILE_FILTER_URI, + case_insensitive_filter, + g_strdup( OUStringToOString(aToken, RTL_TEXTENCODING_UTF8).getStr() ), + g_free ); +#endif + + SAL_INFO( "vcl.gtk", "fustering with " << aToken ); + } +#if OSL_DEBUG_LEVEL > 0 + else + { + g_warning( "Duff filter token '%s'\n", + OUStringToOString( + o3tl::getToken(rType, 0, ';', nIndex ), RTL_TEXTENCODING_UTF8 ).getStr() ); + } +#endif + } + while( nIndex >= 0 ); + } + + gtk_file_chooser_add_filter( GTK_FILE_CHOOSER( m_pDialog ), filter ); + + if (!bAllGlob) + { + GtkTreeIter iter; + gtk_list_store_append (m_pFilterStore, &iter); + gtk_list_store_set (m_pFilterStore, &iter, + 0, OUStringToOString(shrinkFilterName( rFilter, true ), RTL_TEXTENCODING_UTF8).getStr(), + 1, OUStringToOString(aTokens, RTL_TEXTENCODING_UTF8).getStr(), + 2, aFilterName.getStr(), + 3, OUStringToOString(rType, RTL_TEXTENCODING_UTF8).getStr(), + -1); + } + return filter; +} + +void SalGtkFilePicker::implAddFilterGroup( const Sequence< StringPair >& _rFilters ) +{ + // Gtk+ has no filter group concept I think so ... + // implAddFilter( _rFilter, String() ); + for( const auto& rSubFilter : _rFilters ) + implAddFilter( rSubFilter.First, rSubFilter.Second ); +} + +void SalGtkFilePicker::SetFilters() +{ + if (m_aInitialFilter.isEmpty()) + m_aInitialFilter = m_aCurrentFilter; + + OUString sPseudoFilter; + if( GTK_FILE_CHOOSER_ACTION_SAVE == gtk_file_chooser_get_action( GTK_FILE_CHOOSER( m_pDialog ) ) ) + { + std::set aAllFormats; + if( m_pFilterVector ) + { + for (auto & filter : *m_pFilterVector) + { + if( filter.hasSubFilters() ) + { // it's a filter group + css::uno::Sequence< css::beans::StringPair > aSubFilters; + filter.getSubFilters( aSubFilters ); + for( const auto& rSubFilter : std::as_const(aSubFilters) ) + aAllFormats.insert(rSubFilter.Second); + } + else + aAllFormats.insert(filter.getFilter()); + } + } + if (aAllFormats.size() > 1) + { + OUStringBuffer sAllFilter; + for (auto const& format : aAllFormats) + { + if (!sAllFilter.isEmpty()) + sAllFilter.append(";"); + sAllFilter.append(format); + } + sPseudoFilter = getResString(FILE_PICKER_ALLFORMATS); + m_pPseudoFilter = implAddFilter( sPseudoFilter, sAllFilter.makeStringAndClear() ); + } + } + + if( m_pFilterVector ) + { + for (auto & filter : *m_pFilterVector) + { + if( filter.hasSubFilters() ) + { // it's a filter group + + css::uno::Sequence< css::beans::StringPair > aSubFilters; + filter.getSubFilters( aSubFilters ); + + implAddFilterGroup( aSubFilters ); + } + else + { + // it's a single filter + + implAddFilter( filter.getTitle(), filter.getFilter() ); + } + } + } + + // We always hide the expander now and depend on the user using the glob + // list, or type a filename suffix, to select a filter by inference. + gtk_widget_hide(m_pFilterExpander); + + // set the default filter + if (!sPseudoFilter.isEmpty()) + SetCurFilter( sPseudoFilter ); + else if(!m_aCurrentFilter.isEmpty()) + SetCurFilter( m_aCurrentFilter ); + + SAL_INFO( "vcl.gtk", "end setting filters"); +} + +SalGtkFilePicker::~SalGtkFilePicker() +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + SolarMutexGuard g; + + int i; + + for( i = 0; i < TOGGLE_LAST; i++ ) + gtk_widget_destroy( m_pToggles[i] ); + + for( i = 0; i < LIST_LAST; i++ ) + { + gtk_widget_destroy( m_pListLabels[i] ); + gtk_widget_destroy( m_pLists[i] ); + gtk_widget_destroy( m_pHBoxs[i] ); + } + + m_pFilterVector.reset(); + + gtk_widget_destroy( m_pVBox ); +#endif +} + +uno::Reference< ui::dialogs::XFilePicker2 > +GtkInstance::createFilePicker( const css::uno::Reference< css::uno::XComponentContext > &xMSF ) +{ + return uno::Reference< ui::dialogs::XFilePicker2 >( + new SalGtkFilePicker( xMSF ) ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/fpicker/SalGtkFilePicker.hxx b/vcl/unx/gtk3/fpicker/SalGtkFilePicker.hxx new file mode 100644 index 0000000000..fdc701a203 --- /dev/null +++ b/vcl/unx/gtk3/fpicker/SalGtkFilePicker.hxx @@ -0,0 +1,239 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "SalGtkPicker.hxx" + +// Implementation class for the XFilePicker Interface + +struct FilterEntry; +struct ElementEntry_Impl; + + +typedef cppu::WeakComponentImplHelper< + css::ui::dialogs::XFilePickerControlAccess, + css::ui::dialogs::XFilePreview, + css::ui::dialogs::XFilePicker3, + css::lang::XInitialization + > SalGtkFilePicker_Base; + +class SalGtkFilePicker : public SalGtkPicker, public SalGtkFilePicker_Base +{ + public: + + // constructor + SalGtkFilePicker( const css::uno::Reference< css::uno::XComponentContext >& xServiceMgr ); + + // XFilePickerNotifier + + virtual void SAL_CALL addFilePickerListener( const css::uno::Reference< css::ui::dialogs::XFilePickerListener >& xListener ) override; + virtual void SAL_CALL removeFilePickerListener( const css::uno::Reference< css::ui::dialogs::XFilePickerListener >& xListener ) override; + + // XExecutableDialog functions + + virtual void SAL_CALL setTitle( const OUString& aTitle ) override; + + virtual sal_Int16 SAL_CALL execute() override; + + // XFilePicker functions + + virtual void SAL_CALL setMultiSelectionMode( sal_Bool bMode ) override; + + virtual void SAL_CALL setDefaultName( const OUString& aName ) override; + + virtual void SAL_CALL setDisplayDirectory( const OUString& aDirectory ) override; + + virtual OUString SAL_CALL getDisplayDirectory( ) override; + + virtual css::uno::Sequence< OUString > SAL_CALL getFiles( ) override; + + // XFilePicker2 functions + + virtual css::uno::Sequence< OUString > SAL_CALL getSelectedFiles() override; + + // XFilterManager functions + + virtual void SAL_CALL appendFilter( const OUString& aTitle, const OUString& aFilter ) override; + + virtual void SAL_CALL setCurrentFilter( const OUString& aTitle ) override; + + virtual OUString SAL_CALL getCurrentFilter( ) override; + + // XFilterGroupManager functions + + virtual void SAL_CALL appendFilterGroup( const OUString& sGroupTitle, const css::uno::Sequence< css::beans::StringPair >& aFilters ) override; + + // XFilePickerControlAccess functions + + virtual void SAL_CALL setValue( sal_Int16 nControlId, sal_Int16 nControlAction, const css::uno::Any& aValue ) override; + + virtual css::uno::Any SAL_CALL getValue( sal_Int16 aControlId, sal_Int16 aControlAction ) override; + + virtual void SAL_CALL enableControl( sal_Int16 nControlId, sal_Bool bEnable ) override; + + virtual void SAL_CALL setLabel( sal_Int16 nControlId, const OUString& aLabel ) override; + + virtual OUString SAL_CALL getLabel( sal_Int16 nControlId ) override; + + // XFilePreview + + virtual css::uno::Sequence< sal_Int16 > SAL_CALL getSupportedImageFormats( ) override; + + virtual sal_Int32 SAL_CALL getTargetColorDepth( ) override; + + virtual sal_Int32 SAL_CALL getAvailableWidth( ) override; + + virtual sal_Int32 SAL_CALL getAvailableHeight( ) override; + + virtual void SAL_CALL setImage( sal_Int16 aImageFormat, const css::uno::Any& aImage ) override; + + virtual sal_Bool SAL_CALL setShowState( sal_Bool bShowState ) override; + + virtual sal_Bool SAL_CALL getShowState( ) override; + + // XInitialization + + virtual void SAL_CALL initialize( const css::uno::Sequence< css::uno::Any >& aArguments ) override; + + // XCancellable + + virtual void SAL_CALL cancel( ) override; + + // FilePicker Event functions + + private: + SalGtkFilePicker( const SalGtkFilePicker& ) = delete; + SalGtkFilePicker& operator=( const SalGtkFilePicker& ) = delete; + + bool FilterNameExists( const OUString& rTitle ); + bool FilterNameExists( const css::uno::Sequence< css::beans::StringPair >& _rGroupedFilters ); + + void ensureFilterVector( const OUString& _rInitialCurrentFilter ); + + void impl_fileSelectionChanged( const css::ui::dialogs::FilePickerEvent& aEvent ); + void impl_directoryChanged( const css::ui::dialogs::FilePickerEvent& aEvent ); + void impl_controlStateChanged( const css::ui::dialogs::FilePickerEvent& aEvent ); + void impl_initialize(GtkWidget* pParentWidget, sal_Int16 templateId); + + private: + css::uno::Reference< css::ui::dialogs::XFilePickerListener > + m_xListener; + std::unique_ptr> m_pFilterVector; + GtkWidget *m_pVBox; + GtkWidget *m_pFilterExpander; + GtkWidget *m_pFilterView; + GtkListStore *m_pFilterStore; + + enum { + AUTOEXTENSION, + PASSWORD, + FILTEROPTIONS, + READONLY, + LINK, + PREVIEW, + SELECTION, + GPGENCRYPTION, + TOGGLE_LAST + }; + + GtkWidget *m_pToggles[ TOGGLE_LAST ]; + + bool mbToggleVisibility[TOGGLE_LAST]; + + enum { + OK, + CANCEL, + PLAY, + BUTTON_LAST }; + + GtkWidget *m_pButtons[ BUTTON_LAST ]; + + enum { + VERSION, + TEMPLATE, + IMAGE_TEMPLATE, + IMAGE_ANCHOR, + LIST_LAST + }; + + GtkWidget *m_pHBoxs[ LIST_LAST ]; + GtkWidget *m_pLists[ LIST_LAST ]; + GtkWidget *m_pListLabels[ LIST_LAST ]; + bool mbListVisibility[ LIST_LAST ]; + bool mbButtonVisibility[ BUTTON_LAST ]; + gulong mnHID_FolderChange; + gulong mnHID_SelectionChange; + + OUString m_aCurrentFilter; + OUString m_aInitialFilter; + + bool bVersionWidthUnset; + bool mbPreviewState; + bool mbInitialized; + gulong mHID_Preview; + GtkWidget* m_pPreview; + GtkFileFilter* m_pPseudoFilter; + static constexpr sal_Int32 g_PreviewImageWidth = 256; + static constexpr sal_Int32 g_PreviewImageHeight = 256; + + GtkWidget *getWidget( sal_Int16 nControlId, GType *pType = nullptr); + + void SetCurFilter( const OUString& rFilter ); + void SetFilters(); + void UpdateFilterfromUI(); + + void implChangeType( GtkTreeSelection *selection ); + GtkFileFilter * implAddFilter( const OUString& rFilter, const OUString& rType ); + void implAddFilterGroup( + const css::uno::Sequence< css::beans::StringPair>& _rFilters ); + void updateCurrentFilterFromName(const gchar* filtername); + void unselect_type(); + void InitialMapping(); + + void HandleSetListValue(GtkComboBox *pWidget, sal_Int16 nControlAction, + const css::uno::Any& rValue); + static css::uno::Any HandleGetListValue(GtkComboBox *pWidget, sal_Int16 nControlAction); + + static void expander_changed_cb( GtkExpander *expander, SalGtkFilePicker *pobjFP ); + static void preview_toggled_cb( GObject *cb, SalGtkFilePicker *pobjFP ); + static void filter_changed_cb( GtkFileChooser *file_chooser, GParamSpec *pspec, SalGtkFilePicker *pobjFP ); + static void type_changed_cb( GtkTreeSelection *selection, SalGtkFilePicker *pobjFP ); + static void folder_changed_cb (GtkFileChooser *file_chooser, SalGtkFilePicker *pobjFP); + static void selection_changed_cb (GtkFileChooser *file_chooser, SalGtkFilePicker *pobjFP); + static void update_preview_cb (GtkFileChooser *file_chooser, SalGtkFilePicker *pobjFP); + static void dialog_mapped_cb(GtkWidget *widget, SalGtkFilePicker *pobjFP); + public: + virtual ~SalGtkFilePicker() override; + +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/fpicker/SalGtkFolderPicker.cxx b/vcl/unx/gtk3/fpicker/SalGtkFolderPicker.cxx new file mode 100644 index 0000000000..57b6e7c53e --- /dev/null +++ b/vcl/unx/gtk3/fpicker/SalGtkFolderPicker.cxx @@ -0,0 +1,205 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include +#include +#include +#include +#include "SalGtkFolderPicker.hxx" +#include + +using namespace ::com::sun::star; +using namespace ::com::sun::star::ui::dialogs; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::uno; + +// constructor + +SalGtkFolderPicker::SalGtkFolderPicker( const uno::Reference< uno::XComponentContext >& xContext ) : + SalGtkPicker( xContext ) +{ + m_pDialog = gtk_file_chooser_dialog_new( + OUStringToOString( getResString( FOLDERPICKER_TITLE ), RTL_TEXTENCODING_UTF8 ).getStr(), + nullptr, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, getCancelText().getStr(), GTK_RESPONSE_CANCEL, + getOKText().getStr(), GTK_RESPONSE_ACCEPT, nullptr ); + gtk_window_set_modal(GTK_WINDOW(m_pDialog), true); + + gtk_dialog_set_default_response( GTK_DIALOG (m_pDialog), GTK_RESPONSE_ACCEPT ); +#if !GTK_CHECK_VERSION(4, 0, 0) +#if ENABLE_GIO + gtk_file_chooser_set_local_only( GTK_FILE_CHOOSER( m_pDialog ), false ); +#endif +#endif + gtk_file_chooser_set_select_multiple( GTK_FILE_CHOOSER( m_pDialog ), false ); +} + +void SAL_CALL SalGtkFolderPicker::setDisplayDirectory( const OUString& aDirectory ) +{ + SolarMutexGuard g; + + assert( m_pDialog != nullptr ); + + OString aTxt = unicodetouri( aDirectory ); + if( aTxt.isEmpty() ){ + aTxt = unicodetouri("file:///."); + } + + if( aTxt.endsWith("/") ) + aTxt = aTxt.copy( 0, aTxt.getLength() - 1 ); + + SAL_INFO( "vcl", "setting path to " << aTxt ); + +#if GTK_CHECK_VERSION(4, 0, 0) + GFile* pPath = g_file_new_for_uri(aTxt.getStr()); + gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(m_pDialog), pPath, nullptr); + g_object_unref(pPath); +#else + gtk_file_chooser_set_current_folder_uri(GTK_FILE_CHOOSER(m_pDialog), aTxt.getStr()); +#endif +} + +OUString SAL_CALL SalGtkFolderPicker::getDisplayDirectory() +{ + SolarMutexGuard g; + + assert( m_pDialog != nullptr ); + +#if GTK_CHECK_VERSION(4, 0, 0) + GFile* pPath = + gtk_file_chooser_get_current_folder(GTK_FILE_CHOOSER(m_pDialog)); + gchar* pCurrentFolder = g_file_get_uri(pPath); + g_object_unref(pPath); +#else + gchar* pCurrentFolder = + gtk_file_chooser_get_current_folder_uri(GTK_FILE_CHOOSER(m_pDialog)); +#endif + + OUString aCurrentFolderName = uritounicode(pCurrentFolder); + g_free( pCurrentFolder ); + + return aCurrentFolderName; +} + +OUString SAL_CALL SalGtkFolderPicker::getDirectory() +{ + SolarMutexGuard g; + + assert( m_pDialog != nullptr ); + +#if GTK_CHECK_VERSION(4, 0, 0) + GFile* pPath = + gtk_file_chooser_get_file(GTK_FILE_CHOOSER(m_pDialog)); + gchar* pSelectedFolder = g_file_get_uri(pPath); + g_object_unref(pPath); +#else + gchar* pSelectedFolder = + gtk_file_chooser_get_uri( GTK_FILE_CHOOSER( m_pDialog ) ); +#endif + OUString aSelectedFolderName = uritounicode(pSelectedFolder); + g_free( pSelectedFolder ); + + return aSelectedFolderName; +} + +void SAL_CALL SalGtkFolderPicker::setDescription( const OUString& /*rDescription*/ ) +{ +} + +// XExecutableDialog functions + +void SAL_CALL SalGtkFolderPicker::setTitle( const OUString& aTitle ) +{ + SolarMutexGuard g; + + assert( m_pDialog != nullptr ); + + OString aWindowTitle = OUStringToOString( aTitle, RTL_TEXTENCODING_UTF8 ); + + gtk_window_set_title( GTK_WINDOW( m_pDialog ), aWindowTitle.getStr() ); +} + +sal_Int16 SAL_CALL SalGtkFolderPicker::execute() +{ + SolarMutexGuard g; + + assert( m_pDialog != nullptr ); + + sal_Int16 retVal = 0; + + uno::Reference< awt::XExtendedToolkit > xToolkit = + awt::Toolkit::create(m_xContext); + + uno::Reference xDesktop = frame::Desktop::create(m_xContext); + + GtkWindow *pParent = GTK_WINDOW(m_pParentWidget); + if (!pParent) + { + SAL_WARN( "vcl.gtk", "no parent widget set"); + pParent = RunDialog::GetTransientFor(); + } + if (pParent) + gtk_window_set_transient_for(GTK_WINDOW(m_pDialog), pParent); + rtl::Reference pRunDialog = new RunDialog(m_pDialog, xToolkit, xDesktop); + gint nStatus = pRunDialog->run(); + switch( nStatus ) + { + case GTK_RESPONSE_ACCEPT: + retVal = ExecutableDialogResults::OK; + break; + case GTK_RESPONSE_CANCEL: + retVal = ExecutableDialogResults::CANCEL; + break; + default: + retVal = 0; + break; + } + gtk_widget_hide(m_pDialog); + + return retVal; +} + +// XInitialization + +void SAL_CALL SalGtkFolderPicker::initialize(const uno::Sequence& aArguments) +{ + m_pParentWidget = GetParentWidget(aArguments); +} + +// XCancellable + +void SAL_CALL SalGtkFolderPicker::cancel() +{ + SolarMutexGuard g; + + assert( m_pDialog != nullptr ); + + // TODO m_pImpl->cancel(); +} + +uno::Reference< ui::dialogs::XFolderPicker2 > +GtkInstance::createFolderPicker( const uno::Reference< uno::XComponentContext > &xMSF ) +{ + return uno::Reference< ui::dialogs::XFolderPicker2 >( + new SalGtkFolderPicker( xMSF ) ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/fpicker/SalGtkFolderPicker.hxx b/vcl/unx/gtk3/fpicker/SalGtkFolderPicker.hxx new file mode 100644 index 0000000000..5b1d8dcebf --- /dev/null +++ b/vcl/unx/gtk3/fpicker/SalGtkFolderPicker.hxx @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include +#include +#include + +#include "SalGtkPicker.hxx" + +class SalGtkFolderPicker : + public SalGtkPicker, + public cppu::WeakImplHelper +{ + public: + + // constructor + SalGtkFolderPicker( const css::uno::Reference< css::uno::XComponentContext >& xServiceMgr ); + + // XExecutableDialog functions + + virtual void SAL_CALL setTitle( const OUString& aTitle ) override; + + virtual sal_Int16 SAL_CALL execute( ) override; + + // XFolderPicker functions + + virtual void SAL_CALL setDisplayDirectory( const OUString& rDirectory ) override; + + virtual OUString SAL_CALL getDisplayDirectory( ) override; + + virtual OUString SAL_CALL getDirectory( ) override; + + virtual void SAL_CALL setDescription( const OUString& rDescription ) override; + + // XInitialization + + virtual void SAL_CALL initialize( const css::uno::Sequence< css::uno::Any >& aArguments ) override; + + // XCancellable + + virtual void SAL_CALL cancel( ) override; + + private: + SalGtkFolderPicker( const SalGtkFolderPicker& ) = delete; + SalGtkFolderPicker& operator=( const SalGtkFolderPicker& ) = delete; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/fpicker/SalGtkPicker.cxx b/vcl/unx/gtk3/fpicker/SalGtkPicker.cxx new file mode 100644 index 0000000000..9d07a11f76 --- /dev/null +++ b/vcl/unx/gtk3/fpicker/SalGtkPicker.cxx @@ -0,0 +1,295 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include "SalGtkPicker.hxx" + +using namespace ::rtl; +using namespace ::com::sun::star; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::uno; + +OUString SalGtkPicker::uritounicode(const gchar* pIn) const +{ + if (!pIn) + return OUString(); + + OUString sURL( pIn, strlen(pIn), + RTL_TEXTENCODING_UTF8 ); + + INetURLObject aURL(sURL); + if (INetProtocol::File == aURL.GetProtocol()) + { + // all the URLs are handled by office in UTF-8 + // so the Gnome FP related URLs should be converted accordingly + OUString aNewURL = uri::ExternalUriReferenceTranslator::create( m_xContext )->translateToInternal(sURL); + if( !aNewURL.isEmpty() ) + sURL = aNewURL; + } + return sURL; +} + +OString SalGtkPicker::unicodetouri(const OUString &rURL) const +{ + // all the URLs are handled by office in UTF-8 ( and encoded with "%xx" codes based on UTF-8 ) + // so the Gnome FP related URLs should be converted accordingly + OString sURL = OUStringToOString(rURL, RTL_TEXTENCODING_UTF8); + INetURLObject aURL(rURL); + if (INetProtocol::File == aURL.GetProtocol()) + { + OUString aNewURL = uri::ExternalUriReferenceTranslator::create( m_xContext )->translateToExternal(rURL); + + if( !aNewURL.isEmpty() ) + { + // At this point the URL should contain ascii characters only actually + sURL = OUStringToOString( aNewURL, osl_getThreadTextEncoding() ); + } + } + return sURL; +} + +extern "C" +{ + static gboolean canceldialog(RunDialog *pDialog) + { + SolarMutexGuard g; + pDialog->cancel(); + return false; + } +} + +GtkWindow* RunDialog::GetTransientFor() +{ + vcl::Window * pWindow = ::Application::GetActiveTopWindow(); + if (!pWindow) + return nullptr; + GtkSalFrame *pFrame = dynamic_cast(pWindow->ImplGetFrame()); + if (!pFrame) + return nullptr; + return GTK_WINDOW(widget_get_toplevel(pFrame->getWindow())); +} + +RunDialog::RunDialog(GtkWidget *pDialog, uno::Reference xToolkit, + uno::Reference xDesktop) + : cppu::WeakComponentImplHelper(maLock) + , mpDialog(pDialog) + , mbTerminateDesktop(false) + , mxToolkit(std::move(xToolkit)) + , mxDesktop(std::move(xDesktop)) +{ +} + +RunDialog::~RunDialog() +{ + SolarMutexGuard g; + + g_source_remove_by_user_data (this); +} + +void SAL_CALL RunDialog::windowOpened(const css::lang::EventObject& e) +{ + SolarMutexGuard g; + + //Don't popdown dialogs if a tooltip appears elsewhere, that's ok, but do pop down + //if another dialog/frame is launched. + css::uno::Reference xAccessible(e.Source, css::uno::UNO_QUERY); + if (xAccessible.is()) + { + css::uno::Reference xContext(xAccessible->getAccessibleContext()); + if (xContext.is() && xContext->getAccessibleRole() == css::accessibility::AccessibleRole::TOOL_TIP) + { + return; + } + } + + g_timeout_add_full(G_PRIORITY_HIGH_IDLE, 0, reinterpret_cast(canceldialog), this, nullptr); +} + +void SAL_CALL RunDialog::queryTermination( const css::lang::EventObject& ) +{ + SolarMutexGuard g; + + g_timeout_add_full(G_PRIORITY_HIGH_IDLE, 0, reinterpret_cast(canceldialog), this, nullptr); + + mbTerminateDesktop = true; + + throw css::frame::TerminationVetoException(); +} + +void SAL_CALL RunDialog::notifyTermination( const css::lang::EventObject& ) +{ +} + +void RunDialog::cancel() +{ + gtk_dialog_response( GTK_DIALOG( mpDialog ), GTK_RESPONSE_CANCEL ); + gtk_widget_hide( mpDialog ); +} + +namespace +{ + class ExecuteInfo + { + private: + css::uno::Reference mxDesktop; + public: + ExecuteInfo(css::uno::Reference xDesktop) + : mxDesktop(std::move(xDesktop)) + { + } + void terminate() + { + mxDesktop->terminate(); + } + }; +} + +gint RunDialog::run() +{ + if (mxToolkit.is()) + mxToolkit->addTopWindowListener(this); + + mxDesktop->addTerminateListener(this); + + // [Inc/Dec]ModalCount on parent frame so it knows it is in modal mode + GtkWindow* pParent = gtk_window_get_transient_for(GTK_WINDOW(mpDialog)); + GtkSalFrame* pFrame = pParent ? GtkSalFrame::getFromWindow(GTK_WIDGET(pParent)) : nullptr; + VclPtr xFrameWindow = pFrame ? pFrame->GetWindow() : nullptr; + if (xFrameWindow) + { + xFrameWindow->IncModalCount(); + xFrameWindow->ImplGetFrame()->NotifyModalHierarchy(true); + } + + gint nStatus = gtk_dialog_run(GTK_DIALOG(mpDialog)); + + if (xFrameWindow) + { + xFrameWindow->DecModalCount(); + xFrameWindow->ImplGetFrame()->NotifyModalHierarchy(false); + } + + mxDesktop->removeTerminateListener(this); + + if (mxToolkit.is()) + mxToolkit->removeTopWindowListener(this); + + if (mbTerminateDesktop) + { + ExecuteInfo* pExecuteInfo = new ExecuteInfo(mxDesktop); + Application::PostUserEvent(LINK(nullptr, RunDialog, TerminateDesktop), pExecuteInfo); + } + + return nStatus; +} + +IMPL_STATIC_LINK(RunDialog, TerminateDesktop, void*, p, void) +{ + ExecuteInfo* pExecuteInfo = static_cast(p); + pExecuteInfo->terminate(); + delete pExecuteInfo; +} + +SalGtkPicker::SalGtkPicker( uno::Reference xContext ) + : m_pParentWidget(nullptr) + , m_pDialog(nullptr) + , m_xContext(std::move(xContext)) +{ +} + +SalGtkPicker::~SalGtkPicker() +{ + SolarMutexGuard g; + + if (m_pDialog) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_destroy(m_pDialog); +#else + gtk_window_destroy(GTK_WINDOW(m_pDialog)); +#endif + } +} + +void SalGtkPicker::implsetDisplayDirectory( const OUString& aDirectory ) +{ + OSL_ASSERT( m_pDialog != nullptr ); + + OString aTxt = unicodetouri(aDirectory); + if( aTxt.isEmpty() ){ + aTxt = unicodetouri("file:///."); + } + + if( aTxt.endsWith("/") ) + aTxt = aTxt.copy( 0, aTxt.getLength() - 1 ); + + SAL_INFO( "vcl", "setting path to " << aTxt ); + +#if GTK_CHECK_VERSION(4, 0, 0) + GFile* pPath = g_file_new_for_uri(aTxt.getStr()); + gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(m_pDialog), pPath, nullptr); + g_object_unref(pPath); +#else + gtk_file_chooser_set_current_folder_uri(GTK_FILE_CHOOSER(m_pDialog), aTxt.getStr()); +#endif +} + +OUString SalGtkPicker::implgetDisplayDirectory() +{ + OSL_ASSERT( m_pDialog != nullptr ); + +#if GTK_CHECK_VERSION(4, 0, 0) + GFile* pPath = + gtk_file_chooser_get_current_folder(GTK_FILE_CHOOSER(m_pDialog)); + gchar* pCurrentFolder = g_file_get_uri(pPath); + g_object_unref(pPath); +#else + gchar* pCurrentFolder = + gtk_file_chooser_get_current_folder_uri(GTK_FILE_CHOOSER(m_pDialog)); +#endif + OUString aCurrentFolderName = uritounicode(pCurrentFolder); + g_free( pCurrentFolder ); + + return aCurrentFolderName; +} + +void SalGtkPicker::implsetTitle( std::u16string_view aTitle ) +{ + OSL_ASSERT( m_pDialog != nullptr ); + + OString aWindowTitle = OUStringToOString( aTitle, RTL_TEXTENCODING_UTF8 ); + + gtk_window_set_title( GTK_WINDOW( m_pDialog ), aWindowTitle.getStr() ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/fpicker/SalGtkPicker.hxx b/vcl/unx/gtk3/fpicker/SalGtkPicker.hxx new file mode 100644 index 0000000000..cb11445dae --- /dev/null +++ b/vcl/unx/gtk3/fpicker/SalGtkPicker.hxx @@ -0,0 +1,144 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#define FOLDERPICKER_TITLE 500 +#define FOLDER_PICKER_DEF_DESCRIPTION 501 +#define FILE_PICKER_TITLE_OPEN 502 +#define FILE_PICKER_TITLE_SAVE 503 +#define FILE_PICKER_FILE_TYPE 504 +#define FILE_PICKER_OVERWRITE_PRIMARY 505 +#define FILE_PICKER_OVERWRITE_SECONDARY 506 +#define FILE_PICKER_ALLFORMATS 507 + +class SalGtkPicker +{ + public: + SalGtkPicker( css::uno::Reference xContext ); + virtual ~SalGtkPicker(); + protected: + osl::Mutex m_rbHelperMtx; + GtkWidget *m_pParentWidget; + GtkWidget *m_pDialog; + protected: + /// @throws css::uno::RuntimeException + void implsetTitle( std::u16string_view aTitle ); + + /// @throws css::lang::IllegalArgumentException + /// @throws css::uno::RuntimeException + void implsetDisplayDirectory( const OUString& rDirectory ); + + /// @throws css::uno::RuntimeException + OUString implgetDisplayDirectory( ); + OUString uritounicode(const gchar *pIn) const; + OString unicodetouri(const OUString &rURL) const; + + // to instantiate own services + css::uno::Reference< css::uno::XComponentContext > m_xContext; + + static GtkWidget* GetParentWidget(const css::uno::Sequence& rArguments); + + static OUString getResString( sal_Int32 aId ); +}; + +//Run the Gtk Dialog. Watch for any "new windows" created while we're +//executing and consider that a CANCEL event to avoid e.g. "file cannot be opened" +//modal dialogs and this one getting locked if some other API call causes this +//to happen while we're opened waiting for user input, e.g. +//https://bugzilla.redhat.com/show_bug.cgi?id=441108 +class RunDialog : + public cppu::WeakComponentImplHelper< + css::awt::XTopWindowListener, + css::frame::XTerminateListener > +{ +private: + osl::Mutex maLock; + GtkWidget *mpDialog; + bool mbTerminateDesktop; + css::uno::Reference mxToolkit; + css::uno::Reference mxDesktop; + DECL_STATIC_LINK(RunDialog, TerminateDesktop, void*, void); +public: + + // XTopWindowListener + using cppu::WeakComponentImplHelperBase::disposing; + virtual void SAL_CALL disposing( const css::lang::EventObject& ) override {} + virtual void SAL_CALL windowOpened( const css::lang::EventObject& e ) override; + virtual void SAL_CALL windowClosing( const css::lang::EventObject& ) override {} + virtual void SAL_CALL windowClosed( const css::lang::EventObject& ) override {} + virtual void SAL_CALL windowMinimized( const css::lang::EventObject& ) override {} + virtual void SAL_CALL windowNormalized( const css::lang::EventObject& ) override {} + virtual void SAL_CALL windowActivated( const css::lang::EventObject& ) override {} + virtual void SAL_CALL windowDeactivated( const css::lang::EventObject& ) override {} + + // XTerminateListener + virtual void SAL_CALL queryTermination( const css::lang::EventObject& aEvent ) override; + virtual void SAL_CALL notifyTermination( const css::lang::EventObject& aEvent ) override; +public: + RunDialog(GtkWidget *pDialog, + css::uno::Reference xToolkit, + css::uno::Reference xDesktop); + virtual ~RunDialog() override; + gint run(); + void cancel(); + static GtkWindow* GetTransientFor(); +}; + +inline OString getCancelText() +{ + return VclResId(SV_BUTTONTEXT_CANCEL).replace('~', '_').toUtf8(); +} + +inline OString getOpenText() +{ + return VclResId(SV_BUTTONTEXT_OPEN).replace('~', '_').toUtf8(); +} + +inline OString getSaveText() +{ + return VclResId(SV_BUTTONTEXT_SAVE).replace('~', '_').toUtf8(); +} + +inline OString getOKText() +{ + return VclResId(SV_BUTTONTEXT_OK).replace('~', '_').toUtf8(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/fpicker/eventnotification.hxx b/vcl/unx/gtk3/fpicker/eventnotification.hxx new file mode 100644 index 0000000000..214da6da9b --- /dev/null +++ b/vcl/unx/gtk3/fpicker/eventnotification.hxx @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include +#include + +// encapsulate a filepicker event +// notification, because there are +// two types of filepicker notifications +// with and without parameter +// this is an application of the +// "command" pattern see GoF + +class CEventNotification +{ +public: + virtual ~CEventNotification(){}; + + virtual void SAL_CALL notifyEventListener(css::uno::Reference xListener) + = 0; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/fpicker/resourceprovider.cxx b/vcl/unx/gtk3/fpicker/resourceprovider.cxx new file mode 100644 index 0000000000..fa90b81266 --- /dev/null +++ b/vcl/unx/gtk3/fpicker/resourceprovider.cxx @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include + +#include +#include +#include "SalGtkPicker.hxx" + +using namespace ::com::sun::star::ui::dialogs::ExtendedFilePickerElementIds; +using namespace ::com::sun::star::ui::dialogs::CommonFilePickerElementIds; + +// translate control ids to resource ids + +const struct +{ + sal_Int32 ctrlId; + TranslateId resId; +} CtrlIdToResIdTable[] = { + { CHECKBOX_AUTOEXTENSION, STR_SVT_FILEPICKER_AUTO_EXTENSION }, + { CHECKBOX_PASSWORD, STR_SVT_FILEPICKER_PASSWORD }, + { CHECKBOX_GPGENCRYPTION, STR_SVT_FILEPICKER_GPGENCRYPT }, + { CHECKBOX_FILTEROPTIONS, STR_SVT_FILEPICKER_FILTER_OPTIONS }, + { CHECKBOX_READONLY, STR_SVT_FILEPICKER_READONLY }, + { CHECKBOX_LINK, STR_SVT_FILEPICKER_INSERT_AS_LINK }, + { CHECKBOX_PREVIEW, STR_SVT_FILEPICKER_SHOW_PREVIEW }, + { PUSHBUTTON_PLAY, STR_SVT_FILEPICKER_PLAY }, + { LISTBOX_VERSION_LABEL, STR_SVT_FILEPICKER_VERSION }, + { LISTBOX_TEMPLATE_LABEL, STR_SVT_FILEPICKER_TEMPLATES }, + { LISTBOX_IMAGE_TEMPLATE_LABEL, STR_SVT_FILEPICKER_IMAGE_TEMPLATE }, + { LISTBOX_IMAGE_ANCHOR_LABEL, STR_SVT_FILEPICKER_IMAGE_ANCHOR }, + { CHECKBOX_SELECTION, STR_SVT_FILEPICKER_SELECTION }, + { FOLDERPICKER_TITLE, STR_SVT_FOLDERPICKER_DEFAULT_TITLE }, + { FOLDER_PICKER_DEF_DESCRIPTION, STR_SVT_FOLDERPICKER_DEFAULT_DESCRIPTION }, + { FILE_PICKER_OVERWRITE_PRIMARY, STR_SVT_ALREADYEXISTOVERWRITE }, + { FILE_PICKER_OVERWRITE_SECONDARY, STR_SVT_ALREADYEXISTOVERWRITE_SECONDARY }, + { FILE_PICKER_ALLFORMATS, STR_SVT_ALLFORMATS }, + { FILE_PICKER_TITLE_OPEN, STR_FILEDLG_OPEN }, + { FILE_PICKER_TITLE_SAVE, STR_FILEDLG_SAVE }, + { FILE_PICKER_FILE_TYPE, STR_FILEDLG_TYPE } +}; + +static TranslateId CtrlIdToResId( sal_Int32 aControlId ) +{ + for (auto & i : CtrlIdToResIdTable) + { + if ( i.ctrlId == aControlId ) + return i.resId; + } + return {}; +} + +OUString SalGtkPicker::getResString( sal_Int32 aId ) +{ + OUString aResString; + // translate the control id to a resource id + TranslateId pResId = CtrlIdToResId( aId ); + if (pResId) + aResString = FpsResId(pResId); + return aResString.replace('~', '_'); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gloactiongroup.cxx b/vcl/unx/gtk3/gloactiongroup.cxx new file mode 100644 index 0000000000..1b64da0ffa --- /dev/null +++ b/vcl/unx/gtk3/gloactiongroup.cxx @@ -0,0 +1,405 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include + +#include + +#include + +/* + * GLOAction + */ + +#define G_TYPE_LO_ACTION (g_lo_action_get_type ()) +#define G_LO_ACTION(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \ + G_TYPE_LO_ACTION, GLOAction)) +namespace { + +struct GLOAction +{ + GObject parent_instance; + + gint item_id; // Menu item ID. + bool submenu; // TRUE if action is a submenu action. + bool enabled; // TRUE if action is enabled. + GVariantType* parameter_type; // A GVariantType with the action parameter type. + GVariantType* state_type; // A GVariantType with item state type + GVariant* state_hint; // A GVariant with state hints. + GVariant* state; // A GVariant with current item state +}; + +} + +typedef GObjectClass GLOActionClass; + +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic ignored "-Wdeprecated-volatile" +#endif +#endif +#endif +G_DEFINE_TYPE (GLOAction, g_lo_action, G_TYPE_OBJECT); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +static GLOAction* +g_lo_action_new() +{ + return G_LO_ACTION (g_object_new (G_TYPE_LO_ACTION, nullptr)); +} + +static void +g_lo_action_init (GLOAction *action) +{ + action->item_id = -1; + action->submenu = false; + action->enabled = true; + action->parameter_type = nullptr; + action->state_type = nullptr; + action->state_hint = nullptr; + action->state = nullptr; +} + +static void +g_lo_action_finalize (GObject *object) +{ + GLOAction* action = G_LO_ACTION(object); + + if (action->parameter_type) + g_variant_type_free (action->parameter_type); + + if (action->state_type) + g_variant_type_free (action->state_type); + + if (action->state_hint) + g_variant_unref (action->state_hint); + + if (action->state) + g_variant_unref (action->state); + + G_OBJECT_CLASS (g_lo_action_parent_class)->finalize (object); +} + +static void +g_lo_action_class_init (GLOActionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + object_class->finalize = g_lo_action_finalize; +} + +/* + * GLOActionGroup + */ + +struct GLOActionGroupPrivate +{ + GHashTable *table; /* string -> GLOAction */ +}; + +static void g_lo_action_group_iface_init (GActionGroupInterface *); + +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic ignored "-Wdeprecated-volatile" +#endif +#endif +#endif +G_DEFINE_TYPE_WITH_CODE (GLOActionGroup, + g_lo_action_group, G_TYPE_OBJECT, + G_ADD_PRIVATE(GLOActionGroup) + G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, + g_lo_action_group_iface_init)); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +static gchar ** +g_lo_action_group_list_actions (GActionGroup *group) +{ + GLOActionGroup *loGroup = G_LO_ACTION_GROUP (group); + GHashTableIter iter; + gint n, i = 0; + gchar **keys; + gpointer key; + + n = g_hash_table_size (loGroup->priv->table); + keys = g_new (gchar *, n + 1); + + g_hash_table_iter_init (&iter, loGroup->priv->table); + while (g_hash_table_iter_next (&iter, &key, nullptr)) + keys[i++] = g_strdup (static_cast(key)); + g_assert_cmpint (i, ==, n); + keys[n] = nullptr; + + return keys; +} + +static gboolean +g_lo_action_group_query_action (GActionGroup *group, + const gchar *action_name, + gboolean *enabled, + const GVariantType **parameter_type, + const GVariantType **state_type, + GVariant **state_hint, + GVariant **state) +{ + //SAL_INFO("vcl.unity", "g_lo_action_group_query_action on " << group); + GLOActionGroup *lo_group = G_LO_ACTION_GROUP (group); + GLOAction* action = G_LO_ACTION (g_hash_table_lookup (lo_group->priv->table, action_name)); + + if (action == nullptr) + return FALSE; + + if (enabled) + { + *enabled = action->enabled; + } + + if (parameter_type) + *parameter_type = action->parameter_type; + + if (state_type) + *state_type = action->state_type; + + if (state_hint) + *state_hint = (action->state_hint) ? g_variant_ref (action->state_hint) : nullptr; + + if (state) + *state = (action->state) ? g_variant_ref (action->state) : nullptr; + + return true; +} + +static void +g_lo_action_group_perform_submenu_action (GLOActionGroup *group, + const gchar *action_name, + GVariant *state) +{ + bool bState = g_variant_get_boolean (state); + SAL_INFO("vcl.unity", "g_lo_action_group_perform_submenu_action on " << group << " to " << bState); + + if (bState) + GtkSalMenu::Activate(action_name); + else + GtkSalMenu::Deactivate(action_name); +} + +static void +g_lo_action_group_change_state (GActionGroup *group, + const gchar *action_name, + GVariant *value) +{ + SAL_INFO("vcl.unity", "g_lo_action_group_change_state on " << group ); + g_return_if_fail (value != nullptr); + + g_variant_ref_sink (value); + + if (action_name != nullptr) + { + GLOActionGroup* lo_group = G_LO_ACTION_GROUP (group); + GLOAction* action = G_LO_ACTION (g_hash_table_lookup (lo_group->priv->table, action_name)); + + if (action != nullptr) + { + if (action->submenu) + g_lo_action_group_perform_submenu_action (lo_group, action_name, value); + else + { + bool is_new = false; + + /* If action already exists but has no state, it should be removed and added again. */ + if (action->state_type == nullptr) + { + g_action_group_action_removed (G_ACTION_GROUP (group), action_name); + action->state_type = g_variant_type_copy (g_variant_get_type(value)); + is_new = true; + } + + if (g_variant_is_of_type (value, action->state_type)) + { + if (action->state) + g_variant_unref(action->state); + + action->state = g_variant_ref (value); + + if (is_new) + g_action_group_action_added (G_ACTION_GROUP (group), action_name); + else + g_action_group_action_state_changed (group, action_name, value); + } + } + } + } + + g_variant_unref (value); +} + +static void +g_lo_action_group_activate (GActionGroup *group, + const gchar *action_name, + GVariant *parameter) +{ + if (parameter != nullptr) + g_action_group_change_action_state(group, action_name, parameter); + GtkSalMenu::DispatchCommand(action_name); +} + +void +g_lo_action_group_insert (GLOActionGroup *group, + const gchar *action_name, + gint item_id, + gboolean submenu) +{ + g_lo_action_group_insert_stateful (group, action_name, item_id, submenu, nullptr, nullptr, nullptr, nullptr); +} + +void +g_lo_action_group_insert_stateful (GLOActionGroup *group, + const gchar *action_name, + gint item_id, + gboolean submenu, + const GVariantType *parameter_type, + const GVariantType *state_type, + GVariant *state_hint, + GVariant *state) +{ + g_return_if_fail (G_IS_LO_ACTION_GROUP (group)); + + GLOAction* old_action = G_LO_ACTION (g_hash_table_lookup (group->priv->table, action_name)); + + if (old_action != nullptr && old_action->item_id == item_id) + return; + + if (old_action != nullptr) + g_lo_action_group_remove (group, action_name); + + GLOAction* action = g_lo_action_new(); + + g_hash_table_insert (group->priv->table, g_strdup (action_name), action); + + action->item_id = item_id; + action->submenu = submenu; + + if (parameter_type) + action->parameter_type = const_cast(parameter_type); + + if (state_type) + action->state_type = const_cast(state_type); + + if (state_hint) + action->state_hint = g_variant_ref_sink (state_hint); + + if (state) + action->state = g_variant_ref_sink (state); + + g_action_group_action_added (G_ACTION_GROUP (group), action_name); +} + +static void +g_lo_action_group_finalize (GObject *object) +{ + GLOActionGroup *lo_group = G_LO_ACTION_GROUP (object); + + g_hash_table_unref (lo_group->priv->table); + + G_OBJECT_CLASS (g_lo_action_group_parent_class)->finalize (object); +} + +static void +g_lo_action_group_init (GLOActionGroup *group) +{ + SAL_INFO("vcl.unity", "g_lo_action_group_init on " << group); + group->priv = static_cast(g_lo_action_group_get_instance_private (group)); + group->priv->table = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); +} + +static void +g_lo_action_group_class_init (GLOActionGroupClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = g_lo_action_group_finalize; +} + +static void +g_lo_action_group_iface_init (GActionGroupInterface *iface) +{ + iface->list_actions = g_lo_action_group_list_actions; + iface->query_action = g_lo_action_group_query_action; + iface->change_action_state = g_lo_action_group_change_state; + iface->activate_action = g_lo_action_group_activate; +} + +GLOActionGroup * +g_lo_action_group_new() +{ + GLOActionGroup* group = G_LO_ACTION_GROUP (g_object_new (G_TYPE_LO_ACTION_GROUP, nullptr)); + return group; +} + +void +g_lo_action_group_set_action_enabled (GLOActionGroup *group, + const gchar *action_name, + gboolean enabled) +{ + SAL_INFO("vcl.unity", "g_lo_action_group_set_action_enabled on " << group); + g_return_if_fail (G_IS_LO_ACTION_GROUP (group)); + g_return_if_fail (action_name != nullptr); + + GLOAction* action = G_LO_ACTION (g_hash_table_lookup (group->priv->table, action_name)); + + if (action == nullptr) + return; + + action->enabled = enabled; + + g_action_group_action_enabled_changed (G_ACTION_GROUP (group), action_name, enabled); +} + +void +g_lo_action_group_remove (GLOActionGroup *group, + const gchar *action_name) +{ + SAL_INFO("vcl.unity", "g_lo_action_group_remove on " << group); + g_return_if_fail (G_IS_LO_ACTION_GROUP (group)); + + if (action_name != nullptr) + { + g_action_group_action_removed (G_ACTION_GROUP (group), action_name); + g_hash_table_remove (group->priv->table, action_name); + } +} + +void +g_lo_action_group_clear (GLOActionGroup *group) +{ + SAL_INFO("vcl.unity", "g_lo_action_group_clear on " << group); + g_return_if_fail (G_IS_LO_ACTION_GROUP (group)); + + GList* keys = g_hash_table_get_keys (group->priv->table); + + for (GList* element = g_list_first (keys); element != nullptr; element = g_list_next (element)) + { + g_lo_action_group_remove (group, static_cast(element->data)); + } + + g_list_free (keys); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/glomenu.cxx b/vcl/unx/gtk3/glomenu.cxx new file mode 100644 index 0000000000..a391649bbb --- /dev/null +++ b/vcl/unx/gtk3/glomenu.cxx @@ -0,0 +1,694 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include + +#include + +struct GLOMenu +{ + GMenuModel const parent_instance; + + GArray *items; +}; + +typedef GMenuModelClass GLOMenuClass; + +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic ignored "-Wdeprecated-volatile" +#endif +#endif +#endif +G_DEFINE_TYPE (GLOMenu, g_lo_menu, G_TYPE_MENU_MODEL); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +namespace { + +struct item +{ + GHashTable* attributes; // Item attributes. + GHashTable* links; // Item links. +}; + +} + +static void +g_lo_menu_struct_item_init (struct item *menu_item) +{ + menu_item->attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, reinterpret_cast(g_variant_unref)); + menu_item->links = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); +} + +/* We treat attribute names the same as GSettings keys: + * - only lowercase ascii, digits and '-' + * - must start with lowercase + * - must not end with '-' + * - no consecutive '-' + * - not longer than 1024 chars + */ +static bool +valid_attribute_name (const gchar *name) +{ + gint i; + + if (!g_ascii_islower (name[0])) + return false; + + for (i = 1; name[i]; i++) + { + if (name[i] != '-' && + !g_ascii_islower (name[i]) && + !g_ascii_isdigit (name[i])) + return false; + + if (name[i] == '-' && name[i + 1] == '-') + return false; + } + + if (name[i - 1] == '-') + return false; + + if (i > 1024) + return false; + + return true; +} + +/* + * GLOMenu + */ + +static gboolean +g_lo_menu_is_mutable (GMenuModel*) +{ + // Menu is always mutable. + return true; +} + +static gint +g_lo_menu_get_n_items (GMenuModel *model) +{ + g_return_val_if_fail (model != nullptr, 0); + GLOMenu *menu = G_LO_MENU (model); + g_return_val_if_fail (menu->items != nullptr, 0); + + return menu->items->len; +} + +gint +g_lo_menu_get_n_items_from_section (GLOMenu *menu, + gint section) +{ + g_return_val_if_fail (0 <= section && o3tl::make_unsigned(section) < menu->items->len, 0); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_val_if_fail (model != nullptr, 0); + + gint length = model->items->len; + + g_object_unref (model); + + return length; +} + +static void +g_lo_menu_get_item_attributes (GMenuModel *model, + gint position, + GHashTable **table) +{ + GLOMenu *menu = G_LO_MENU (model); + *table = g_hash_table_ref (g_array_index (menu->items, struct item, position).attributes); +} + +static void +g_lo_menu_get_item_links (GMenuModel *model, + gint position, + GHashTable **table) +{ + GLOMenu *menu = G_LO_MENU (model); + *table = g_hash_table_ref (g_array_index (menu->items, struct item, position).links); +} + +void +g_lo_menu_insert (GLOMenu *menu, + gint position, + const gchar *label) +{ + g_lo_menu_insert_section (menu, position, label, nullptr); +} + +void +g_lo_menu_insert_in_section (GLOMenu *menu, + gint section, + gint position, + const gchar *label) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + g_return_if_fail (0 <= section && o3tl::make_unsigned(section) < menu->items->len); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_if_fail (model != nullptr); + + g_lo_menu_insert (model, position, label); + + g_object_unref (model); +} + +GLOMenu * +g_lo_menu_new() +{ + return G_LO_MENU( g_object_new (G_TYPE_LO_MENU, nullptr) ); +} + +static void +g_lo_menu_set_attribute_value (GLOMenu *menu, + gint position, + const gchar *attribute, + GVariant *value) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + g_return_if_fail (attribute != nullptr); + g_return_if_fail (valid_attribute_name (attribute)); + + if (position >= static_cast(menu->items->len)) + return; + + struct item menu_item = g_array_index (menu->items, struct item, position); + + if (value != nullptr) + g_hash_table_insert (menu_item.attributes, g_strdup (attribute), g_variant_ref_sink (value)); + else + g_hash_table_remove (menu_item.attributes, attribute); +} + +static GVariant* +g_lo_menu_get_attribute_value_from_item_in_section (GLOMenu *menu, + gint section, + gint position, + const gchar *attribute, + const GVariantType *type) +{ + GMenuModel *model = G_MENU_MODEL (g_lo_menu_get_section (menu, section)); + + g_return_val_if_fail (model != nullptr, nullptr); + + GVariant *value = g_menu_model_get_item_attribute_value (model, + position, + attribute, + type); + + g_object_unref (model); + + return value; +} + +void +g_lo_menu_set_label (GLOMenu *menu, + gint position, + const gchar *label) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GVariant *value; + + if (label != nullptr) + value = g_variant_new_string (label); + else + value = nullptr; + + g_lo_menu_set_attribute_value (menu, position, G_MENU_ATTRIBUTE_LABEL, value); +} + +void +g_lo_menu_set_icon (GLOMenu *menu, + gint position, + const GIcon *icon) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GVariant *value; + + if (icon != nullptr) + { +#if GLIB_CHECK_VERSION(2,38,0) + value = g_icon_serialize (const_cast(icon)); +#else + value = nullptr; +#endif + } + else + value = nullptr; + +#ifndef G_MENU_ATTRIBUTE_ICON +# define G_MENU_ATTRIBUTE_ICON "icon" +#endif + + g_lo_menu_set_attribute_value (menu, position, G_MENU_ATTRIBUTE_ICON, value); + if (value) + g_variant_unref (value); +} + +void +g_lo_menu_set_label_to_item_in_section (GLOMenu *menu, + gint section, + gint position, + const gchar *label) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_if_fail (model != nullptr); + + g_lo_menu_set_label (model, position, label); + + // Notify the update. + g_menu_model_items_changed (G_MENU_MODEL (model), position, 1, 1); + + g_object_unref (model); +} + +void +g_lo_menu_set_icon_to_item_in_section (GLOMenu *menu, + gint section, + gint position, + const GIcon *icon) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_if_fail (model != nullptr); + + g_lo_menu_set_icon (model, position, icon); + + // Notify the update. + g_menu_model_items_changed (G_MENU_MODEL (model), position, 1, 1); + + g_object_unref (model); +} + +gchar * +g_lo_menu_get_label_from_item_in_section (GLOMenu *menu, + gint section, + gint position) +{ + g_return_val_if_fail (G_IS_LO_MENU (menu), nullptr); + + GVariant *label_value = g_lo_menu_get_attribute_value_from_item_in_section (menu, + section, + position, + G_MENU_ATTRIBUTE_LABEL, + G_VARIANT_TYPE_STRING); + + gchar *label = nullptr; + + if (label_value) + { + label = g_variant_dup_string (label_value, nullptr); + g_variant_unref (label_value); + } + + return label; +} + +void +g_lo_menu_set_action_and_target_value (GLOMenu *menu, + gint position, + const gchar *action, + GVariant *target_value) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GVariant *action_value; + + if (action != nullptr) + { + action_value = g_variant_new_string (action); + } + else + { + action_value = nullptr; + target_value = nullptr; + } + + g_lo_menu_set_attribute_value (menu, position, G_MENU_ATTRIBUTE_ACTION, action_value); + g_lo_menu_set_attribute_value (menu, position, G_MENU_ATTRIBUTE_TARGET, target_value); + g_lo_menu_set_attribute_value (menu, position, G_LO_MENU_ATTRIBUTE_SUBMENU_ACTION, nullptr); + + g_menu_model_items_changed (G_MENU_MODEL (menu), position, 1, 1); +} + +void +g_lo_menu_set_action_and_target_value_to_item_in_section (GLOMenu *menu, + gint section, + gint position, + const gchar *command, + GVariant *target_value) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_if_fail (model != nullptr); + + g_lo_menu_set_action_and_target_value (model, position, command, target_value); + + g_object_unref (model); +} + +void +g_lo_menu_set_accelerator_to_item_in_section (GLOMenu *menu, + gint section, + gint position, + const gchar *accelerator) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_if_fail (model != nullptr); + + GVariant *value; + + if (accelerator != nullptr) + value = g_variant_new_string (accelerator); + else + value = nullptr; + + g_lo_menu_set_attribute_value (model, position, G_LO_MENU_ATTRIBUTE_ACCELERATOR, value); + + // Notify the update. + g_menu_model_items_changed (G_MENU_MODEL (model), position, 1, 1); + + g_object_unref (model); +} + +gchar * +g_lo_menu_get_accelerator_from_item_in_section (GLOMenu *menu, + gint section, + gint position) +{ + g_return_val_if_fail (G_IS_LO_MENU (menu), nullptr); + + GVariant *accel_value = g_lo_menu_get_attribute_value_from_item_in_section (menu, + section, + position, + G_LO_MENU_ATTRIBUTE_ACCELERATOR, + G_VARIANT_TYPE_STRING); + + gchar *accel = nullptr; + + if (accel_value != nullptr) + { + accel = g_variant_dup_string (accel_value, nullptr); + g_variant_unref (accel_value); + } + + return accel; +} + +void +g_lo_menu_set_command_to_item_in_section (GLOMenu *menu, + gint section, + gint position, + const gchar *command) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_if_fail (model != nullptr); + + GVariant *value; + + if (command != nullptr) + value = g_variant_new_string (command); + else + value = nullptr; + + g_lo_menu_set_attribute_value (model, position, G_LO_MENU_ATTRIBUTE_COMMAND, value); + + // Notify the update. + g_menu_model_items_changed (G_MENU_MODEL (model), position, 1, 1); + + g_object_unref (model); +} + +gchar * +g_lo_menu_get_command_from_item_in_section (GLOMenu *menu, + gint section, + gint position) +{ + g_return_val_if_fail (G_IS_LO_MENU (menu), nullptr); + + GVariant *command_value = g_lo_menu_get_attribute_value_from_item_in_section (menu, + section, + position, + G_LO_MENU_ATTRIBUTE_COMMAND, + G_VARIANT_TYPE_STRING); + + gchar *command = nullptr; + + if (command_value != nullptr) + { + command = g_variant_dup_string (command_value, nullptr); + g_variant_unref (command_value); + } + + return command; +} + +static void +g_lo_menu_set_link (GLOMenu *menu, + gint position, + const gchar *link, + GMenuModel *model) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + g_return_if_fail (link != nullptr); + g_return_if_fail (valid_attribute_name (link)); + + if (position < 0 || o3tl::make_unsigned(position) >= menu->items->len) + position = menu->items->len - 1; + + struct item menu_item = g_array_index (menu->items, struct item, position); + + if (model != nullptr) + g_hash_table_insert (menu_item.links, g_strdup (link), g_object_ref (model)); + else + g_hash_table_remove (menu_item.links, link); +} + +void +g_lo_menu_insert_section (GLOMenu *menu, + gint position, + const gchar *label, + GMenuModel *section) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + if (position < 0 || o3tl::make_unsigned(position) > menu->items->len) + position = menu->items->len; + + struct item menu_item; + + g_lo_menu_struct_item_init(&menu_item); + + g_array_insert_val (menu->items, position, menu_item); + + g_lo_menu_set_label (menu, position, label); + g_lo_menu_set_link (menu, position, G_MENU_LINK_SECTION, section); + + g_menu_model_items_changed (G_MENU_MODEL (menu), position, 0, 1); +} + +void +g_lo_menu_new_section (GLOMenu *menu, + gint position, + const gchar *label) +{ + GMenuModel *section = G_MENU_MODEL (g_lo_menu_new()); + + g_lo_menu_insert_section (menu, position, label, section); + + g_object_unref (section); +} + +GLOMenu * +g_lo_menu_get_section (GLOMenu *menu, + gint section) +{ + g_return_val_if_fail (G_IS_LO_MENU (menu), nullptr); + + return G_LO_MENU (G_MENU_MODEL_CLASS (g_lo_menu_parent_class) + ->get_item_link (G_MENU_MODEL (menu), section, G_MENU_LINK_SECTION)); +} + +void +g_lo_menu_new_submenu_in_item_in_section (GLOMenu *menu, + gint section, + gint position) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + g_return_if_fail (0 <= section && o3tl::make_unsigned(section) < menu->items->len); + + GLOMenu* model = g_lo_menu_get_section (menu, section); + + g_return_if_fail (model != nullptr); + + if (0 <= position && o3tl::make_unsigned(position) < model->items->len) { + GMenuModel* submenu = G_MENU_MODEL (g_lo_menu_new()); + + g_lo_menu_set_link (model, position, G_MENU_LINK_SUBMENU, submenu); + + g_object_unref (submenu); + + g_menu_model_items_changed (G_MENU_MODEL (model), position, 1, 1); + + g_object_unref (model); + } +} + +GLOMenu * +g_lo_menu_get_submenu_from_item_in_section (GLOMenu *menu, + gint section, + gint position) +{ + g_return_val_if_fail (G_IS_LO_MENU (menu), nullptr); + g_return_val_if_fail (0 <= section && o3tl::make_unsigned(section) < menu->items->len, nullptr); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_val_if_fail (model != nullptr, nullptr); + + GLOMenu *submenu = nullptr; + + if (0 <= position && o3tl::make_unsigned(position) < model->items->len) + submenu = G_LO_MENU (G_MENU_MODEL_CLASS (g_lo_menu_parent_class) + ->get_item_link (G_MENU_MODEL (model), position, G_MENU_LINK_SUBMENU)); + //submenu = g_menu_model_get_item_link (G_MENU_MODEL (model), position, G_MENU_LINK_SUBMENU); + + g_object_unref (model); + + return submenu; +} + +void +g_lo_menu_set_submenu_action_to_item_in_section (GLOMenu *menu, + gint section, + gint position, + const gchar *action) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + + GMenuModel *model = G_MENU_MODEL (g_lo_menu_get_section (menu, section)); + + g_return_if_fail (model != nullptr); + + GVariant *value; + + if (action != nullptr) + value = g_variant_new_string (action); + else + value = nullptr; + + g_lo_menu_set_attribute_value (G_LO_MENU (model), position, G_LO_MENU_ATTRIBUTE_SUBMENU_ACTION, value); + + // Notify the update. + g_menu_model_items_changed (model, position, 1, 1); + + g_object_unref (model); +} + +static void +g_lo_menu_clear_item (struct item *menu_item) +{ + if (menu_item->attributes != nullptr) + g_hash_table_unref (menu_item->attributes); + if (menu_item->links != nullptr) + g_hash_table_unref (menu_item->links); +} + +void +g_lo_menu_remove (GLOMenu *menu, + gint position) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + g_return_if_fail (0 <= position && o3tl::make_unsigned(position) < menu->items->len); + + g_lo_menu_clear_item (&g_array_index (menu->items, struct item, position)); + g_array_remove_index (menu->items, position); + g_menu_model_items_changed (G_MENU_MODEL (menu), position, 1, 0); +} + +void +g_lo_menu_remove_from_section (GLOMenu *menu, + gint section, + gint position) +{ + g_return_if_fail (G_IS_LO_MENU (menu)); + g_return_if_fail (0 <= section && o3tl::make_unsigned(section) < menu->items->len); + + GLOMenu *model = g_lo_menu_get_section (menu, section); + + g_return_if_fail (model != nullptr); + + g_lo_menu_remove (model, position); + + g_object_unref (model); +} + +static void +g_lo_menu_finalize (GObject *object) +{ + GLOMenu *menu = G_LO_MENU (object); + struct item *items; + gint n_items; + gint i; + + n_items = menu->items->len; + items = reinterpret_cast(g_array_free (menu->items, FALSE)); + for (i = 0; i < n_items; i++) + g_lo_menu_clear_item (&items[i]); + g_free (items); + + G_OBJECT_CLASS (g_lo_menu_parent_class) + ->finalize (object); +} + +static void +g_lo_menu_init (GLOMenu *menu) +{ + menu->items = g_array_new (FALSE, FALSE, sizeof (struct item)); +} + +static void +g_lo_menu_class_init (GLOMenuClass *klass) +{ + GMenuModelClass *model_class = G_MENU_MODEL_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = g_lo_menu_finalize; + + model_class->is_mutable = g_lo_menu_is_mutable; + model_class->get_n_items = g_lo_menu_get_n_items; + model_class->get_item_attributes = g_lo_menu_get_item_attributes; + model_class->get_item_links = g_lo_menu_get_item_links; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtkcairo.cxx b/vcl/unx/gtk3/gtkcairo.cxx new file mode 100644 index 0000000000..f389f4d087 --- /dev/null +++ b/vcl/unx/gtk3/gtkcairo.cxx @@ -0,0 +1,129 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "gtkcairo.hxx" + +#include +#include +#include + +#include + +namespace +{ + Size get_surface_size(cairo_surface_t *surface) + { + cairo_t *cr = cairo_create(surface); + double x1, x2, y1, y2; + cairo_clip_extents(cr, &x1, &y1, &x2, &y2); + cairo_destroy(cr); + return Size(x2 - x1, y2 - y1); + } +} + +namespace cairo +{ + /** + * Surface::Surface: Create generic Canvas surface using given Cairo Surface + * + * @param pSurface Cairo Surface + * + * This constructor only stores data, it does no processing. + * It is used with e.g. cairo_image_surface_create_for_data() + * + * Set the mpSurface as pSurface + **/ + Gtk3Surface::Gtk3Surface(CairoSurfaceSharedPtr pSurface) + : mpGraphics(nullptr) + , cr(nullptr) + , mpSurface(std::move(pSurface)) + {} + + /** + * Surface::Surface: Create Canvas surface from Window reference. + * @param x horizontal location of the new surface + * @param y vertical location of the new surface + * @param width width of the new surface + * @param height height of the new surface + * + * Set the mpSurface to the new surface or NULL + **/ + Gtk3Surface::Gtk3Surface(const GtkSalGraphics* pGraphics, int x, int y, int width, int height) + : mpGraphics(pGraphics) + , cr(pGraphics->getCairoContext()) + { + cairo_surface_t* surface = cairo_get_target(cr); + mpSurface.reset( + cairo_surface_create_for_rectangle(surface, x, y, width, height), + &cairo_surface_destroy); + } + + Gtk3Surface::~Gtk3Surface() + { + if (cr) + cairo_destroy(cr); + } + + /** + * Surface::getCairo: Create Cairo (drawing object) for the Canvas surface + * + * @return new Cairo or NULL + **/ + CairoSharedPtr Gtk3Surface::getCairo() const + { + return CairoSharedPtr( cairo_create(mpSurface.get()), + &cairo_destroy ); + } + + /** + * Surface::getSimilar: Create new similar Canvas surface + * @param cairo_content_type format of the new surface (cairo_content_t from cairo/src/cairo.h) + * @param width width of the new surface + * @param height height of the new surface + * + * Creates a new Canvas surface. This normally creates platform native surface, even though + * generic function is used. + * + * Cairo surface from cairo_content_type (cairo_content_t) + * + * @return new surface or NULL + **/ + SurfaceSharedPtr Gtk3Surface::getSimilar(int cairo_content_type, int width, int height ) const + { + return std::make_shared( + CairoSurfaceSharedPtr( + cairo_surface_create_similar( mpSurface.get(), + static_cast(cairo_content_type), width, height ), + &cairo_surface_destroy )); + } + + void Gtk3Surface::flush() const + { + cairo_surface_flush(mpSurface.get()); + //Wonder if there is any benefit in using cairo_fill/stroke extents on + //every canvas call and only redrawing the union of those in a + //poor-mans-damage tracking + if (mpGraphics) + mpGraphics->WidgetQueueDraw(); + } + + VclPtr Gtk3Surface::createVirtualDevice() const + { + SystemGraphicsData aSystemGraphicsData; + + aSystemGraphicsData.nSize = sizeof(SystemGraphicsData); + aSystemGraphicsData.pSurface = mpSurface.get(); + + return VclPtr::Create(aSystemGraphicsData, + get_surface_size(mpSurface.get()), + DeviceFormat::WITHOUT_ALPHA); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtkcairo.hxx b/vcl/unx/gtk3/gtkcairo.hxx new file mode 100644 index 0000000000..c5912181fc --- /dev/null +++ b/vcl/unx/gtk3/gtkcairo.hxx @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include + +#include + +class GtkSalGraphics; +class OutputDevice; + +namespace cairo { + + class Gtk3Surface : public Surface + { + const GtkSalGraphics* mpGraphics; + cairo_t* cr; + CairoSurfaceSharedPtr mpSurface; + public: + /// takes over ownership of passed cairo_surface + explicit Gtk3Surface(CairoSurfaceSharedPtr pSurface); + /// create surface on subarea of given drawable + explicit Gtk3Surface(const GtkSalGraphics* pGraphics, int x, int y, int width, int height); + + // Surface interface + virtual CairoSharedPtr getCairo() const override; + virtual CairoSurfaceSharedPtr getCairoSurface() const override { return mpSurface; } + virtual SurfaceSharedPtr getSimilar(int nContentType, int width, int height) const override; + + virtual VclPtr createVirtualDevice() const override; + + virtual void flush() const override; + + virtual ~Gtk3Surface() override; + }; + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtkdata.cxx b/vcl/unx/gtk3/gtkdata.cxx new file mode 100644 index 0000000000..f595369f8b --- /dev/null +++ b/vcl/unx/gtk3/gtkdata.cxx @@ -0,0 +1,984 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include +#if defined(FREEBSD) || defined(NETBSD) +#include +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +using namespace vcl_sal; + +/*************************************************************** + * class GtkSalDisplay * + ***************************************************************/ + +GtkSalDisplay::GtkSalDisplay( GdkDisplay* pDisplay ) : + m_pSys( GtkSalSystem::GetSingleton() ), + m_pGdkDisplay( pDisplay ), + m_bStartupCompleted( false ) +{ + for(GdkCursor* & rpCsr : m_aCursors) + rpCsr = nullptr; + + if ( getenv( "SAL_IGNOREXERRORS" ) ) + GetGenericUnixSalData()->ErrorTrapPush(); // and leak the trap + + gtk_widget_set_default_direction(AllSettings::GetLayoutRTL() ? GTK_TEXT_DIR_RTL : GTK_TEXT_DIR_LTR); +} + +GtkSalDisplay::~GtkSalDisplay() +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + if( !m_bStartupCompleted ) + gdk_notify_startup_complete(); + + for(GdkCursor* & rpCsr : m_aCursors) + if( rpCsr ) + gdk_cursor_unref( rpCsr ); +#endif +} + +#if GTK_CHECK_VERSION(4, 0, 0) + +static void signalMonitorsChanged(GListModel*, gpointer data) +{ + GtkSalDisplay* pDisp = static_cast(data); + pDisp->emitDisplayChanged(); +} + +#else + +static void signalScreenSizeChanged( GdkScreen* pScreen, gpointer data ) +{ + GtkSalDisplay* pDisp = static_cast(data); + pDisp->screenSizeChanged( pScreen ); +} + +static void signalMonitorsChanged( GdkScreen* pScreen, gpointer data ) +{ + GtkSalDisplay* pDisp = static_cast(data); + pDisp->monitorsChanged( pScreen ); +} + +void GtkSalDisplay::screenSizeChanged( GdkScreen const * pScreen ) +{ + m_pSys->countScreenMonitors(); + if (pScreen) + emitDisplayChanged(); +} + +void GtkSalDisplay::monitorsChanged( GdkScreen const * pScreen ) +{ + m_pSys->countScreenMonitors(); + if (pScreen) + emitDisplayChanged(); +} +#endif + +GdkCursor* GtkSalDisplay::getFromSvg(OUString const & name, int nXHot, int nYHot) +{ + GdkPixbuf* pPixBuf = load_icon_by_name(name); + assert(pPixBuf && "missing image?"); + if (!pPixBuf) + return nullptr; + +#if !GTK_CHECK_VERSION(4, 0, 0) + guint nDefaultCursorSize = gdk_display_get_default_cursor_size( m_pGdkDisplay ); + int nPixWidth = gdk_pixbuf_get_width(pPixBuf); + int nPixHeight = gdk_pixbuf_get_height(pPixBuf); + double fScalefactor = static_cast(nDefaultCursorSize) / std::max(nPixWidth, nPixHeight); + GdkPixbuf* pScaledPixBuf = gdk_pixbuf_scale_simple(pPixBuf, + nPixWidth * fScalefactor, + nPixHeight * fScalefactor, + GDK_INTERP_HYPER); + g_object_unref(pPixBuf); + return gdk_cursor_new_from_pixbuf(m_pGdkDisplay, pScaledPixBuf, + nXHot * fScalefactor, nYHot * fScalefactor); +#else + GdkTexture* pTexture = gdk_texture_new_for_pixbuf(pPixBuf); + g_object_unref(pPixBuf); + return gdk_cursor_new_from_texture(pTexture, nXHot, nYHot, nullptr); +#endif +} + +#define MAKE_CURSOR( vcl_name, name, name2 ) \ + case vcl_name: \ + pCursor = getFromSvg(name2, name##curs_x_hot, name##curs_y_hot); \ + break + +#if !GTK_CHECK_VERSION(4, 0, 0) +#define MAP_BUILTIN( vcl_name, gdk3_name, css_name ) \ + case vcl_name: \ + pCursor = gdk_cursor_new_for_display( m_pGdkDisplay, gdk3_name ); \ + break +#else +#define MAP_BUILTIN( vcl_name, gdk3_name, css_name ) \ + case vcl_name: \ + pCursor = gdk_cursor_new_from_name(css_name, nullptr); \ + break +#endif + +GdkCursor *GtkSalDisplay::getCursor( PointerStyle ePointerStyle ) +{ + if ( !m_aCursors[ ePointerStyle ] ) + { + GdkCursor *pCursor = nullptr; + + switch( ePointerStyle ) + { + MAP_BUILTIN( PointerStyle::Arrow, GDK_LEFT_PTR, "default" ); + MAP_BUILTIN( PointerStyle::Text, GDK_XTERM, "text" ); + MAP_BUILTIN( PointerStyle::Help, GDK_QUESTION_ARROW, "help" ); + MAP_BUILTIN( PointerStyle::Cross, GDK_CROSSHAIR, "crosshair" ); + MAP_BUILTIN( PointerStyle::Wait, GDK_WATCH, "wait" ); + + MAP_BUILTIN( PointerStyle::NSize, GDK_SB_V_DOUBLE_ARROW, "n-resize" ); + MAP_BUILTIN( PointerStyle::SSize, GDK_SB_V_DOUBLE_ARROW, "s-resize" ); + MAP_BUILTIN( PointerStyle::WSize, GDK_SB_H_DOUBLE_ARROW, "w-resize" ); + MAP_BUILTIN( PointerStyle::ESize, GDK_SB_H_DOUBLE_ARROW, "e-resize" ); + + MAP_BUILTIN( PointerStyle::NWSize, GDK_TOP_LEFT_CORNER, "nw-resize" ); + MAP_BUILTIN( PointerStyle::NESize, GDK_TOP_RIGHT_CORNER, "ne-resize" ); + MAP_BUILTIN( PointerStyle::SWSize, GDK_BOTTOM_LEFT_CORNER, "sw-resize" ); + MAP_BUILTIN( PointerStyle::SESize, GDK_BOTTOM_RIGHT_CORNER, "se-resize" ); + + MAP_BUILTIN( PointerStyle::WindowNSize, GDK_TOP_SIDE, "n-resize" ); + MAP_BUILTIN( PointerStyle::WindowSSize, GDK_BOTTOM_SIDE, "s-resize" ); + MAP_BUILTIN( PointerStyle::WindowWSize, GDK_LEFT_SIDE, "w-resize" ); + MAP_BUILTIN( PointerStyle::WindowESize, GDK_RIGHT_SIDE, "e-resize" ); + + MAP_BUILTIN( PointerStyle::WindowNWSize, GDK_TOP_LEFT_CORNER, "nw-resize" ); + MAP_BUILTIN( PointerStyle::WindowNESize, GDK_TOP_RIGHT_CORNER, "ne-resize" ); + MAP_BUILTIN( PointerStyle::WindowSWSize, GDK_BOTTOM_LEFT_CORNER, "sw-resize" ); + MAP_BUILTIN( PointerStyle::WindowSESize, GDK_BOTTOM_RIGHT_CORNER, "se-resize" ); + + MAP_BUILTIN( PointerStyle::HSizeBar, GDK_SB_H_DOUBLE_ARROW, "col-resize" ); + MAP_BUILTIN( PointerStyle::VSizeBar, GDK_SB_V_DOUBLE_ARROW, "row-resize" ); + + MAP_BUILTIN( PointerStyle::RefHand, GDK_HAND2, "grab" ); + MAP_BUILTIN( PointerStyle::Hand, GDK_HAND2, "grab" ); + +#if !GTK_CHECK_VERSION(4, 0, 0) + MAP_BUILTIN( PointerStyle::Pen, GDK_PENCIL, "" ); +#else + MAKE_CURSOR( PointerStyle::Pen, pen_, RID_CURSOR_PEN ); +#endif + + MAP_BUILTIN( PointerStyle::HSplit, GDK_SB_H_DOUBLE_ARROW, "col-resize" ); + MAP_BUILTIN( PointerStyle::VSplit, GDK_SB_V_DOUBLE_ARROW, "row-resize" ); + + MAP_BUILTIN( PointerStyle::Move, GDK_FLEUR, "move" ); + +#if !GTK_CHECK_VERSION(4, 0, 0) + MAKE_CURSOR( PointerStyle::Null, null, RID_CURSOR_NULL ); +#else + MAP_BUILTIN( PointerStyle::Null, 0, "none" ); +#endif + + MAKE_CURSOR( PointerStyle::Magnify, magnify_, RID_CURSOR_MAGNIFY ); + MAKE_CURSOR( PointerStyle::Fill, fill_, RID_CURSOR_FILL ); + MAKE_CURSOR( PointerStyle::MoveData, movedata_, RID_CURSOR_MOVE_DATA ); + MAKE_CURSOR( PointerStyle::CopyData, copydata_, RID_CURSOR_COPY_DATA ); + MAKE_CURSOR( PointerStyle::MoveFile, movefile_, RID_CURSOR_MOVE_FILE ); + MAKE_CURSOR( PointerStyle::CopyFile, copyfile_, RID_CURSOR_COPY_FILE ); + MAKE_CURSOR( PointerStyle::MoveFiles, movefiles_, RID_CURSOR_MOVE_FILES ); + MAKE_CURSOR( PointerStyle::CopyFiles, copyfiles_, RID_CURSOR_COPY_FILES ); + +#if !GTK_CHECK_VERSION(4, 0, 0) + MAKE_CURSOR( PointerStyle::NotAllowed, nodrop_, RID_CURSOR_NOT_ALLOWED ); +#else + MAP_BUILTIN( PointerStyle::NotAllowed, 0, "not-allowed" ); +#endif + + MAKE_CURSOR( PointerStyle::Rotate, rotate_, RID_CURSOR_ROTATE ); + MAKE_CURSOR( PointerStyle::HShear, hshear_, RID_CURSOR_H_SHEAR ); + MAKE_CURSOR( PointerStyle::VShear, vshear_, RID_CURSOR_V_SHEAR ); + MAKE_CURSOR( PointerStyle::DrawLine, drawline_, RID_CURSOR_DRAW_LINE ); + MAKE_CURSOR( PointerStyle::DrawRect, drawrect_, RID_CURSOR_DRAW_RECT ); + MAKE_CURSOR( PointerStyle::DrawPolygon, drawpolygon_, RID_CURSOR_DRAW_POLYGON ); + MAKE_CURSOR( PointerStyle::DrawBezier, drawbezier_, RID_CURSOR_DRAW_BEZIER ); + MAKE_CURSOR( PointerStyle::DrawArc, drawarc_, RID_CURSOR_DRAW_ARC ); + MAKE_CURSOR( PointerStyle::DrawPie, drawpie_, RID_CURSOR_DRAW_PIE ); + MAKE_CURSOR( PointerStyle::DrawCircleCut, drawcirclecut_, RID_CURSOR_DRAW_CIRCLE_CUT ); + MAKE_CURSOR( PointerStyle::DrawEllipse, drawellipse_, RID_CURSOR_DRAW_ELLIPSE ); + MAKE_CURSOR( PointerStyle::DrawConnect, drawconnect_, RID_CURSOR_DRAW_CONNECT ); + MAKE_CURSOR( PointerStyle::DrawText, drawtext_, RID_CURSOR_DRAW_TEXT ); + MAKE_CURSOR( PointerStyle::Mirror, mirror_, RID_CURSOR_MIRROR ); + MAKE_CURSOR( PointerStyle::Crook, crook_, RID_CURSOR_CROOK ); + MAKE_CURSOR( PointerStyle::Crop, crop_, RID_CURSOR_CROP ); + MAKE_CURSOR( PointerStyle::MovePoint, movepoint_, RID_CURSOR_MOVE_POINT ); + MAKE_CURSOR( PointerStyle::MoveBezierWeight, movebezierweight_, RID_CURSOR_MOVE_BEZIER_WEIGHT ); + MAKE_CURSOR( PointerStyle::DrawFreehand, drawfreehand_, RID_CURSOR_DRAW_FREEHAND ); + MAKE_CURSOR( PointerStyle::DrawCaption, drawcaption_, RID_CURSOR_DRAW_CAPTION ); + MAKE_CURSOR( PointerStyle::LinkData, linkdata_, RID_CURSOR_LINK_DATA ); + MAKE_CURSOR( PointerStyle::MoveDataLink, movedlnk_, RID_CURSOR_MOVE_DATA_LINK ); + MAKE_CURSOR( PointerStyle::CopyDataLink, copydlnk_, RID_CURSOR_COPY_DATA_LINK ); + MAKE_CURSOR( PointerStyle::LinkFile, linkfile_, RID_CURSOR_LINK_FILE ); + MAKE_CURSOR( PointerStyle::MoveFileLink, moveflnk_, RID_CURSOR_MOVE_FILE_LINK ); + MAKE_CURSOR( PointerStyle::CopyFileLink, copyflnk_, RID_CURSOR_COPY_FILE_LINK ); + MAKE_CURSOR( PointerStyle::Chart, chart_, RID_CURSOR_CHART ); + MAKE_CURSOR( PointerStyle::Detective, detective_, RID_CURSOR_DETECTIVE ); + MAKE_CURSOR( PointerStyle::PivotCol, pivotcol_, RID_CURSOR_PIVOT_COLUMN ); + MAKE_CURSOR( PointerStyle::PivotRow, pivotrow_, RID_CURSOR_PIVOT_ROW ); + MAKE_CURSOR( PointerStyle::PivotField, pivotfld_, RID_CURSOR_PIVOT_FIELD ); + MAKE_CURSOR( PointerStyle::PivotDelete, pivotdel_, RID_CURSOR_PIVOT_DELETE ); + MAKE_CURSOR( PointerStyle::Chain, chain_, RID_CURSOR_CHAIN ); + MAKE_CURSOR( PointerStyle::ChainNotAllowed, chainnot_, RID_CURSOR_CHAIN_NOT_ALLOWED ); + MAKE_CURSOR( PointerStyle::AutoScrollN, asn_, RID_CURSOR_AUTOSCROLL_N ); + MAKE_CURSOR( PointerStyle::AutoScrollS, ass_, RID_CURSOR_AUTOSCROLL_S ); + MAKE_CURSOR( PointerStyle::AutoScrollW, asw_, RID_CURSOR_AUTOSCROLL_W ); + MAKE_CURSOR( PointerStyle::AutoScrollE, ase_, RID_CURSOR_AUTOSCROLL_E ); + MAKE_CURSOR( PointerStyle::AutoScrollNW, asnw_, RID_CURSOR_AUTOSCROLL_NW ); + MAKE_CURSOR( PointerStyle::AutoScrollNE, asne_, RID_CURSOR_AUTOSCROLL_NE ); + MAKE_CURSOR( PointerStyle::AutoScrollSW, assw_, RID_CURSOR_AUTOSCROLL_SW ); + MAKE_CURSOR( PointerStyle::AutoScrollSE, asse_, RID_CURSOR_AUTOSCROLL_SE ); + MAKE_CURSOR( PointerStyle::AutoScrollNS, asns_, RID_CURSOR_AUTOSCROLL_NS ); + MAKE_CURSOR( PointerStyle::AutoScrollWE, aswe_, RID_CURSOR_AUTOSCROLL_WE ); + MAKE_CURSOR( PointerStyle::AutoScrollNSWE, asnswe_, RID_CURSOR_AUTOSCROLL_NSWE ); + MAKE_CURSOR( PointerStyle::TextVertical, vertcurs_, RID_CURSOR_TEXT_VERTICAL ); + + // #i32329# + MAKE_CURSOR( PointerStyle::TabSelectS, tblsels_, RID_CURSOR_TAB_SELECT_S ); + MAKE_CURSOR( PointerStyle::TabSelectE, tblsele_, RID_CURSOR_TAB_SELECT_E ); + MAKE_CURSOR( PointerStyle::TabSelectSE, tblselse_, RID_CURSOR_TAB_SELECT_SE ); + MAKE_CURSOR( PointerStyle::TabSelectW, tblselw_, RID_CURSOR_TAB_SELECT_W ); + MAKE_CURSOR( PointerStyle::TabSelectSW, tblselsw_, RID_CURSOR_TAB_SELECT_SW ); + + MAKE_CURSOR( PointerStyle::HideWhitespace, hidewhitespace_, RID_CURSOR_HIDE_WHITESPACE ); + MAKE_CURSOR( PointerStyle::ShowWhitespace, showwhitespace_, RID_CURSOR_SHOW_WHITESPACE ); + MAKE_CURSOR( PointerStyle::FatCross, fatcross_, RID_CURSOR_FATCROSS ); + + default: + SAL_WARN( "vcl.gtk", "pointer " << static_cast(ePointerStyle) << "not implemented" ); + break; + } + if( !pCursor ) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + pCursor = gdk_cursor_new_for_display( m_pGdkDisplay, GDK_LEFT_PTR ); +#else + pCursor = gdk_cursor_new_from_name("normal", nullptr); +#endif + } + + m_aCursors[ ePointerStyle ] = pCursor; + } + + return m_aCursors[ ePointerStyle ]; +} + +int GtkSalDisplay::CaptureMouse( SalFrame* pSFrame ) +{ + GtkSalFrame* pFrame = static_cast(pSFrame); + + if( !pFrame ) + { + if( m_pCapture ) + static_cast(m_pCapture)->grabPointer( false, false, false ); + m_pCapture = nullptr; + return 0; + } + + if( m_pCapture ) + { + if( pFrame == m_pCapture ) + return 1; + static_cast(m_pCapture)->grabPointer( false, false, false ); + } + + m_pCapture = pFrame; + pFrame->grabPointer( true, false, false ); + return 1; +} + +/********************************************************************** + * class GtkSalData * + **********************************************************************/ + +GtkSalData::GtkSalData() + : GenericUnixSalData() +{ + m_pUserEvent = nullptr; +} + +#if defined(GDK_WINDOWING_X11) +static XIOErrorHandler aOrigXIOErrorHandler = nullptr; + +extern "C" { + +static int XIOErrorHdl(Display *) +{ + fprintf(stderr, "X IO Error\n"); + _exit(1); + // avoid crashes in unrelated threads that still run while atexit + // handlers are in progress +} + +} +#endif + +GtkSalData::~GtkSalData() +{ + // sanity check: at this point nobody should be yielding, but wake them + // up anyway before the condition they're waiting on gets destroyed. + m_aDispatchCondition.set(); + + osl::MutexGuard g( m_aDispatchMutex ); + if (m_pUserEvent) + { + g_source_destroy (m_pUserEvent); + g_source_unref (m_pUserEvent); + m_pUserEvent = nullptr; + } +#if defined(GDK_WINDOWING_X11) + if (DLSYM_GDK_IS_X11_DISPLAY(gdk_display_get_default())) + XSetIOErrorHandler(aOrigXIOErrorHandler); +#endif +} + +void GtkSalData::Dispose() +{ + deInitNWF(); +} + +/// Allows events to be processed, returns true if we processed an event. +bool GtkSalData::Yield( bool bWait, bool bHandleAllCurrentEvents ) +{ + /* #i33212# only enter g_main_context_iteration in one thread at any one + * time, else one of them potentially will never end as long as there is + * another thread in there. Having only one yielding thread actually dispatch + * fits the vcl event model (see e.g. the generic plugin). + */ + bool bDispatchThread = false; + bool bWasEvent = false; + { + // release YieldMutex (and re-acquire at block end) + SolarMutexReleaser aReleaser; + if( m_aDispatchMutex.tryToAcquire() ) + bDispatchThread = true; + else if( ! bWait ) + { + return false; // someone else is waiting already, return + } + + if( bDispatchThread ) + { + int nMaxEvents = bHandleAllCurrentEvents ? 100 : 1; + bool wasOneEvent = true; + while( nMaxEvents-- && wasOneEvent ) + { + wasOneEvent = g_main_context_iteration( nullptr, bWait && !bWasEvent ); + if( wasOneEvent ) + bWasEvent = true; + } + if (m_aException) + std::rethrow_exception(m_aException); + } + else if( bWait ) + { + /* #i41693# in case the dispatch thread hangs in join + * for this thread the condition will never be set + * workaround: timeout of 1 second an emergency exit + */ + // we are the dispatch thread + m_aDispatchCondition.reset(); + m_aDispatchCondition.wait(std::chrono::seconds(1)); + } + } + + if( bDispatchThread ) + { + m_aDispatchMutex.release(); + if( bWasEvent ) + m_aDispatchCondition.set(); // trigger non dispatch thread yields + } + + return bWasEvent; +} + +static GtkStyleProvider* CreateStyleProvider() +{ + /* + set a provider to: + + 1) allow certain widgets to have no padding + + 1.a) little close button in menubar to close back to start-center + 1.b) and small buttons in view->data sources (button.small-button) + 1.c.1) gtk3 small toolbar button in infobars (toolbar.small-button button) + 1.c.2) gtk4 small toolbar button in infobars (box.small-button button) + 1.d) comboboxes in the data browser for tdf#137695 (box#combobox button.small-button, + which would instead be combobox button.small-button if we didn't replace GtkComboBox, + see GtkInstanceComboBox for an explanation for why we do that) + 1.e) entry in the data browser for tdf#137695 (entry.small-button) + 1.f) spinbutton in the data browser tdf#141633 (spinbutton.small-button) + + 2) hide the unwanted active tab in an 'overflow' notebook of double-decker notebooks. + (tdf#122623) it's nigh impossible to have a GtkNotebook without an active (checked) tab, + so theme the unwanted tab into invisibility + */ + GtkCssProvider* pStyleProvider = gtk_css_provider_new(); + static const gchar data[] = + "button.small-button, toolbar.small-button button, box.small-button button, " + "combobox.small-button *.combo, box#combobox.small-button *.combo, entry.small-button, " + "spinbutton.small-button, spinbutton.small-button entry, spinbutton.small-button button { " + "padding: 0; margin-left: 0; margin-right: 0; margin-top: 0; margin-bottom: 0;" + "border-width: 0; min-height: 0; min-width: 0; }" +#if GTK_CHECK_VERSION(4, 0, 0) + // we basically assumed during dialog design that the frame's were invisible, because + // they used to be in the default theme during gtk3 + "frame { border-style: none; }" +#endif + "notebook.overflow > header.top > tabs > tab:checked { " + "box-shadow: none; padding: 0 0 0 0; margin: 0 0 0 0;" + "border-image: none; border-image-width: 0 0 0 0;" + "background-image: none; background-color: transparent;" + "border-radius: 0 0 0 0; border-width: 0 0 0 0;" + "border-style: none; border-color: transparent;" + "opacity: 0; min-height: 0; min-width: 0; }" + // https://css-tricks.com/restart-css-animation/ + // This animation appears twice with two different names so we can change + // the class from "call_attention_1" to "call_attention_2" to restart the + // animation + "@keyframes shinkandrestore1 { 50% { margin-left: 15px; margin-right: 15px; opacity: 0.5; } }" + "@keyframes shinkandrestore2 { 50% { margin-left: 15px; margin-right: 15px; opacity: 0.5; } }" + " *.call_attention_1 {" + "animation-name: shinkandrestore1; animation-duration: 1s; " + "animation-timing-function: linear; animation-iteration-count: 2; }" + " *.call_attention_2 {" + "animation-name: shinkandrestore2; animation-duration: 1s; " + "animation-timing-function: linear; animation-iteration-count: 2; }"; + css_provider_load_from_data(pStyleProvider, data, -1); + return GTK_STYLE_PROVIDER(pStyleProvider); +} + +void GtkSalData::Init() +{ + SAL_INFO( "vcl.gtk", "GtkMainloop::Init()" ); + + /* + * open connection to X11 Display + * try in this order: + * o -display command line parameter, + * o $DISPLAY environment variable + * o default display + */ + + GdkDisplay *pGdkDisp = nullptr; + + // is there a -display command line parameter? + rtl_TextEncoding aEnc = osl_getThreadTextEncoding(); + int nParams = osl_getCommandArgCount(); + OString aDisplay; + OUString aParam, aBin; + char** pCmdLineAry = new char*[ nParams+1 ]; + osl_getExecutableFile( &aParam.pData ); + osl_getSystemPathFromFileURL( aParam.pData, &aBin.pData ); + pCmdLineAry[0] = g_strdup( OUStringToOString( aBin, aEnc ).getStr() ); + for (int i = 0; i < nParams; ++i) + { + osl_getCommandArg(i, &aParam.pData ); + OString aBParam( OUStringToOString( aParam, aEnc ) ); + + if( aParam == "-display" || aParam == "--display" ) + { + pCmdLineAry[i+1] = g_strdup( "--display" ); + osl_getCommandArg(i+1, &aParam.pData ); + aDisplay = OUStringToOString( aParam, aEnc ); + } + else + pCmdLineAry[i+1] = g_strdup( aBParam.getStr() ); + } + // add executable + nParams++; + + g_set_application_name(SalGenericSystem::getFrameClassName()); + + // Set consistent name of the root accessible + OUString aAppName = Application::GetAppName(); + if( !aAppName.isEmpty() ) + { + OString aPrgName = OUStringToOString(aAppName, aEnc); + g_set_prgname(aPrgName.getStr()); + } + + // init gtk/gdk +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_init_check(); +#else + gtk_init_check( &nParams, &pCmdLineAry ); +#endif + + for (int i = 0; i < nParams; ++i) + g_free( pCmdLineAry[i] ); + delete [] pCmdLineAry; + +#if OSL_DEBUG_LEVEL > 1 + if (g_getenv("SAL_DEBUG_UPDATES")) + gdk_window_set_debug_updates (TRUE); +#endif + + pGdkDisp = gdk_display_get_default(); + if ( !pGdkDisp ) + { + OUString aProgramFileURL; + osl_getExecutableFile( &aProgramFileURL.pData ); + OUString aProgramSystemPath; + osl_getSystemPathFromFileURL (aProgramFileURL.pData, &aProgramSystemPath.pData); + OString aProgramName = OUStringToOString( + aProgramSystemPath, + osl_getThreadTextEncoding() ); + fprintf( stderr, "%s X11 error: Can't open display: %s\n", + aProgramName.getStr(), aDisplay.getStr()); + fprintf( stderr, " Set DISPLAY environment variable, use -display option\n"); + fprintf( stderr, " or check permissions of your X-Server\n"); + fprintf( stderr, " (See \"man X\" resp. \"man xhost\" for details)\n"); + fflush( stderr ); + exit(0); + } + + ErrorTrapPush(); + +#if defined(GDK_WINDOWING_X11) + if (DLSYM_GDK_IS_X11_DISPLAY(pGdkDisp)) + aOrigXIOErrorHandler = XSetIOErrorHandler(XIOErrorHdl); +#endif + + GtkSalDisplay *pDisplay = new GtkSalDisplay( pGdkDisp ); + SetDisplay( pDisplay ); + +#if GTK_CHECK_VERSION(4, 0, 0) + pDisplay->emitDisplayChanged(); + GListModel *pMonitors = gdk_display_get_monitors(pGdkDisp); + g_signal_connect(pMonitors, "items-changed", G_CALLBACK(signalMonitorsChanged), pDisplay); + + gtk_style_context_add_provider_for_display(pGdkDisp, CreateStyleProvider(), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +#else + int nScreens = gdk_display_get_n_screens( pGdkDisp ); + for( int n = 0; n < nScreens; n++ ) + { + GdkScreen *pScreen = gdk_display_get_screen( pGdkDisp, n ); + if (!pScreen) + continue; + + pDisplay->screenSizeChanged( pScreen ); + pDisplay->monitorsChanged( pScreen ); + // add signal handler to notify screen size changes + g_signal_connect( G_OBJECT(pScreen), "size-changed", + G_CALLBACK(signalScreenSizeChanged), pDisplay ); + g_signal_connect( G_OBJECT(pScreen), "monitors-changed", + G_CALLBACK(signalMonitorsChanged), pDisplay ); + + gtk_style_context_add_provider_for_screen(pScreen, CreateStyleProvider(), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } +#endif +} + +void GtkSalData::ErrorTrapPush() +{ +#if GTK_CHECK_VERSION(4, 0, 0) +# if defined(GDK_WINDOWING_X11) + GdkDisplay* pGdkDisp = gdk_display_get_default(); + if (DLSYM_GDK_IS_X11_DISPLAY(pGdkDisp)) + gdk_x11_display_error_trap_push(pGdkDisp); +# endif +#else + gdk_error_trap_push(); +#endif +} + +bool GtkSalData::ErrorTrapPop( bool bIgnoreError ) +{ +#if GTK_CHECK_VERSION(4, 0, 0) +# if defined(GDK_WINDOWING_X11) + GdkDisplay* pGdkDisp = gdk_display_get_default(); + if (DLSYM_GDK_IS_X11_DISPLAY(pGdkDisp)) + { + if (bIgnoreError) + { + gdk_x11_display_error_trap_pop_ignored(pGdkDisp); // faster + return false; + } + return gdk_x11_display_error_trap_pop(pGdkDisp) != 0; + } +# endif + return false; +#else + if (bIgnoreError) + { + gdk_error_trap_pop_ignored (); // faster + return false; + } + return gdk_error_trap_pop () != 0; +#endif +} + +#if !GLIB_CHECK_VERSION(2,32,0) +#define G_SOURCE_REMOVE FALSE +#endif + +extern "C" { + + struct SalGtkTimeoutSource { + GSource aParent; + GTimeVal aFireTime; + GtkSalTimer *pInstance; + }; + + static void sal_gtk_timeout_defer( SalGtkTimeoutSource *pTSource ) + { + g_get_current_time( &pTSource->aFireTime ); + g_time_val_add( &pTSource->aFireTime, pTSource->pInstance->m_nTimeoutMS * 1000 ); + } + + static gboolean sal_gtk_timeout_expired( SalGtkTimeoutSource *pTSource, + gint *nTimeoutMS, GTimeVal const *pTimeNow ) + { + glong nDeltaSec = pTSource->aFireTime.tv_sec - pTimeNow->tv_sec; + glong nDeltaUSec = pTSource->aFireTime.tv_usec - pTimeNow->tv_usec; + if( nDeltaSec < 0 || ( nDeltaSec == 0 && nDeltaUSec < 0) ) + { + *nTimeoutMS = 0; + return true; + } + if( nDeltaUSec < 0 ) + { + nDeltaUSec += 1000000; + nDeltaSec -= 1; + } + // if the clock changes backwards we need to cope ... + if( o3tl::make_unsigned(nDeltaSec) > 1 + ( pTSource->pInstance->m_nTimeoutMS / 1000 ) ) + { + sal_gtk_timeout_defer( pTSource ); + return true; + } + + *nTimeoutMS = MIN( G_MAXINT, ( nDeltaSec * 1000 + (nDeltaUSec + 999) / 1000 ) ); + + return *nTimeoutMS == 0; + } + + static gboolean sal_gtk_timeout_prepare( GSource *pSource, gint *nTimeoutMS ) + { + SalGtkTimeoutSource *pTSource = reinterpret_cast(pSource); + + GTimeVal aTimeNow; + g_get_current_time( &aTimeNow ); + + return sal_gtk_timeout_expired( pTSource, nTimeoutMS, &aTimeNow ); + } + + static gboolean sal_gtk_timeout_check( GSource *pSource ) + { + SalGtkTimeoutSource *pTSource = reinterpret_cast(pSource); + + GTimeVal aTimeNow; + g_get_current_time( &aTimeNow ); + + return ( pTSource->aFireTime.tv_sec < aTimeNow.tv_sec || + ( pTSource->aFireTime.tv_sec == aTimeNow.tv_sec && + pTSource->aFireTime.tv_usec < aTimeNow.tv_usec ) ); + } + + static gboolean sal_gtk_timeout_dispatch( GSource *pSource, GSourceFunc, gpointer ) + { + SalGtkTimeoutSource *pTSource = reinterpret_cast(pSource); + + if( !pTSource->pInstance ) + return FALSE; + + SolarMutexGuard aGuard; + + sal_gtk_timeout_defer( pTSource ); + + ImplSVData* pSVData = ImplGetSVData(); + if( pSVData->maSchedCtx.mpSalTimer ) + pSVData->maSchedCtx.mpSalTimer->CallCallback(); + + return G_SOURCE_REMOVE; + } + + static GSourceFuncs sal_gtk_timeout_funcs = + { + sal_gtk_timeout_prepare, + sal_gtk_timeout_check, + sal_gtk_timeout_dispatch, + nullptr, nullptr, nullptr + }; +} + +static SalGtkTimeoutSource * +create_sal_gtk_timeout( GtkSalTimer *pTimer ) +{ + GSource *pSource = g_source_new( &sal_gtk_timeout_funcs, sizeof( SalGtkTimeoutSource ) ); + SalGtkTimeoutSource *pTSource = reinterpret_cast(pSource); + pTSource->pInstance = pTimer; + + // #i36226# timers should be executed with lower priority + // than XEvents like in generic plugin + g_source_set_priority( pSource, G_PRIORITY_LOW ); + g_source_set_can_recurse( pSource, true ); + g_source_set_callback( pSource, + /* unused dummy */ g_idle_remove_by_data, + nullptr, nullptr ); + g_source_attach( pSource, g_main_context_default() ); +#ifdef DBG_UTIL + g_source_set_name( pSource, "VCL timeout source" ); +#endif + + sal_gtk_timeout_defer( pTSource ); + + return pTSource; +} + +GtkSalTimer::GtkSalTimer() + : m_pTimeout(nullptr) + , m_nTimeoutMS(0) +{ +} + +GtkSalTimer::~GtkSalTimer() +{ + GetGtkInstance()->RemoveTimer(); + Stop(); +} + +bool GtkSalTimer::Expired() +{ + if( !m_pTimeout || g_source_is_destroyed( &m_pTimeout->aParent ) ) + return false; + + gint nDummy = 0; + GTimeVal aTimeNow; + g_get_current_time( &aTimeNow ); + return !!sal_gtk_timeout_expired( m_pTimeout, &nDummy, &aTimeNow); +} + +void GtkSalTimer::Start( sal_uInt64 nMS ) +{ + // glib is not 64bit safe in this regard. + assert( nMS <= G_MAXINT ); + if ( nMS > G_MAXINT ) + nMS = G_MAXINT; + m_nTimeoutMS = nMS; // for restarting + Stop(); // FIXME: ideally re-use an existing m_pTimeout + m_pTimeout = create_sal_gtk_timeout( this ); +} + +void GtkSalTimer::Stop() +{ + if( m_pTimeout ) + { + g_source_destroy( &m_pTimeout->aParent ); + g_source_unref( &m_pTimeout->aParent ); + m_pTimeout = nullptr; + } +} + +extern "C" { + static gboolean call_userEventFn( void *data ) + { + SolarMutexGuard aGuard; + const SalGenericDisplay *pDisplay = GetGenericUnixSalData()->GetDisplay(); + if ( pDisplay ) + { + GtkSalDisplay *pThisDisplay = static_cast(data)->GetGtkDisplay(); + assert(static_cast(pThisDisplay) == pDisplay); + pThisDisplay->DispatchInternalEvent(); + } + return true; + } +} + +void GtkSalData::TriggerUserEventProcessing() +{ + if (m_pUserEvent) + g_main_context_wakeup (nullptr); // really needed ? + else // nothing pending anyway + { + m_pUserEvent = g_idle_source_new(); + // tdf#110737 set user-events to a lower priority than system redraw + // events, which is G_PRIORITY_HIGH_IDLE + 20, so presentations + // queue-redraw has a chance to be fulfilled + g_source_set_priority (m_pUserEvent, G_PRIORITY_HIGH_IDLE + 30); + g_source_set_can_recurse (m_pUserEvent, true); + g_source_set_callback (m_pUserEvent, call_userEventFn, + static_cast(this), nullptr); + g_source_attach (m_pUserEvent, g_main_context_default ()); + } +} + +void GtkSalData::TriggerAllUserEventsProcessed() +{ + assert( m_pUserEvent ); + g_source_destroy( m_pUserEvent ); + g_source_unref( m_pUserEvent ); + m_pUserEvent = nullptr; +} + +void GtkSalDisplay::TriggerUserEventProcessing() +{ + GetGtkSalData()->TriggerUserEventProcessing(); +} + +void GtkSalDisplay::TriggerAllUserEventsProcessed() +{ + GetGtkSalData()->TriggerAllUserEventsProcessed(); +} + +GtkWidget* GtkSalDisplay::findGtkWidgetForNativeHandle(sal_uIntPtr hWindow) const +{ + for (auto pSalFrame : m_aFrames ) + { + const SystemEnvData* pEnvData = pSalFrame->GetSystemData(); + if (pEnvData->GetWindowHandle(pSalFrame) == hWindow) + return GTK_WIDGET(pEnvData->pWidget); + } + return nullptr; +} + +void GtkSalDisplay::deregisterFrame( SalFrame* pFrame ) +{ + if( m_pCapture == pFrame ) + { + static_cast(m_pCapture)->grabPointer( false, false, false ); + m_pCapture = nullptr; + } + SalGenericDisplay::deregisterFrame( pFrame ); +} + +namespace { + +struct ButtonOrder +{ + std::u16string_view m_aType; + int m_nPriority; +}; + +} + +int getButtonPriority(std::u16string_view rType) +{ + static const size_t N_TYPES = 8; + static const ButtonOrder aDiscardCancelSave[N_TYPES] = + { + { u"discard", 0 }, + { u"cancel", 1 }, + { u"close", 1 }, + { u"no", 2 }, + { u"open", 3 }, + { u"save", 3 }, + { u"yes", 3 }, + { u"ok", 3 } + }; + + static const ButtonOrder aSaveDiscardCancel[N_TYPES] = + { + { u"open", 0 }, + { u"save", 0 }, + { u"yes", 0 }, + { u"ok", 0 }, + { u"discard", 1 }, + { u"no", 1 }, + { u"cancel", 2 }, + { u"close", 2 } + }; + + const ButtonOrder* pOrder = &aDiscardCancelSave[0]; + + const OUString &rEnv = Application::GetDesktopEnvironment(); + + if (rEnv.equalsIgnoreAsciiCase("windows") || + rEnv.equalsIgnoreAsciiCase("tde") || + rEnv.startsWithIgnoreAsciiCase("kde")) + { + pOrder = &aSaveDiscardCancel[0]; + } + + for (size_t i = 0; i < N_TYPES; ++i, ++pOrder) + { + if (rType == pOrder->m_aType) + return pOrder->m_nPriority; + } + + return -1; +} + +void container_remove(GtkWidget* pContainer, GtkWidget* pChild) +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_remove(GTK_CONTAINER(pContainer), pChild); +#else + assert(GTK_IS_BOX(pContainer) || GTK_IS_GRID(pContainer) || GTK_IS_POPOVER(pContainer) || + GTK_IS_FIXED(pContainer) || GTK_IS_WINDOW(pContainer)); + if (GTK_IS_BOX(pContainer)) + gtk_box_remove(GTK_BOX(pContainer), pChild); + else if (GTK_IS_GRID(pContainer)) + gtk_grid_remove(GTK_GRID(pContainer), pChild); + else if (GTK_IS_POPOVER(pContainer)) + gtk_popover_set_child(GTK_POPOVER(pContainer), nullptr); + else if (GTK_IS_WINDOW(pContainer)) + gtk_window_set_child(GTK_WINDOW(pContainer), nullptr); + else if (GTK_IS_FIXED(pContainer)) + gtk_fixed_remove(GTK_FIXED(pContainer), pChild); +#endif +} + +void container_add(GtkWidget* pContainer, GtkWidget* pChild) +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_add(GTK_CONTAINER(pContainer), pChild); +#else + assert(GTK_IS_BOX(pContainer) || GTK_IS_GRID(pContainer) || GTK_IS_POPOVER(pContainer) || + GTK_IS_FIXED(pContainer) || GTK_IS_WINDOW(pContainer)); + if (GTK_IS_BOX(pContainer)) + gtk_box_append(GTK_BOX(pContainer), pChild); + else if (GTK_IS_GRID(pContainer)) + gtk_grid_attach(GTK_GRID(pContainer), pChild, 0, 0, 1, 1); + else if (GTK_IS_POPOVER(pContainer)) + gtk_popover_set_child(GTK_POPOVER(pContainer), pChild); + else if (GTK_IS_WINDOW(pContainer)) + gtk_window_set_child(GTK_WINDOW(pContainer), pChild); + else if (GTK_IS_FIXED(pContainer)) + gtk_fixed_put(GTK_FIXED(pContainer), pChild, 0, 0); +#endif +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtkframe.cxx b/vcl/unx/gtk3/gtkframe.cxx new file mode 100644 index 0000000000..f996b4359b --- /dev/null +++ b/vcl/unx/gtk3/gtkframe.cxx @@ -0,0 +1,6416 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include + +#include +#include + +#include + +#include + +#if OSL_DEBUG_LEVEL > 1 +# include +#endif + +#include + +#include +#include + +#include +#include +#include +#include + +#if !GTK_CHECK_VERSION(4, 0, 0) +# define GDK_ALT_MASK GDK_MOD1_MASK +# define GDK_TOPLEVEL_STATE_MAXIMIZED GDK_WINDOW_STATE_MAXIMIZED +# define GDK_TOPLEVEL_STATE_MINIMIZED GDK_WINDOW_STATE_ICONIFIED +# define gdk_wayland_surface_get_wl_surface gdk_wayland_window_get_wl_surface +# define gdk_x11_surface_get_xid gdk_x11_window_get_xid +#endif + +using namespace com::sun::star; + +int GtkSalFrame::m_nFloats = 0; + +static GDBusConnection* pSessionBus = nullptr; + +static void EnsureSessionBus() +{ + if (!pSessionBus) + pSessionBus = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr); +} + +sal_uInt16 GtkSalFrame::GetKeyModCode( guint state ) +{ + sal_uInt16 nCode = 0; + if( state & GDK_SHIFT_MASK ) + nCode |= KEY_SHIFT; + if( state & GDK_CONTROL_MASK ) + nCode |= KEY_MOD1; + if (state & GDK_ALT_MASK) + nCode |= KEY_MOD2; + if( state & GDK_SUPER_MASK ) + nCode |= KEY_MOD3; + return nCode; +} + +sal_uInt16 GtkSalFrame::GetMouseModCode( guint state ) +{ + sal_uInt16 nCode = GetKeyModCode( state ); + if( state & GDK_BUTTON1_MASK ) + nCode |= MOUSE_LEFT; + if( state & GDK_BUTTON2_MASK ) + nCode |= MOUSE_MIDDLE; + if( state & GDK_BUTTON3_MASK ) + nCode |= MOUSE_RIGHT; + + return nCode; +} + +// KEY_F26 is the last function key known to keycodes.hxx +static bool IsFunctionKeyVal(guint keyval) +{ + return keyval >= GDK_KEY_F1 && keyval <= GDK_KEY_F26; +} + +sal_uInt16 GtkSalFrame::GetKeyCode(guint keyval) +{ + sal_uInt16 nCode = 0; + if( keyval >= GDK_KEY_0 && keyval <= GDK_KEY_9 ) + nCode = KEY_0 + (keyval-GDK_KEY_0); + else if( keyval >= GDK_KEY_KP_0 && keyval <= GDK_KEY_KP_9 ) + nCode = KEY_0 + (keyval-GDK_KEY_KP_0); + else if( keyval >= GDK_KEY_A && keyval <= GDK_KEY_Z ) + nCode = KEY_A + (keyval-GDK_KEY_A ); + else if( keyval >= GDK_KEY_a && keyval <= GDK_KEY_z ) + nCode = KEY_A + (keyval-GDK_KEY_a ); + else if (IsFunctionKeyVal(keyval)) + { + switch( keyval ) + { + // - - - - - Sun keyboard, see vcl/unx/source/app/saldisp.cxx + // although GDK_KEY_F1 ... GDK_KEY_L10 are known to + // gdk/gdkkeysyms.h, they are unlikely to be generated + // except possibly by Solaris systems + // this whole section needs review + case GDK_KEY_L2: + nCode = KEY_F12; + break; + case GDK_KEY_L3: nCode = KEY_PROPERTIES; break; + case GDK_KEY_L4: nCode = KEY_UNDO; break; + case GDK_KEY_L6: nCode = KEY_COPY; break; // KEY_F16 + case GDK_KEY_L8: nCode = KEY_PASTE; break; // KEY_F18 + case GDK_KEY_L10: nCode = KEY_CUT; break; // KEY_F20 + default: + nCode = KEY_F1 + (keyval-GDK_KEY_F1); break; + } + } + else + { + switch( keyval ) + { + case GDK_KEY_KP_Down: + case GDK_KEY_Down: nCode = KEY_DOWN; break; + case GDK_KEY_KP_Up: + case GDK_KEY_Up: nCode = KEY_UP; break; + case GDK_KEY_KP_Left: + case GDK_KEY_Left: nCode = KEY_LEFT; break; + case GDK_KEY_KP_Right: + case GDK_KEY_Right: nCode = KEY_RIGHT; break; + case GDK_KEY_KP_Begin: + case GDK_KEY_KP_Home: + case GDK_KEY_Begin: + case GDK_KEY_Home: nCode = KEY_HOME; break; + case GDK_KEY_KP_End: + case GDK_KEY_End: nCode = KEY_END; break; + case GDK_KEY_KP_Page_Up: + case GDK_KEY_Page_Up: nCode = KEY_PAGEUP; break; + case GDK_KEY_KP_Page_Down: + case GDK_KEY_Page_Down: nCode = KEY_PAGEDOWN; break; + case GDK_KEY_KP_Enter: + case GDK_KEY_Return: nCode = KEY_RETURN; break; + case GDK_KEY_Escape: nCode = KEY_ESCAPE; break; + case GDK_KEY_ISO_Left_Tab: + case GDK_KEY_KP_Tab: + case GDK_KEY_Tab: nCode = KEY_TAB; break; + case GDK_KEY_BackSpace: nCode = KEY_BACKSPACE; break; + case GDK_KEY_KP_Space: + case GDK_KEY_space: nCode = KEY_SPACE; break; + case GDK_KEY_KP_Insert: + case GDK_KEY_Insert: nCode = KEY_INSERT; break; + case GDK_KEY_KP_Delete: + case GDK_KEY_Delete: nCode = KEY_DELETE; break; + case GDK_KEY_plus: + case GDK_KEY_KP_Add: nCode = KEY_ADD; break; + case GDK_KEY_minus: + case GDK_KEY_KP_Subtract: nCode = KEY_SUBTRACT; break; + case GDK_KEY_asterisk: + case GDK_KEY_KP_Multiply: nCode = KEY_MULTIPLY; break; + case GDK_KEY_slash: + case GDK_KEY_KP_Divide: nCode = KEY_DIVIDE; break; + case GDK_KEY_period: nCode = KEY_POINT; break; + case GDK_KEY_decimalpoint: nCode = KEY_POINT; break; + case GDK_KEY_comma: nCode = KEY_COMMA; break; + case GDK_KEY_less: nCode = KEY_LESS; break; + case GDK_KEY_greater: nCode = KEY_GREATER; break; + case GDK_KEY_KP_Equal: + case GDK_KEY_equal: nCode = KEY_EQUAL; break; + case GDK_KEY_Find: nCode = KEY_FIND; break; + case GDK_KEY_Menu: nCode = KEY_CONTEXTMENU;break; + case GDK_KEY_Help: nCode = KEY_HELP; break; + case GDK_KEY_Undo: nCode = KEY_UNDO; break; + case GDK_KEY_Redo: nCode = KEY_REPEAT; break; + // on a sun keyboard this actually is usually SunXK_Stop = 0x0000FF69 (XK_Cancel), + // but VCL doesn't have a key definition for that + case GDK_KEY_Cancel: nCode = KEY_F11; break; + case GDK_KEY_KP_Decimal: + case GDK_KEY_KP_Separator: nCode = KEY_DECIMAL; break; + case GDK_KEY_asciitilde: nCode = KEY_TILDE; break; + case GDK_KEY_leftsinglequotemark: + case GDK_KEY_quoteleft: nCode = KEY_QUOTELEFT; break; + case GDK_KEY_bracketleft: nCode = KEY_BRACKETLEFT; break; + case GDK_KEY_bracketright: nCode = KEY_BRACKETRIGHT; break; + case GDK_KEY_semicolon: nCode = KEY_SEMICOLON; break; + case GDK_KEY_quoteright: nCode = KEY_QUOTERIGHT; break; + case GDK_KEY_braceright: nCode = KEY_RIGHTCURLYBRACKET; break; + case GDK_KEY_numbersign: nCode = KEY_NUMBERSIGN; break; + case GDK_KEY_Forward: nCode = KEY_XF86FORWARD; break; + case GDK_KEY_Back: nCode = KEY_XF86BACK; break; + case GDK_KEY_colon: nCode = KEY_COLON; break; + // some special cases, also see saldisp.cxx + // - - - - - - - - - - - - - Apollo - - - - - - - - - - - - - 0x1000 + // These can be found in ap_keysym.h + case 0x1000FF02: // apXK_Copy + nCode = KEY_COPY; + break; + case 0x1000FF03: // apXK_Cut + nCode = KEY_CUT; + break; + case 0x1000FF04: // apXK_Paste + nCode = KEY_PASTE; + break; + case 0x1000FF14: // apXK_Repeat + nCode = KEY_REPEAT; + break; + // Exit, Save + // - - - - - - - - - - - - - - D E C - - - - - - - - - - - - - 0x1000 + // These can be found in DECkeysym.h + case 0x1000FF00: + nCode = KEY_DELETE; + break; + // - - - - - - - - - - - - - - H P - - - - - - - - - - - - - 0x1000 + // These can be found in HPkeysym.h + case 0x1000FF73: // hpXK_DeleteChar + nCode = KEY_DELETE; + break; + case 0x1000FF74: // hpXK_BackTab + case 0x1000FF75: // hpXK_KP_BackTab + nCode = KEY_TAB; + break; + // - - - - - - - - - - - - - - I B M - - - - - - - - - - - - - + // - - - - - - - - - - - - - - O S F - - - - - - - - - - - - - 0x1004 + // These also can be found in HPkeysym.h + case 0x1004FF02: // osfXK_Copy + nCode = KEY_COPY; + break; + case 0x1004FF03: // osfXK_Cut + nCode = KEY_CUT; + break; + case 0x1004FF04: // osfXK_Paste + nCode = KEY_PASTE; + break; + case 0x1004FF07: // osfXK_BackTab + nCode = KEY_TAB; + break; + case 0x1004FF08: // osfXK_BackSpace + nCode = KEY_BACKSPACE; + break; + case 0x1004FF1B: // osfXK_Escape + nCode = KEY_ESCAPE; + break; + // Up, Down, Left, Right, PageUp, PageDown + // - - - - - - - - - - - - - - S C O - - - - - - - - - - - - - + // - - - - - - - - - - - - - - S G I - - - - - - - - - - - - - 0x1007 + // - - - - - - - - - - - - - - S N I - - - - - - - - - - - - - + // - - - - - - - - - - - - - - S U N - - - - - - - - - - - - - 0x1005 + // These can be found in Sunkeysym.h + case 0x1005FF10: // SunXK_F36 + nCode = KEY_F11; + break; + case 0x1005FF11: // SunXK_F37 + nCode = KEY_F12; + break; + case 0x1005FF70: // SunXK_Props + nCode = KEY_PROPERTIES; + break; + case 0x1005FF71: // SunXK_Front + nCode = KEY_FRONT; + break; + case 0x1005FF72: // SunXK_Copy + nCode = KEY_COPY; + break; + case 0x1005FF73: // SunXK_Open + nCode = KEY_OPEN; + break; + case 0x1005FF74: // SunXK_Paste + nCode = KEY_PASTE; + break; + case 0x1005FF75: // SunXK_Cut + nCode = KEY_CUT; + break; + // - - - - - - - - - - - - - X F 8 6 - - - - - - - - - - - - - 0x1008 + // These can be found in XF86keysym.h + // but more importantly they are also available GTK/Gdk version 3 + // and hence are already provided in gdk/gdkkeysyms.h, and hence + // in gdk/gdk.h + case GDK_KEY_Copy: nCode = KEY_COPY; break; // 0x1008ff57 + case GDK_KEY_Cut: nCode = KEY_CUT; break; // 0x1008ff58 + case GDK_KEY_Open: nCode = KEY_OPEN; break; // 0x1008ff6b + case GDK_KEY_Paste: nCode = KEY_PASTE; break; // 0x1008ff6d + } + } + + return nCode; +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +guint GtkSalFrame::GetKeyValFor(GdkKeymap* pKeyMap, guint16 hardware_keycode, guint8 group) +{ + guint updated_keyval = 0; + gdk_keymap_translate_keyboard_state(pKeyMap, hardware_keycode, + GdkModifierType(0), group, &updated_keyval, nullptr, nullptr, nullptr); + return updated_keyval; +} +#endif + +namespace { + +// F10 means either KEY_F10 or KEY_MENU, which has to be decided +// in the independent part. +struct KeyAlternate +{ + sal_uInt16 nKeyCode; + sal_Unicode nCharCode; + KeyAlternate() : nKeyCode( 0 ), nCharCode( 0 ) {} + KeyAlternate( sal_uInt16 nKey, sal_Unicode nChar = 0 ) : nKeyCode( nKey ), nCharCode( nChar ) {} +}; + +} + +static KeyAlternate +GetAlternateKeyCode( const sal_uInt16 nKeyCode ) +{ + KeyAlternate aAlternate; + + switch( nKeyCode ) + { + case KEY_F10: aAlternate = KeyAlternate( KEY_MENU );break; + case KEY_F24: aAlternate = KeyAlternate( KEY_SUBTRACT, '-' );break; + } + + return aAlternate; +} + +#if OSL_DEBUG_LEVEL > 0 +static bool dumpframes = false; +#endif + +bool GtkSalFrame::doKeyCallback( guint state, + guint keyval, + guint16 hardware_keycode, + guint8 group, + sal_Unicode aOrigCode, + bool bDown, + bool bSendRelease + ) +{ + SalKeyEvent aEvent; + + aEvent.mnCharCode = aOrigCode; + aEvent.mnRepeat = 0; + + vcl::DeletionListener aDel( this ); + +#if OSL_DEBUG_LEVEL > 0 + const char* pKeyDebug = getenv("VCL_GTK3_PAINTDEBUG"); + + if (pKeyDebug && *pKeyDebug == '1') + { + if (bDown) + { + // shift-zero forces a re-draw and event is swallowed + if (keyval == GDK_KEY_0) + { + SAL_INFO("vcl.gtk3", "force widget_queue_draw."); + gtk_widget_queue_draw(GTK_WIDGET(m_pDrawingArea)); + return false; + } + else if (keyval == GDK_KEY_1) + { + SAL_INFO("vcl.gtk3", "force repaint all."); + TriggerPaintEvent(); + return false; + } + else if (keyval == GDK_KEY_2) + { + dumpframes = !dumpframes; + SAL_INFO("vcl.gtk3", "toggle dump frames to " << dumpframes); + return false; + } + } + } +#endif + + /* + * #i42122# translate all keys with Ctrl and/or Alt to group 0 else + * shortcuts (e.g. Ctrl-o) will not work but be inserted by the + * application + * + * #i52338# do this for all keys that the independent part has no key code + * for + * + * fdo#41169 rather than use group 0, detect if there is a group which can + * be used to input Latin text and use that if possible + */ + aEvent.mnCode = GetKeyCode( keyval ); +#if !GTK_CHECK_VERSION(4, 0, 0) + if( aEvent.mnCode == 0 ) + { + gint best_group = SAL_MAX_INT32; + + // Try and find Latin layout + GdkKeymap* keymap = gdk_keymap_get_default(); + GdkKeymapKey *keys; + gint n_keys; + if (gdk_keymap_get_entries_for_keyval(keymap, GDK_KEY_A, &keys, &n_keys)) + { + // Find the lowest group that supports Latin layout + for (gint i = 0; i < n_keys; ++i) + { + if (keys[i].level != 0 && keys[i].level != 1) + continue; + best_group = std::min(best_group, keys[i].group); + if (best_group == 0) + break; + } + g_free(keys); + } + + //Unavailable, go with original group then I suppose + if (best_group == SAL_MAX_INT32) + best_group = group; + + guint updated_keyval = GetKeyValFor(keymap, hardware_keycode, best_group); + aEvent.mnCode = GetKeyCode(updated_keyval); + } +#else + (void)hardware_keycode; + (void)group; +#endif + + aEvent.mnCode |= GetKeyModCode( state ); + + bool bStopProcessingKey; + if (bDown) + { + // tdf#152404 Commit uncommitted text before dispatching key shortcuts. In + // certain cases such as pressing Control-Alt-C in a Writer document while + // there is uncommitted text will call GtkSalFrame::EndExtTextInput() which + // will dispatch a SalEvent::EndExtTextInput event. Writer's handler for that + // event will delete the uncommitted text and then insert the committed text + // but LibreOffice will crash when deleting the uncommitted text because + // deletion of the text also removes and deletes the newly inserted comment. + if (m_pIMHandler && !m_pIMHandler->m_aInputEvent.maText.isEmpty() && (aEvent.mnCode & (KEY_MOD1 | KEY_MOD2))) + m_pIMHandler->doCallEndExtTextInput(); + + bStopProcessingKey = CallCallbackExc(SalEvent::KeyInput, &aEvent); + // #i46889# copy AlternateKeyCode handling from generic plugin + if (!bStopProcessingKey) + { + KeyAlternate aAlternate = GetAlternateKeyCode( aEvent.mnCode ); + if( aAlternate.nKeyCode ) + { + aEvent.mnCode = aAlternate.nKeyCode; + if( aAlternate.nCharCode ) + aEvent.mnCharCode = aAlternate.nCharCode; + bStopProcessingKey = CallCallbackExc(SalEvent::KeyInput, &aEvent); + } + } + if( bSendRelease && ! aDel.isDeleted() ) + { + CallCallbackExc(SalEvent::KeyUp, &aEvent); + } + } + else + bStopProcessingKey = CallCallbackExc(SalEvent::KeyUp, &aEvent); + return bStopProcessingKey; +} + +GtkSalFrame::GtkSalFrame( SalFrame* pParent, SalFrameStyleFlags nStyle ) + : m_nXScreen( getDisplay()->GetDefaultXScreen() ) + , m_pHeaderBar(nullptr) + , m_bGraphics(false) + , m_nSetFocusSignalId(0) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_aSmoothScrollIdle("GtkSalFrame m_aSmoothScrollIdle") +#endif +{ + getDisplay()->registerFrame( this ); + m_bDefaultPos = true; + m_bDefaultSize = ( (nStyle & SalFrameStyleFlags::SIZEABLE) && ! pParent ); + Init( pParent, nStyle ); +} + +GtkSalFrame::GtkSalFrame( SystemParentData* pSysData ) + : m_nXScreen( getDisplay()->GetDefaultXScreen() ) + , m_pHeaderBar(nullptr) + , m_bGraphics(false) + , m_nSetFocusSignalId(0) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_aSmoothScrollIdle("GtkSalFrame m_aSmoothScrollIdle") +#endif +{ + getDisplay()->registerFrame( this ); + // permanently ignore errors from our unruly children ... + GetGenericUnixSalData()->ErrorTrapPush(); + m_bDefaultPos = true; + m_bDefaultSize = true; + Init( pSysData ); +} + +// AppMenu watch functions. + +static void ObjectDestroyedNotify( gpointer data ) +{ + if ( data ) { + g_object_unref( data ); + } +} + +#if !GTK_CHECK_VERSION(4,0,0) +static void hud_activated( gboolean hud_active, gpointer user_data ) +{ + if ( hud_active ) + { + SolarMutexGuard aGuard; + GtkSalFrame* pSalFrame = static_cast< GtkSalFrame* >( user_data ); + GtkSalMenu* pSalMenu = reinterpret_cast< GtkSalMenu* >( pSalFrame->GetMenu() ); + + if ( pSalMenu ) + pSalMenu->UpdateFull(); + } +} +#endif + +static void attach_menu_model(GtkSalFrame* pSalFrame) +{ + GtkWidget* pWidget = pSalFrame->getWindow(); + GdkSurface* gdkWindow = widget_get_surface(pWidget); + + if ( gdkWindow == nullptr || g_object_get_data( G_OBJECT( gdkWindow ), "g-lo-menubar" ) != nullptr ) + return; + + // Create menu model and action group attached to this frame. + GMenuModel* pMenuModel = G_MENU_MODEL( g_lo_menu_new() ); + GActionGroup* pActionGroup = reinterpret_cast(g_lo_action_group_new()); + + // Set window properties. + g_object_set_data_full( G_OBJECT( gdkWindow ), "g-lo-menubar", pMenuModel, ObjectDestroyedNotify ); + g_object_set_data_full( G_OBJECT( gdkWindow ), "g-lo-action-group", pActionGroup, ObjectDestroyedNotify ); + +#if !GTK_CHECK_VERSION(4,0,0) + // Get a DBus session connection. + EnsureSessionBus(); + if (!pSessionBus) + return; + + // Generate menu paths. + sal_uIntPtr windowId = GtkSalFrame::GetNativeWindowHandle(pWidget); + gchar* aDBusWindowPath = g_strdup_printf( "/org/libreoffice/window/%lu", windowId ); + gchar* aDBusMenubarPath = g_strdup_printf( "/org/libreoffice/window/%lu/menus/menubar", windowId ); + + GdkDisplay *pDisplay = GtkSalFrame::getGdkDisplay(); +#if defined(GDK_WINDOWING_X11) + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + { + gdk_x11_window_set_utf8_property( gdkWindow, "_GTK_APPLICATION_ID", "org.libreoffice" ); + gdk_x11_window_set_utf8_property( gdkWindow, "_GTK_MENUBAR_OBJECT_PATH", aDBusMenubarPath ); + gdk_x11_window_set_utf8_property( gdkWindow, "_GTK_WINDOW_OBJECT_PATH", aDBusWindowPath ); + gdk_x11_window_set_utf8_property( gdkWindow, "_GTK_APPLICATION_OBJECT_PATH", "/org/libreoffice" ); + gdk_x11_window_set_utf8_property( gdkWindow, "_GTK_UNIQUE_BUS_NAME", g_dbus_connection_get_unique_name( pSessionBus ) ); + } +#endif +#if defined(GDK_WINDOWING_WAYLAND) + if (DLSYM_GDK_IS_WAYLAND_DISPLAY(pDisplay)) + { + gdk_wayland_window_set_dbus_properties_libgtk_only(gdkWindow, "org.libreoffice", + nullptr, + aDBusMenubarPath, + aDBusWindowPath, + "/org/libreoffice", + g_dbus_connection_get_unique_name( pSessionBus )); + } +#endif + // Publish the menu model and the action group. + SAL_INFO("vcl.unity", "exporting menu model at " << pMenuModel << " for window " << windowId); + pSalFrame->m_nMenuExportId = g_dbus_connection_export_menu_model (pSessionBus, aDBusMenubarPath, pMenuModel, nullptr); + SAL_INFO("vcl.unity", "exporting action group at " << pActionGroup << " for window " << windowId); + pSalFrame->m_nActionGroupExportId = g_dbus_connection_export_action_group( pSessionBus, aDBusWindowPath, pActionGroup, nullptr); + pSalFrame->m_nHudAwarenessId = hud_awareness_register( pSessionBus, aDBusMenubarPath, hud_activated, pSalFrame, nullptr, nullptr ); + + g_free( aDBusWindowPath ); + g_free( aDBusMenubarPath ); +#endif +} + +void on_registrar_available( GDBusConnection * /*connection*/, + const gchar * /*name*/, + const gchar * /*name_owner*/, + gpointer user_data ) +{ + SolarMutexGuard aGuard; + + GtkSalFrame* pSalFrame = static_cast< GtkSalFrame* >( user_data ); + + SalMenu* pSalMenu = pSalFrame->GetMenu(); + + if ( pSalMenu != nullptr ) + { + GtkSalMenu* pGtkSalMenu = static_cast(pSalMenu); + pGtkSalMenu->EnableUnity(true); + } +} + +// This is called when the registrar becomes unavailable. It shows the menubar. +void on_registrar_unavailable( GDBusConnection * /*connection*/, + const gchar * /*name*/, + gpointer user_data ) +{ + SolarMutexGuard aGuard; + + SAL_INFO("vcl.unity", "on_registrar_unavailable"); + + GtkSalFrame* pSalFrame = static_cast< GtkSalFrame* >( user_data ); + + SalMenu* pSalMenu = pSalFrame->GetMenu(); + + if ( pSalMenu ) { + GtkSalMenu* pGtkSalMenu = static_cast< GtkSalMenu* >( pSalMenu ); + pGtkSalMenu->EnableUnity(false); + } +} + +void GtkSalFrame::EnsureAppMenuWatch() +{ + if ( m_nWatcherId ) + return; + + // Get a DBus session connection. + EnsureSessionBus(); + if (!pSessionBus) + return; + + // Publish the menu only if AppMenu registrar is available. + m_nWatcherId = g_bus_watch_name_on_connection( pSessionBus, + "com.canonical.AppMenu.Registrar", + G_BUS_NAME_WATCHER_FLAGS_NONE, + on_registrar_available, + on_registrar_unavailable, + this, + nullptr ); +} + +void GtkSalFrame::InvalidateGraphics() +{ + if( m_pGraphics ) + { + m_bGraphics = false; + } +} + +GtkSalFrame::~GtkSalFrame() +{ +#if !GTK_CHECK_VERSION(4,0,0) + m_aSmoothScrollIdle.Stop(); + m_aSmoothScrollIdle.ClearInvokeHandler(); +#endif + + if (m_pDropTarget) + { + m_pDropTarget->deinitialize(); + m_pDropTarget = nullptr; + } + + if (m_pDragSource) + { + m_pDragSource->deinitialize(); + m_pDragSource= nullptr; + } + + InvalidateGraphics(); + + if (m_pParent) + { + m_pParent->m_aChildren.remove( this ); + } + + getDisplay()->deregisterFrame( this ); + + if( m_pRegion ) + { + cairo_region_destroy( m_pRegion ); + } + + m_pIMHandler.reset(); + + //tdf#108705 remove grabs on event widget before + //destroying event widget + while (m_nGrabLevel) + removeGrabLevel(); + + { + SolarMutexGuard aGuard; + + if (m_nWatcherId) + g_bus_unwatch_name(m_nWatcherId); + + if (m_nPortalSettingChangedSignalId) + g_signal_handler_disconnect(m_pSettingsPortal, m_nPortalSettingChangedSignalId); + + if (m_pSettingsPortal) + g_object_unref(m_pSettingsPortal); + + if (m_nSessionClientSignalId) + g_signal_handler_disconnect(m_pSessionClient, m_nSessionClientSignalId); + + if (m_pSessionClient) + g_object_unref(m_pSessionClient); + + if (m_pSessionManager) + g_object_unref(m_pSessionManager); + } + + GtkWidget *pEventWidget = getMouseEventWidget(); + for (auto handler_id : m_aMouseSignalIds) + g_signal_handler_disconnect(G_OBJECT(pEventWidget), handler_id); + +#if !GTK_CHECK_VERSION(4, 0, 0) + if( m_pFixedContainer ) + gtk_widget_destroy( GTK_WIDGET( m_pFixedContainer ) ); + if( m_pEventBox ) + gtk_widget_destroy( GTK_WIDGET(m_pEventBox) ); + if( m_pTopLevelGrid ) + gtk_widget_destroy( GTK_WIDGET(m_pTopLevelGrid) ); +#else + g_signal_handler_disconnect(G_OBJECT(gtk_widget_get_display(pEventWidget)), m_nSettingChangedSignalId); +#endif + { + SolarMutexGuard aGuard; + + if( m_pWindow ) + { + g_object_set_data( G_OBJECT( m_pWindow ), "SalFrame", nullptr ); + + if ( pSessionBus ) + { + if ( m_nHudAwarenessId ) + hud_awareness_unregister( pSessionBus, m_nHudAwarenessId ); + if ( m_nMenuExportId ) + g_dbus_connection_unexport_menu_model( pSessionBus, m_nMenuExportId ); + if ( m_nActionGroupExportId ) + g_dbus_connection_unexport_action_group( pSessionBus, m_nActionGroupExportId ); + } + m_xFrameWeld.reset(); +#if !GTK_CHECK_VERSION(4,0,0) + gtk_widget_destroy( m_pWindow ); +#else + if (GTK_IS_WINDOW(m_pWindow)) + gtk_window_destroy(GTK_WINDOW(m_pWindow)); + else + g_clear_pointer(&m_pWindow, gtk_widget_unparent); +#endif + } + } + +#if !GTK_CHECK_VERSION(4,0,0) + if( m_pForeignParent ) + g_object_unref( G_OBJECT( m_pForeignParent ) ); + if( m_pForeignTopLevel ) + g_object_unref( G_OBJECT( m_pForeignTopLevel) ); +#endif + + m_pGraphics.reset(); + + if (m_pSurface) + cairo_surface_destroy(m_pSurface); +} + +void GtkSalFrame::moveWindow( tools::Long nX, tools::Long nY ) +{ + if( isChild( false ) ) + { + GtkWidget* pParent = m_pParent ? gtk_widget_get_parent(m_pWindow) : nullptr; + // tdf#130414 it's possible that we were reparented and are no longer inside + // our original GtkFixed parent + if (pParent && GTK_IS_FIXED(pParent)) + { + gtk_fixed_move( GTK_FIXED(pParent), + m_pWindow, + nX - m_pParent->maGeometry.x(), nY - m_pParent->maGeometry.y() ); + } + return; + } +#if GTK_CHECK_VERSION(4,0,0) + if (GTK_IS_POPOVER(m_pWindow)) + { + GdkRectangle aRect; + aRect.x = nX; + aRect.y = nY; + aRect.width = 1; + aRect.height = 1; + gtk_popover_set_pointing_to(GTK_POPOVER(m_pWindow), &aRect); + return; + } +#else + gtk_window_move( GTK_WINDOW(m_pWindow), nX, nY ); +#endif +} + +void GtkSalFrame::widget_set_size_request(tools::Long nWidth, tools::Long nHeight) +{ + gtk_widget_set_size_request(GTK_WIDGET(m_pFixedContainer), nWidth, nHeight ); +#if GTK_CHECK_VERSION(4,0,0) + gtk_widget_set_size_request(GTK_WIDGET(m_pDrawingArea), nWidth, nHeight ); +#endif +} + +void GtkSalFrame::window_resize(tools::Long nWidth, tools::Long nHeight) +{ + m_nWidthRequest = nWidth; + m_nHeightRequest = nHeight; + if (!GTK_IS_WINDOW(m_pWindow)) + { +#if GTK_CHECK_VERSION(4,0,0) + gtk_widget_set_size_request(GTK_WIDGET(m_pDrawingArea), nWidth, nHeight); +#endif + return; + } + gtk_window_set_default_size(GTK_WINDOW(m_pWindow), nWidth, nHeight); +#if !GTK_CHECK_VERSION(4,0,0) + gtk_window_resize(GTK_WINDOW(m_pWindow), nWidth, nHeight); +#endif +} + +void GtkSalFrame::resizeWindow( tools::Long nWidth, tools::Long nHeight ) +{ + if( isChild( false ) ) + { + widget_set_size_request(nWidth, nHeight); + } + else if( ! isChild( true, false ) ) + window_resize(nWidth, nHeight); +} + +#if !GTK_CHECK_VERSION(4,0,0) +// tdf#124694 GtkFixed takes the max size of all its children as its +// preferred size, causing it to not clip its child, but grow instead. + +static void +ooo_fixed_get_preferred_height(GtkWidget*, gint *minimum, gint *natural) +{ + *minimum = 0; + *natural = 0; +} + +static void +ooo_fixed_get_preferred_width(GtkWidget*, gint *minimum, gint *natural) +{ + *minimum = 0; + *natural = 0; +} + +static void +ooo_fixed_class_init(GtkFixedClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + widget_class->get_accessible = ooo_fixed_get_accessible; + widget_class->get_preferred_height = ooo_fixed_get_preferred_height; + widget_class->get_preferred_width = ooo_fixed_get_preferred_width; +} + +/* + * Always use a sub-class of GtkFixed we can tag for a11y. This allows us to + * utilize GAIL for the toplevel window and toolkit implementation incl. + * key event listener support .. + */ + +GType +ooo_fixed_get_type() +{ + static GType type = 0; + + if (!type) { + static const GTypeInfo tinfo = + { + sizeof (GtkFixedClass), + nullptr, /* base init */ + nullptr, /* base finalize */ + reinterpret_cast(ooo_fixed_class_init), /* class init */ + nullptr, /* class finalize */ + nullptr, /* class data */ + sizeof (GtkFixed), /* instance size */ + 0, /* nb preallocs */ + nullptr, /* instance init */ + nullptr /* value table */ + }; + + type = g_type_register_static( GTK_TYPE_FIXED, "OOoFixed", + &tinfo, GTypeFlags(0)); + } + + return type; +} + +#endif + +void GtkSalFrame::updateScreenNumber() +{ +#if !GTK_CHECK_VERSION(4,0,0) + int nScreen = 0; + GdkScreen *pScreen = gtk_widget_get_screen( m_pWindow ); + if( pScreen ) + nScreen = getDisplay()->getSystem()->getScreenMonitorIdx( pScreen, maGeometry.x(), maGeometry.y() ); + maGeometry.setScreen(nScreen); +#endif +} + +GtkWidget *GtkSalFrame::getMouseEventWidget() const +{ +#if !GTK_CHECK_VERSION(4,0,0) + return GTK_WIDGET(m_pEventBox); +#else + return GTK_WIDGET(m_pFixedContainer); +#endif +} + +static void damaged(void *handle, + sal_Int32 nExtentsX, sal_Int32 nExtentsY, + sal_Int32 nExtentsWidth, sal_Int32 nExtentsHeight) +{ + GtkSalFrame* pThis = static_cast(handle); + pThis->damaged(nExtentsX, nExtentsY, nExtentsWidth, nExtentsHeight); +} + +void GtkSalFrame::InitCommon() +{ + m_pSurface = nullptr; + m_nGrabLevel = 0; + m_bSalObjectSetPosSize = false; + m_nPortalSettingChangedSignalId = 0; + m_nSessionClientSignalId = 0; + m_pSettingsPortal = nullptr; + m_pSessionManager = nullptr; + m_pSessionClient = nullptr; + + m_aDamageHandler.handle = this; + m_aDamageHandler.damaged = ::damaged; + +#if !GTK_CHECK_VERSION(4,0,0) + m_aSmoothScrollIdle.SetInvokeHandler(LINK(this, GtkSalFrame, AsyncScroll)); +#endif + + m_pTopLevelGrid = GTK_GRID(gtk_grid_new()); + container_add(m_pWindow, GTK_WIDGET(m_pTopLevelGrid)); + +#if !GTK_CHECK_VERSION(4,0,0) + m_pEventBox = GTK_EVENT_BOX(gtk_event_box_new()); + gtk_widget_add_events( GTK_WIDGET(m_pEventBox), + GDK_ALL_EVENTS_MASK ); + gtk_widget_set_vexpand(GTK_WIDGET(m_pEventBox), true); + gtk_widget_set_hexpand(GTK_WIDGET(m_pEventBox), true); + gtk_grid_attach(m_pTopLevelGrid, GTK_WIDGET(m_pEventBox), 0, 0, 1, 1); +#endif + + // add the fixed container child, + // fixed is needed since we have to position plugin windows +#if !GTK_CHECK_VERSION(4,0,0) + m_pFixedContainer = GTK_FIXED(g_object_new( ooo_fixed_get_type(), nullptr )); + m_pDrawingArea = m_pFixedContainer; +#else + m_pOverlay = GTK_OVERLAY(gtk_overlay_new()); +#if GTK_CHECK_VERSION(4,9,0) + m_pFixedContainer = GTK_FIXED(g_object_new( ooo_fixed_get_type(), nullptr )); +#else + m_pFixedContainer = GTK_FIXED(gtk_fixed_new()); +#endif + m_pDrawingArea = GTK_DRAWING_AREA(gtk_drawing_area_new()); +#endif + if (GTK_IS_WINDOW(m_pWindow)) + { + Size aDefWindowSize = calcDefaultSize(); + gtk_window_set_default_size(GTK_WINDOW(m_pWindow), aDefWindowSize.Width(), aDefWindowSize.Height()); + } + gtk_widget_set_can_focus(GTK_WIDGET(m_pFixedContainer), true); + gtk_widget_set_size_request(GTK_WIDGET(m_pFixedContainer), 1, 1); +#if !GTK_CHECK_VERSION(4,0,0) + gtk_container_add( GTK_CONTAINER(m_pEventBox), GTK_WIDGET(m_pFixedContainer) ); +#else + gtk_widget_set_vexpand(GTK_WIDGET(m_pOverlay), true); + gtk_widget_set_hexpand(GTK_WIDGET(m_pOverlay), true); + gtk_grid_attach(m_pTopLevelGrid, GTK_WIDGET(m_pOverlay), 0, 0, 1, 1); + gtk_overlay_set_child(m_pOverlay, GTK_WIDGET(m_pDrawingArea)); + gtk_overlay_add_overlay(m_pOverlay, GTK_WIDGET(m_pFixedContainer)); +#endif + + GtkWidget *pEventWidget = getMouseEventWidget(); +#if !GTK_CHECK_VERSION(4,0,0) + gtk_widget_set_app_paintable(GTK_WIDGET(m_pFixedContainer), true); + gtk_widget_set_redraw_on_allocate(GTK_WIDGET(m_pFixedContainer), false); +#endif + +#if GTK_CHECK_VERSION(4,0,0) + m_nSettingChangedSignalId = g_signal_connect(G_OBJECT(gtk_widget_get_display(pEventWidget)), "setting-changed", G_CALLBACK(signalStyleUpdated), this); +#else + // use pEventWidget instead of m_pWindow to avoid infinite event loop under Linux Mint Mate 18.3 + g_signal_connect(G_OBJECT(pEventWidget), "style-updated", G_CALLBACK(signalStyleUpdated), this); +#endif + gtk_widget_set_has_tooltip(pEventWidget, true); + // connect signals + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "query-tooltip", G_CALLBACK(signalTooltipQuery), this )); +#if !GTK_CHECK_VERSION(4,0,0) + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "button-press-event", G_CALLBACK(signalButton), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "button-release-event", G_CALLBACK(signalButton), this )); + + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "motion-notify-event", G_CALLBACK(signalMotion), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "leave-notify-event", G_CALLBACK(signalCrossing), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "enter-notify-event", G_CALLBACK(signalCrossing), this )); + + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "scroll-event", G_CALLBACK(signalScroll), this )); +#else + GtkGesture *pClick = gtk_gesture_click_new(); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(pClick), 0); + // use GTK_PHASE_TARGET instead of default GTK_PHASE_BUBBLE because I don't + // want click events in gtk widgets inside the overlay, e.g. the font size + // combobox GtkEntry in the toolbar, to be propagated down to this widget + gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(pClick), GTK_PHASE_TARGET); + gtk_widget_add_controller(pEventWidget, GTK_EVENT_CONTROLLER(pClick)); + g_signal_connect(pClick, "pressed", G_CALLBACK(gesturePressed), this); + g_signal_connect(pClick, "released", G_CALLBACK(gestureReleased), this); + + GtkEventController* pMotionController = gtk_event_controller_motion_new(); + g_signal_connect(pMotionController, "motion", G_CALLBACK(signalMotion), this); + g_signal_connect(pMotionController, "enter", G_CALLBACK(signalEnter), this); + g_signal_connect(pMotionController, "leave", G_CALLBACK(signalLeave), this); + gtk_widget_add_controller(pEventWidget, pMotionController); + + GtkEventController* pScrollController = gtk_event_controller_scroll_new(GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES); + g_signal_connect(pScrollController, "scroll", G_CALLBACK(signalScroll), this); + gtk_widget_add_controller(pEventWidget, pScrollController); +#endif + +#if GTK_CHECK_VERSION(4,0,0) + GtkGesture* pZoomGesture = gtk_gesture_zoom_new(); + gtk_widget_add_controller(pEventWidget, GTK_EVENT_CONTROLLER(pZoomGesture)); +#else + GtkGesture* pZoomGesture = gtk_gesture_zoom_new(GTK_WIDGET(pEventWidget)); + g_object_weak_ref(G_OBJECT(pEventWidget), reinterpret_cast(g_object_unref), pZoomGesture); +#endif + gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(pZoomGesture), + GTK_PHASE_TARGET); + // Note that the default zoom gesture signal handler needs to run first to setup correct + // scale delta. Otherwise the first "begin" event will always contain scale delta of infinity. + g_signal_connect_after(pZoomGesture, "begin", G_CALLBACK(signalZoomBegin), this); + g_signal_connect_after(pZoomGesture, "update", G_CALLBACK(signalZoomUpdate), this); + g_signal_connect_after(pZoomGesture, "end", G_CALLBACK(signalZoomEnd), this); + +#if GTK_CHECK_VERSION(4,0,0) + GtkGesture* pRotateGesture = gtk_gesture_rotate_new(); + gtk_widget_add_controller(pEventWidget, GTK_EVENT_CONTROLLER(pRotateGesture)); +#else + GtkGesture* pRotateGesture = gtk_gesture_rotate_new(GTK_WIDGET(pEventWidget)); + g_object_weak_ref(G_OBJECT(pEventWidget), reinterpret_cast(g_object_unref), pRotateGesture); +#endif + gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(pRotateGesture), + GTK_PHASE_TARGET); + g_signal_connect(pRotateGesture, "begin", G_CALLBACK(signalRotateBegin), this); + g_signal_connect(pRotateGesture, "update", G_CALLBACK(signalRotateUpdate), this); + g_signal_connect(pRotateGesture, "end", G_CALLBACK(signalRotateEnd), this); + + //Drop Target Stuff +#if GTK_CHECK_VERSION(4,0,0) + GtkDropTargetAsync* pDropTarget = gtk_drop_target_async_new(nullptr, GdkDragAction(GDK_ACTION_ALL)); + g_signal_connect(G_OBJECT(pDropTarget), "drag-enter", G_CALLBACK(signalDragMotion), this); + g_signal_connect(G_OBJECT(pDropTarget), "drag-motion", G_CALLBACK(signalDragMotion), this); + g_signal_connect(G_OBJECT(pDropTarget), "drag-leave", G_CALLBACK(signalDragLeave), this); + g_signal_connect(G_OBJECT(pDropTarget), "drop", G_CALLBACK(signalDragDrop), this); + gtk_widget_add_controller(pEventWidget, GTK_EVENT_CONTROLLER(pDropTarget)); +#else + gtk_drag_dest_set(GTK_WIDGET(pEventWidget), GtkDestDefaults(0), nullptr, 0, GdkDragAction(0)); + gtk_drag_dest_set_track_motion(GTK_WIDGET(pEventWidget), true); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "drag-motion", G_CALLBACK(signalDragMotion), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "drag-drop", G_CALLBACK(signalDragDrop), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "drag-data-received", G_CALLBACK(signalDragDropReceived), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "drag-leave", G_CALLBACK(signalDragLeave), this )); +#endif + +#if !GTK_CHECK_VERSION(4,0,0) + //Drag Source Stuff + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "drag-end", G_CALLBACK(signalDragEnd), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "drag-failed", G_CALLBACK(signalDragFailed), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "drag-data-delete", G_CALLBACK(signalDragDelete), this )); + m_aMouseSignalIds.push_back(g_signal_connect( G_OBJECT(pEventWidget), "drag-data-get", G_CALLBACK(signalDragDataGet), this )); +#endif + +#if !GTK_CHECK_VERSION(4,0,0) + g_signal_connect( G_OBJECT(m_pFixedContainer), "draw", G_CALLBACK(signalDraw), this ); + g_signal_connect( G_OBJECT(m_pFixedContainer), "size-allocate", G_CALLBACK(sizeAllocated), this ); +#else + gtk_drawing_area_set_draw_func(m_pDrawingArea, signalDraw, this, nullptr); + g_signal_connect(G_OBJECT(m_pDrawingArea), "resize", G_CALLBACK(sizeAllocated), this); +#endif + + g_signal_connect(G_OBJECT(m_pFixedContainer), "realize", G_CALLBACK(signalRealize), this); + +#if !GTK_CHECK_VERSION(4,0,0) + GtkGesture *pSwipe = gtk_gesture_swipe_new(pEventWidget); + g_object_weak_ref(G_OBJECT(pEventWidget), reinterpret_cast(g_object_unref), pSwipe); +#else + GtkGesture *pSwipe = gtk_gesture_swipe_new(); + gtk_widget_add_controller(pEventWidget, GTK_EVENT_CONTROLLER(pSwipe)); +#endif + g_signal_connect(pSwipe, "swipe", G_CALLBACK(gestureSwipe), this); + gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER (pSwipe), GTK_PHASE_TARGET); + +#if !GTK_CHECK_VERSION(4,0,0) + GtkGesture *pLongPress = gtk_gesture_long_press_new(pEventWidget); + g_object_weak_ref(G_OBJECT(pEventWidget), reinterpret_cast(g_object_unref), pLongPress); +#else + GtkGesture *pLongPress = gtk_gesture_long_press_new(); + gtk_widget_add_controller(pEventWidget, GTK_EVENT_CONTROLLER(pLongPress)); +#endif + g_signal_connect(pLongPress, "pressed", G_CALLBACK(gestureLongPress), this); + gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER (pLongPress), GTK_PHASE_TARGET); + +#if !GTK_CHECK_VERSION(4,0,0) + g_signal_connect_after( G_OBJECT(m_pWindow), "focus-in-event", G_CALLBACK(signalFocus), this ); + g_signal_connect_after( G_OBJECT(m_pWindow), "focus-out-event", G_CALLBACK(signalFocus), this ); + if (GTK_IS_WINDOW(m_pWindow)) // i.e. not if it's a GtkEventBox which doesn't have the signal + m_nSetFocusSignalId = g_signal_connect( G_OBJECT(m_pWindow), "set-focus", G_CALLBACK(signalSetFocus), this ); +#else + GtkEventController* pFocusController = gtk_event_controller_focus_new(); + g_signal_connect(pFocusController, "enter", G_CALLBACK(signalFocusEnter), this); + g_signal_connect(pFocusController, "leave", G_CALLBACK(signalFocusLeave), this); + gtk_widget_set_focusable(pEventWidget, true); + gtk_widget_add_controller(pEventWidget, pFocusController); + if (GTK_IS_WINDOW(m_pWindow)) // i.e. not if it's a GtkEventBox which doesn't have the property (presumably?) + m_nSetFocusSignalId = g_signal_connect( G_OBJECT(m_pWindow), "notify::focus-widget", G_CALLBACK(signalSetFocus), this ); +#endif +#if !GTK_CHECK_VERSION(4,0,0) + g_signal_connect( G_OBJECT(m_pWindow), "map-event", G_CALLBACK(signalMap), this ); + g_signal_connect( G_OBJECT(m_pWindow), "unmap-event", G_CALLBACK(signalUnmap), this ); + g_signal_connect( G_OBJECT(m_pWindow), "delete-event", G_CALLBACK(signalDelete), this ); +#else + g_signal_connect( G_OBJECT(m_pWindow), "map", G_CALLBACK(signalMap), this ); + g_signal_connect( G_OBJECT(m_pWindow), "unmap", G_CALLBACK(signalUnmap), this ); + if (GTK_IS_WINDOW(m_pWindow)) + g_signal_connect( G_OBJECT(m_pWindow), "close-request", G_CALLBACK(signalDelete), this ); +#endif +#if !GTK_CHECK_VERSION(4,0,0) + g_signal_connect( G_OBJECT(m_pWindow), "configure-event", G_CALLBACK(signalConfigure), this ); +#endif + +#if !GTK_CHECK_VERSION(4,0,0) + g_signal_connect( G_OBJECT(m_pWindow), "key-press-event", G_CALLBACK(signalKey), this ); + g_signal_connect( G_OBJECT(m_pWindow), "key-release-event", G_CALLBACK(signalKey), this ); +#else + m_pKeyController = GTK_EVENT_CONTROLLER_KEY(gtk_event_controller_key_new()); + g_signal_connect(m_pKeyController, "key-pressed", G_CALLBACK(signalKeyPressed), this); + g_signal_connect(m_pKeyController, "key-released", G_CALLBACK(signalKeyReleased), this); + gtk_widget_add_controller(pEventWidget, GTK_EVENT_CONTROLLER(m_pKeyController)); + +#endif + g_signal_connect( G_OBJECT(m_pWindow), "destroy", G_CALLBACK(signalDestroy), this ); + + // init members + m_nKeyModifiers = ModKeyFlags::NONE; + m_bFullscreen = false; +#if GTK_CHECK_VERSION(4,0,0) + m_nState = static_cast(0); +#else + m_nState = GDK_WINDOW_STATE_WITHDRAWN; +#endif + m_pIMHandler = nullptr; + m_pRegion = nullptr; + m_pDropTarget = nullptr; + m_pDragSource = nullptr; + m_bGeometryIsProvisional = false; + m_bIconSetWhileUnmapped = false; + m_bTooltipBlocked = false; + m_ePointerStyle = static_cast(0xffff); + m_pSalMenu = nullptr; + m_nWatcherId = 0; + m_nMenuExportId = 0; + m_nActionGroupExportId = 0; + m_nHudAwarenessId = 0; + +#if !GTK_CHECK_VERSION(4,0,0) + gtk_widget_add_events( m_pWindow, + GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | + GDK_SCROLL_MASK | GDK_TOUCHPAD_GESTURE_MASK + ); +#endif + + // show the widgets +#if !GTK_CHECK_VERSION(4,0,0) + gtk_widget_show_all(GTK_WIDGET(m_pTopLevelGrid)); +#else + gtk_widget_show(GTK_WIDGET(m_pTopLevelGrid)); +#endif + + // realize the window, we need an XWindow id + gtk_widget_realize( m_pWindow ); + + if (GTK_IS_WINDOW(m_pWindow)) + { +#if !GTK_CHECK_VERSION(4,0,0) + g_signal_connect(G_OBJECT(m_pWindow), "window-state-event", G_CALLBACK(signalWindowState), this); +#else + GdkSurface* gdkWindow = widget_get_surface(m_pWindow); + g_signal_connect(G_OBJECT(gdkWindow), "notify::state", G_CALLBACK(signalWindowState), this); +#endif + } + + //system data + m_aSystemData.SetWindowHandle(GetNativeWindowHandle(m_pWindow)); + m_aSystemData.aShellWindow = reinterpret_cast(this); + m_aSystemData.pSalFrame = this; + m_aSystemData.pWidget = m_pWindow; + m_aSystemData.nScreen = m_nXScreen.getXScreen(); + m_aSystemData.toolkit = SystemEnvData::Toolkit::Gtk; + +#if defined(GDK_WINDOWING_X11) + GdkDisplay *pDisplay = getGdkDisplay(); + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + { + m_aSystemData.pDisplay = gdk_x11_display_get_xdisplay(pDisplay); + m_aSystemData.platform = SystemEnvData::Platform::Xcb; +#if !GTK_CHECK_VERSION(4,0,0) + GdkScreen* pScreen = gtk_widget_get_screen(m_pWindow); + GdkVisual* pVisual = gdk_screen_get_system_visual(pScreen); + m_aSystemData.pVisual = gdk_x11_visual_get_xvisual(pVisual); +#endif + } +#endif +#if defined(GDK_WINDOWING_WAYLAND) + if (DLSYM_GDK_IS_WAYLAND_DISPLAY(pDisplay)) + { + m_aSystemData.pDisplay = gdk_wayland_display_get_wl_display(pDisplay); + m_aSystemData.platform = SystemEnvData::Platform::Wayland; + } +#endif + + m_bGraphics = false; + m_pGraphics = nullptr; + + m_nFloatFlags = FloatWinPopupFlags::NONE; + m_bFloatPositioned = false; + + m_nWidthRequest = 0; + m_nHeightRequest = 0; + + // fake an initial geometry, gets updated via configure event or SetPosSize + if (m_bDefaultPos || m_bDefaultSize) + { + Size aDefSize = calcDefaultSize(); + maGeometry.setPosSize({ -1, -1 }, aDefSize); + maGeometry.setDecorations(0, 0, 0, 0); + } + updateScreenNumber(); + + SetIcon(SV_ICON_ID_OFFICE); +} + +GtkSalFrame *GtkSalFrame::getFromWindow( GtkWidget *pWindow ) +{ + return static_cast(g_object_get_data( G_OBJECT( pWindow ), "SalFrame" )); +} + +void GtkSalFrame::DisallowCycleFocusOut() +{ + if (!m_nSetFocusSignalId) + return; + // don't enable/disable can-focus as control enters and leaves + // embedded native gtk widgets + g_signal_handler_disconnect(G_OBJECT(m_pWindow), m_nSetFocusSignalId); + m_nSetFocusSignalId = 0; + +#if !GTK_CHECK_VERSION(4, 0, 0) + // gtk3: set container without can-focus and focus will tab between + // the native embedded widgets using the default gtk handling for + // that + // gtk4: no need because the native widgets are the only + // thing in the overlay and the drawing widget is underneath so + // the natural focus cycle is sufficient + gtk_widget_set_can_focus(GTK_WIDGET(m_pFixedContainer), false); +#endif +} + +bool GtkSalFrame::IsCycleFocusOutDisallowed() const +{ + return m_nSetFocusSignalId == 0; +} + +void GtkSalFrame::AllowCycleFocusOut() +{ + if (m_nSetFocusSignalId) + return; +#if !GTK_CHECK_VERSION(4,0,0) + // enable/disable can-focus as control enters and leaves + // embedded native gtk widgets + m_nSetFocusSignalId = g_signal_connect(G_OBJECT(m_pWindow), "set-focus", G_CALLBACK(signalSetFocus), this); +#else + m_nSetFocusSignalId = g_signal_connect(G_OBJECT(m_pWindow), "notify::focus-widget", G_CALLBACK(signalSetFocus), this); +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + // set container without can-focus and focus will tab between + // the native embedded widgets using the default gtk handling for + // that + // gtk4: no need because the native widgets are the only + // thing in the overlay and the drawing widget is underneath so + // the natural focus cycle is sufficient + gtk_widget_set_can_focus(GTK_WIDGET(m_pFixedContainer), true); +#endif +} + +namespace +{ + enum ColorScheme + { + DEFAULT, + PREFER_DARK, + PREFER_LIGHT + }; + + void ReadColorScheme(GDBusProxy* proxy, GVariant** out) + { + g_autoptr (GVariant) ret = + g_dbus_proxy_call_sync(proxy, "Read", + g_variant_new ("(ss)", "org.freedesktop.appearance", "color-scheme"), + G_DBUS_CALL_FLAGS_NONE, G_MAXINT, nullptr, nullptr); + if (!ret) + return; + + g_autoptr (GVariant) child = nullptr; + g_variant_get(ret, "(v)", &child); + g_variant_get(child, "v", out); + + return; + } +} + +void GtkSalFrame::SetColorScheme(GVariant* variant) +{ + if (!m_pWindow) + return; + + guint32 color_scheme; + + switch (officecfg::Office::Common::Misc::Appearance::get()) + { + default: + case 0: // Auto + { + if (variant) + { + color_scheme = g_variant_get_uint32(variant); + if (color_scheme > PREFER_LIGHT) + color_scheme = DEFAULT; + } + else + color_scheme = DEFAULT; + break; + } + case 1: // Light + color_scheme = PREFER_LIGHT; + break; + case 2: // Dark + color_scheme = PREFER_DARK; + break; + } + + bool bDarkIconTheme(color_scheme == PREFER_DARK); + GtkSettings* pSettings = gtk_widget_get_settings(m_pWindow); + g_object_set(pSettings, "gtk-application-prefer-dark-theme", bDarkIconTheme, nullptr); +} + +bool GtkSalFrame::GetUseDarkMode() const +{ + if (!m_pWindow) + return false; + GtkSettings* pSettings = gtk_widget_get_settings(m_pWindow); + gboolean bDarkIconTheme = false; + g_object_get(pSettings, "gtk-application-prefer-dark-theme", &bDarkIconTheme, nullptr); + return bDarkIconTheme; +} + +bool GtkSalFrame::GetUseReducedAnimation() const +{ + if (!m_pWindow) + return false; + GtkSettings* pSettings = gtk_widget_get_settings(m_pWindow); + gboolean bAnimations; + g_object_get(pSettings, "gtk-enable-animations", &bAnimations, nullptr); + return !bAnimations; +} + +static void settings_portal_changed_cb(GDBusProxy*, const char*, const char* signal_name, + GVariant* parameters, gpointer frame) +{ + if (g_strcmp0(signal_name, "SettingChanged")) + return; + + g_autoptr (GVariant) value = nullptr; + const char *name_space; + const char *name; + g_variant_get(parameters, "(&s&sv)", &name_space, &name, &value); + + if (g_strcmp0(name_space, "org.freedesktop.appearance") || + g_strcmp0(name, "color-scheme")) + return; + + GtkSalFrame* pThis = static_cast(frame); + pThis->SetColorScheme(value); +} + +void GtkSalFrame::ListenPortalSettings() +{ + EnsureSessionBus(); + + if (!pSessionBus) + return; + + m_pSettingsPortal = g_dbus_proxy_new_sync(pSessionBus, + G_DBUS_PROXY_FLAGS_NONE, + nullptr, + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Settings", + nullptr, + nullptr); + + UpdateDarkMode(); + + if (!m_pSettingsPortal) + return; + + m_nPortalSettingChangedSignalId = g_signal_connect(m_pSettingsPortal, "g-signal", G_CALLBACK(settings_portal_changed_cb), this); +} + +static void session_client_response(GDBusProxy* client_proxy) +{ + g_dbus_proxy_call(client_proxy, + "EndSessionResponse", + g_variant_new ("(bs)", true, ""), + G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, + nullptr, nullptr, nullptr); +} + +// unset documents "modify" flag so they won't veto closing +static void clear_modify_and_terminate() +{ + css::uno::Reference xContext = ::comphelper::getProcessComponentContext(); + uno::Reference xDesktop(frame::Desktop::create(xContext)); + uno::Reference xComponents = xDesktop->getComponents()->createEnumeration(); + while (xComponents->hasMoreElements()) + { + css::uno::Reference xModifiable(xComponents->nextElement(), css::uno::UNO_QUERY); + if (xModifiable) + xModifiable->setModified(false); + } + xDesktop->terminate(); +} + +static void session_client_signal(GDBusProxy* client_proxy, const char*, const char* signal_name, + GVariant* /*parameters*/, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + + if (g_str_equal (signal_name, "QueryEndSession")) + { + css::uno::Reference xContext = ::comphelper::getProcessComponentContext(); + uno::Reference xDesktop(frame::Desktop::create(xContext)); + + bool bModified = false; + + // find the XModifiable for this GtkSalFrame + if (UnoWrapperBase* pWrapper = UnoWrapperBase::GetUnoWrapper(false)) + { + VclPtr xThisWindow = pThis->GetWindow(); + css::uno::Reference xList = xDesktop->getFrames(); + sal_Int32 nFrameCount = xList->getCount(); + for (sal_Int32 i = 0; i < nFrameCount; ++i) + { + css::uno::Reference xFrame; + xList->getByIndex(i) >>= xFrame; + if (!xFrame) + continue; + VclPtr xWin = pWrapper->GetWindow(xFrame->getContainerWindow()); + if (!xWin) + continue; + if (xWin->GetFrameWindow() != xThisWindow) + continue; + css::uno::Reference xController = xFrame->getController(); + if (!xController) + break; + css::uno::Reference xModifiable(xController->getModel(), css::uno::UNO_QUERY); + if (!xModifiable) + break; + bModified = xModifiable->isModified(); + break; + } + } + + pThis->SessionManagerInhibit(bModified, APPLICATION_INHIBIT_LOGOUT, VclResId(STR_UNSAVED_DOCUMENTS), + gtk_window_get_icon_name(GTK_WINDOW(pThis->getWindow()))); + + session_client_response(client_proxy); + } + else if (g_str_equal (signal_name, "CancelEndSession")) + { + // restore back to uninhibited (to set again if queried), so frames + // that go away before the next logout don't affect that logout + pThis->SessionManagerInhibit(false, APPLICATION_INHIBIT_LOGOUT, VclResId(STR_UNSAVED_DOCUMENTS), + gtk_window_get_icon_name(GTK_WINDOW(pThis->getWindow()))); + } + else if (g_str_equal (signal_name, "EndSession")) + { + session_client_response(client_proxy); + clear_modify_and_terminate(); + } + else if (g_str_equal (signal_name, "Stop")) + { + clear_modify_and_terminate(); + } +} + +void GtkSalFrame::ListenSessionManager() +{ + EnsureSessionBus(); + + if (!pSessionBus) + return; + + m_pSessionManager = g_dbus_proxy_new_sync(pSessionBus, + G_DBUS_PROXY_FLAGS_NONE, + nullptr, + "org.gnome.SessionManager", + "/org/gnome/SessionManager", + "org.gnome.SessionManager", + nullptr, + nullptr); + + if (!m_pSessionManager) + return; + + GVariant* res = g_dbus_proxy_call_sync(m_pSessionManager, + "RegisterClient", + g_variant_new ("(ss)", "org.libreoffice", ""), + G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, + nullptr, + nullptr); + + if (!res) + return; + + gchar* client_path; + g_variant_get(res, "(o)", &client_path); + g_variant_unref(res); + + m_pSessionClient = g_dbus_proxy_new_sync(pSessionBus, + G_DBUS_PROXY_FLAGS_NONE, + nullptr, + "org.gnome.SessionManager", + client_path, + "org.gnome.SessionManager.ClientPrivate", + nullptr, + nullptr); + + g_free(client_path); + + if (!m_pSessionClient) + return; + + m_nSessionClientSignalId = g_signal_connect(m_pSessionClient, "g-signal", G_CALLBACK(session_client_signal), this); +} + +void GtkSalFrame::UpdateDarkMode() +{ + g_autoptr (GVariant) value = nullptr; + if (m_pSettingsPortal) + ReadColorScheme(m_pSettingsPortal, &value); + SetColorScheme(value); +} + +#if GTK_CHECK_VERSION(4,0,0) +static void PopoverClosed(GtkPopover*, GtkSalFrame* pThis) +{ + SolarMutexGuard aGuard; + pThis->closePopup(); +} +#endif + +void GtkSalFrame::Init( SalFrame* pParent, SalFrameStyleFlags nStyle ) +{ + if( nStyle & SalFrameStyleFlags::DEFAULT ) // ensure default style + { + nStyle |= SalFrameStyleFlags::MOVEABLE | SalFrameStyleFlags::SIZEABLE | SalFrameStyleFlags::CLOSEABLE; + nStyle &= ~SalFrameStyleFlags::FLOAT; + } + + m_pParent = static_cast(pParent); +#if !GTK_CHECK_VERSION(4,0,0) + m_pForeignParent = nullptr; + m_aForeignParentWindow = None; + m_pForeignTopLevel = nullptr; + m_aForeignTopLevelWindow = None; +#endif + m_nStyle = nStyle; + + bool bPopup = ((nStyle & SalFrameStyleFlags::FLOAT) && + !(nStyle & SalFrameStyleFlags::OWNERDRAWDECORATION)); + + if( nStyle & SalFrameStyleFlags::SYSTEMCHILD ) + { +#if !GTK_CHECK_VERSION(4,0,0) + m_pWindow = gtk_event_box_new(); +#else + m_pWindow = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); +#endif + if( m_pParent ) + { + // insert into container + gtk_fixed_put( m_pParent->getFixedContainer(), + m_pWindow, 0, 0 ); + } + } + else + { +#if !GTK_CHECK_VERSION(4,0,0) + m_pWindow = gtk_window_new(bPopup ? GTK_WINDOW_POPUP : GTK_WINDOW_TOPLEVEL); +#else + if (!bPopup) + m_pWindow = gtk_window_new(); + else + { + m_pWindow = gtk_popover_new(); + gtk_popover_set_has_arrow(GTK_POPOVER(m_pWindow), false); + g_signal_connect(m_pWindow, "closed", G_CALLBACK(PopoverClosed), this); + } +#endif + +#if !GTK_CHECK_VERSION(4,0,0) + // hook up F1 to show help for embedded native gtk widgets + GtkAccelGroup *pGroup = gtk_accel_group_new(); + GClosure* closure = g_cclosure_new(G_CALLBACK(GtkSalFrame::NativeWidgetHelpPressed), GTK_WINDOW(m_pWindow), nullptr); + gtk_accel_group_connect(pGroup, GDK_KEY_F1, static_cast(0), GTK_ACCEL_LOCKED, closure); + gtk_window_add_accel_group(GTK_WINDOW(m_pWindow), pGroup); +#endif + } + + g_object_set_data( G_OBJECT( m_pWindow ), "SalFrame", this ); + g_object_set_data( G_OBJECT( m_pWindow ), "libo-version", const_cast(LIBO_VERSION_DOTTED)); + + // force wm class hint + if (!isChild()) + { + if (m_pParent) + m_sWMClass = m_pParent->m_sWMClass; + updateWMClass(); + } + + if (GTK_IS_WINDOW(m_pWindow)) + { + if (m_pParent) + { + GtkWidget* pTopLevel = widget_get_toplevel(m_pParent->m_pWindow); +#if !GTK_CHECK_VERSION(4,0,0) + if (!isChild()) + gtk_window_set_screen(GTK_WINDOW(m_pWindow), gtk_widget_get_screen(pTopLevel)); +#endif + + if (!(m_pParent->m_nStyle & SalFrameStyleFlags::PLUG)) + gtk_window_set_transient_for(GTK_WINDOW(m_pWindow), GTK_WINDOW(pTopLevel)); + m_pParent->m_aChildren.push_back( this ); + gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(pTopLevel)), GTK_WINDOW(m_pWindow)); + } + else + { + gtk_window_group_add_window(gtk_window_group_new(), GTK_WINDOW(m_pWindow)); + g_object_unref(gtk_window_get_group(GTK_WINDOW(m_pWindow))); + } + } + else if (GTK_IS_POPOVER(m_pWindow)) + { + assert(m_pParent); + gtk_widget_set_parent(m_pWindow, m_pParent->getMouseEventWidget()); + } + + // set window type + bool bDecoHandling = + ! isChild() && + ( ! (nStyle & SalFrameStyleFlags::FLOAT) || + (nStyle & SalFrameStyleFlags::OWNERDRAWDECORATION) ); + + if( bDecoHandling ) + { +#if !GTK_CHECK_VERSION(4,0,0) + GdkWindowTypeHint eType = GDK_WINDOW_TYPE_HINT_NORMAL; + if( (nStyle & SalFrameStyleFlags::DIALOG) && m_pParent != nullptr ) + eType = GDK_WINDOW_TYPE_HINT_DIALOG; +#endif + if( nStyle & SalFrameStyleFlags::INTRO ) + { +#if !GTK_CHECK_VERSION(4,0,0) + gtk_window_set_role( GTK_WINDOW(m_pWindow), "splashscreen" ); + eType = GDK_WINDOW_TYPE_HINT_SPLASHSCREEN; +#endif + } + else if( nStyle & SalFrameStyleFlags::TOOLWINDOW ) + { +#if !GTK_CHECK_VERSION(4,0,0) + eType = GDK_WINDOW_TYPE_HINT_DIALOG; + gtk_window_set_skip_taskbar_hint( GTK_WINDOW(m_pWindow), true ); +#endif + } + else if( nStyle & SalFrameStyleFlags::OWNERDRAWDECORATION ) + { +#if !GTK_CHECK_VERSION(4,0,0) + eType = GDK_WINDOW_TYPE_HINT_TOOLBAR; + gtk_window_set_focus_on_map(GTK_WINDOW(m_pWindow), false); +#endif + gtk_window_set_decorated(GTK_WINDOW(m_pWindow), false); + } +#if !GTK_CHECK_VERSION(4,0,0) + gtk_window_set_type_hint( GTK_WINDOW(m_pWindow), eType ); + gtk_window_set_gravity( GTK_WINDOW(m_pWindow), GDK_GRAVITY_STATIC ); +#endif + gtk_window_set_resizable( GTK_WINDOW(m_pWindow), bool(nStyle & SalFrameStyleFlags::SIZEABLE) ); + +#if !GTK_CHECK_VERSION(4,0,0) +#if defined(GDK_WINDOWING_WAYLAND) + //rhbz#1392145 under wayland/csd if we've overridden the default widget direction in order to set LibreOffice's + //UI to the configured ui language but the system ui locale is a different text direction, then the toplevel + //built-in close button of the titlebar follows the overridden direction rather than continue in the same + //direction as every other titlebar on the user's desktop. So if they don't match set an explicit + //header bar with the desired 'outside' direction + if ((eType == GDK_WINDOW_TYPE_HINT_NORMAL || eType == GDK_WINDOW_TYPE_HINT_DIALOG) && DLSYM_GDK_IS_WAYLAND_DISPLAY(GtkSalFrame::getGdkDisplay())) + { + const bool bDesktopIsRTL = MsLangId::isRightToLeft(MsLangId::getConfiguredSystemUILanguage()); + const bool bAppIsRTL = gtk_widget_get_default_direction() == GTK_TEXT_DIR_RTL; + if (bDesktopIsRTL != bAppIsRTL) + { + m_pHeaderBar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_set_direction(GTK_WIDGET(m_pHeaderBar), bDesktopIsRTL ? GTK_TEXT_DIR_RTL : GTK_TEXT_DIR_LTR); + gtk_header_bar_set_show_close_button(m_pHeaderBar, true); + gtk_window_set_titlebar(GTK_WINDOW(m_pWindow), GTK_WIDGET(m_pHeaderBar)); + gtk_widget_show(GTK_WIDGET(m_pHeaderBar)); + } + } +#endif +#endif + } +#if !GTK_CHECK_VERSION(4,0,0) + else if( nStyle & SalFrameStyleFlags::FLOAT ) + gtk_window_set_type_hint( GTK_WINDOW(m_pWindow), GDK_WINDOW_TYPE_HINT_POPUP_MENU ); +#endif + + InitCommon(); + + if (!bPopup) + { + // Enable GMenuModel native menu + attach_menu_model(this); + + // Listen to portal settings for e.g. prefer dark theme + ListenPortalSettings(); + + // Listen to session manager for e.g. query-end + ListenSessionManager(); + } +} + +#if !GTK_CHECK_VERSION(4,0,0) +GdkNativeWindow GtkSalFrame::findTopLevelSystemWindow( GdkNativeWindow ) +{ + //FIXME: no findToplevelSystemWindow + return 0; +} +#endif + +void GtkSalFrame::Init( SystemParentData* pSysData ) +{ + m_pParent = nullptr; +#if !GTK_CHECK_VERSION(4,0,0) + m_aForeignParentWindow = pSysData->aWindow; + m_pForeignParent = nullptr; + m_aForeignTopLevelWindow = findTopLevelSystemWindow(pSysData->aWindow); + m_pForeignTopLevel = gdk_x11_window_foreign_new_for_display( getGdkDisplay(), m_aForeignTopLevelWindow ); + gdk_window_set_events( m_pForeignTopLevel, GDK_STRUCTURE_MASK ); + + if( pSysData->nSize > sizeof(pSysData->nSize)+sizeof(pSysData->aWindow) && pSysData->bXEmbedSupport ) + { + m_pWindow = gtk_plug_new_for_display( getGdkDisplay(), pSysData->aWindow ); + gtk_widget_set_can_default(m_pWindow, true); + gtk_widget_set_can_focus(m_pWindow, true); + gtk_widget_set_sensitive(m_pWindow, true); + } + else + { + m_pWindow = gtk_window_new( GTK_WINDOW_POPUP ); + } +#endif + m_nStyle = SalFrameStyleFlags::PLUG; + InitCommon(); + +#if !GTK_CHECK_VERSION(4,0,0) + m_pForeignParent = gdk_x11_window_foreign_new_for_display( getGdkDisplay(), m_aForeignParentWindow ); + gdk_window_set_events( m_pForeignParent, GDK_STRUCTURE_MASK ); +#else + (void)pSysData; +#endif + + //FIXME: Handling embedded windows, is going to be fun ... +} + +void GtkSalFrame::SetExtendedFrameStyle(SalExtStyle) +{ +} + +SalGraphics* GtkSalFrame::AcquireGraphics() +{ + if( m_bGraphics ) + return nullptr; + + if( !m_pGraphics ) + { + m_pGraphics.reset( new GtkSalGraphics( this, m_pWindow ) ); + if (!m_pSurface) + { + AllocateFrame(); + TriggerPaintEvent(); + } + m_pGraphics->setSurface(m_pSurface, m_aFrameSize); + } + m_bGraphics = true; + return m_pGraphics.get(); +} + +void GtkSalFrame::ReleaseGraphics( SalGraphics* pGraphics ) +{ + (void) pGraphics; + assert( pGraphics == m_pGraphics.get() ); + m_bGraphics = false; +} + +bool GtkSalFrame::PostEvent(std::unique_ptr pData) +{ + getDisplay()->SendInternalEvent( this, pData.release() ); + return true; +} + +void GtkSalFrame::SetTitle( const OUString& rTitle ) +{ + if (m_pWindow && GTK_IS_WINDOW(m_pWindow) && !isChild()) + { + OString sTitle(OUStringToOString(rTitle, RTL_TEXTENCODING_UTF8)); + gtk_window_set_title(GTK_WINDOW(m_pWindow), sTitle.getStr()); +#if !GTK_CHECK_VERSION(4,0,0) + if (m_pHeaderBar) + gtk_header_bar_set_title(m_pHeaderBar, sTitle.getStr()); +#endif + } +} + +void GtkSalFrame::SetIcon(const char* appicon) +{ + gtk_window_set_icon_name(GTK_WINDOW(m_pWindow), appicon); + +#if defined(GDK_WINDOWING_WAYLAND) + if (!DLSYM_GDK_IS_WAYLAND_DISPLAY(getGdkDisplay())) + return; + +#if GTK_CHECK_VERSION(4,0,0) + GdkSurface* gdkWindow = gtk_native_get_surface(gtk_widget_get_native(m_pWindow)); + gdk_wayland_toplevel_set_application_id((GDK_TOPLEVEL(gdkWindow)), appicon); +#else + static auto set_application_id = reinterpret_cast( + dlsym(nullptr, "gdk_wayland_window_set_application_id")); + if (set_application_id) + { + GdkSurface* gdkWindow = widget_get_surface(m_pWindow); + set_application_id(gdkWindow, appicon); + } +#endif + // gdk_wayland_window_set_application_id doesn't seem to work before + // the window is mapped, so set this for real when/if we are mapped + m_bIconSetWhileUnmapped = !gtk_widget_get_mapped(m_pWindow); +#endif +} + +void GtkSalFrame::SetIcon( sal_uInt16 nIcon ) +{ + if( (m_nStyle & (SalFrameStyleFlags::PLUG|SalFrameStyleFlags::SYSTEMCHILD|SalFrameStyleFlags::FLOAT|SalFrameStyleFlags::INTRO|SalFrameStyleFlags::OWNERDRAWDECORATION)) + || ! m_pWindow ) + return; + + gchar* appicon; + + if (nIcon == SV_ICON_ID_TEXT) + appicon = g_strdup ("libreoffice-writer"); + else if (nIcon == SV_ICON_ID_SPREADSHEET) + appicon = g_strdup ("libreoffice-calc"); + else if (nIcon == SV_ICON_ID_DRAWING) + appicon = g_strdup ("libreoffice-draw"); + else if (nIcon == SV_ICON_ID_PRESENTATION) + appicon = g_strdup ("libreoffice-impress"); + else if (nIcon == SV_ICON_ID_DATABASE) + appicon = g_strdup ("libreoffice-base"); + else if (nIcon == SV_ICON_ID_FORMULA) + appicon = g_strdup ("libreoffice-math"); + else + appicon = g_strdup ("libreoffice-startcenter"); + + SetIcon(appicon); + + g_free (appicon); +} + +void GtkSalFrame::SetMenu( SalMenu* pSalMenu ) +{ + m_pSalMenu = static_cast(pSalMenu); +} + +SalMenu* GtkSalFrame::GetMenu() +{ + return m_pSalMenu; +} + +void GtkSalFrame::Center() +{ + if (!GTK_IS_WINDOW(m_pWindow)) + return; +#if !GTK_CHECK_VERSION(4,0,0) + if (m_pParent) + gtk_window_set_position(GTK_WINDOW(m_pWindow), GTK_WIN_POS_CENTER_ON_PARENT); + else + gtk_window_set_position(GTK_WINDOW(m_pWindow), GTK_WIN_POS_CENTER); +#endif +} + +Size GtkSalFrame::calcDefaultSize() +{ + Size aScreenSize(getDisplay()->GetScreenSize(GetDisplayScreen())); + int scale = gtk_widget_get_scale_factor(m_pWindow); + aScreenSize.setWidth( aScreenSize.Width() / scale ); + aScreenSize.setHeight( aScreenSize.Height() / scale ); + return bestmaxFrameSizeForScreenSize(aScreenSize); +} + +void GtkSalFrame::SetDefaultSize() +{ + Size aDefSize = calcDefaultSize(); + + SetPosSize( 0, 0, aDefSize.Width(), aDefSize.Height(), + SAL_FRAME_POSSIZE_WIDTH | SAL_FRAME_POSSIZE_HEIGHT ); + + if( (m_nStyle & SalFrameStyleFlags::DEFAULT) && m_pWindow ) + gtk_window_maximize( GTK_WINDOW(m_pWindow) ); +} + +void GtkSalFrame::Show( bool bVisible, bool /*bNoActivate*/ ) +{ + if( !m_pWindow ) + return; + + if( bVisible ) + { + getDisplay()->startupNotificationCompleted(); + + if( m_bDefaultPos ) + Center(); + if( m_bDefaultSize ) + SetDefaultSize(); + setMinMaxSize(); + + if (isFloatGrabWindow() && !getDisplay()->GetCaptureFrame()) + { + m_pParent->grabPointer(true, true, true); + m_pParent->addGrabLevel(); + } + +#if defined(GDK_WINDOWING_WAYLAND) && !GTK_CHECK_VERSION(4,0,0) + /* + rhbz#1334915, gnome#779143, tdf#100158 + https://gitlab.gnome.org/GNOME/gtk/-/issues/767 + + before gdk_wayland_window_set_application_id was available gtk + under wayland lacked a way to change the app_id of a window, so + brute force everything as a startcenter when initially shown to at + least get the default LibreOffice icon and not the broken app icon + */ + static bool bAppIdImmutable = DLSYM_GDK_IS_WAYLAND_DISPLAY(getGdkDisplay()) && + !dlsym(nullptr, "gdk_wayland_window_set_application_id"); + if (bAppIdImmutable) + { + OString sOrigName(g_get_prgname()); + g_set_prgname("libreoffice-startcenter"); + gtk_widget_show(m_pWindow); + g_set_prgname(sOrigName.getStr()); + } + else + { + gtk_widget_show(m_pWindow); + } +#else + gtk_widget_show(m_pWindow); +#endif + + if( isFloatGrabWindow() ) + { + m_nFloats++; + if (!getDisplay()->GetCaptureFrame()) + { + grabPointer(true, true, true); + addGrabLevel(); + } + // #i44068# reset parent's IM context + if( m_pParent ) + m_pParent->EndExtTextInput(EndExtTextInputFlags::NONE); + } + } + else + { + if( isFloatGrabWindow() ) + { + m_nFloats--; + if (!getDisplay()->GetCaptureFrame()) + { + removeGrabLevel(); + grabPointer(false, true, false); + m_pParent->removeGrabLevel(); + bool bParentIsFloatGrabWindow = m_pParent->isFloatGrabWindow(); + m_pParent->grabPointer(bParentIsFloatGrabWindow, true, bParentIsFloatGrabWindow); + } + } + gtk_widget_hide( m_pWindow ); + if( m_pIMHandler ) + m_pIMHandler->focusChanged( false ); + } +} + +void GtkSalFrame::setMinMaxSize() +{ + /* #i34504# metacity (and possibly others) do not treat + * _NET_WM_STATE_FULLSCREEN and max_width/height independently; + * whether they should is undefined. So don't set the max size hint + * for a full screen window. + */ + if( !m_pWindow || isChild() ) + return; + +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkGeometry aGeo; + int aHints = 0; + if( m_nStyle & SalFrameStyleFlags::SIZEABLE ) + { + if( m_aMinSize.Width() && m_aMinSize.Height() && ! m_bFullscreen ) + { + aGeo.min_width = m_aMinSize.Width(); + aGeo.min_height = m_aMinSize.Height(); + aHints |= GDK_HINT_MIN_SIZE; + } + if( m_aMaxSize.Width() && m_aMaxSize.Height() && ! m_bFullscreen ) + { + aGeo.max_width = m_aMaxSize.Width(); + aGeo.max_height = m_aMaxSize.Height(); + aHints |= GDK_HINT_MAX_SIZE; + } + } + else + { + if (!m_bFullscreen && m_nWidthRequest && m_nHeightRequest) + { + aGeo.min_width = m_nWidthRequest; + aGeo.min_height = m_nHeightRequest; + aHints |= GDK_HINT_MIN_SIZE; + + aGeo.max_width = m_nWidthRequest; + aGeo.max_height = m_nHeightRequest; + aHints |= GDK_HINT_MAX_SIZE; + } + } + + if( m_bFullscreen && m_aMaxSize.Width() && m_aMaxSize.Height() ) + { + aGeo.max_width = m_aMaxSize.Width(); + aGeo.max_height = m_aMaxSize.Height(); + aHints |= GDK_HINT_MAX_SIZE; + } + if( aHints ) + { + gtk_window_set_geometry_hints( GTK_WINDOW(m_pWindow), + nullptr, + &aGeo, + GdkWindowHints( aHints ) ); + } +#endif +} + +void GtkSalFrame::SetMaxClientSize( tools::Long nWidth, tools::Long nHeight ) +{ + if( ! isChild() ) + { + m_aMaxSize = Size( nWidth, nHeight ); + setMinMaxSize(); + } +} +void GtkSalFrame::SetMinClientSize( tools::Long nWidth, tools::Long nHeight ) +{ + if( ! isChild() ) + { + m_aMinSize = Size( nWidth, nHeight ); + if( m_pWindow ) + { + widget_set_size_request(nWidth, nHeight); + setMinMaxSize(); + } + } +} + +void GtkSalFrame::AllocateFrame() +{ + basegfx::B2IVector aFrameSize( maGeometry.width(), maGeometry.height() ); + if (m_pSurface && m_aFrameSize.getX() == aFrameSize.getX() && + m_aFrameSize.getY() == aFrameSize.getY() ) + return; + + if( aFrameSize.getX() == 0 ) + aFrameSize.setX( 1 ); + if( aFrameSize.getY() == 0 ) + aFrameSize.setY( 1 ); + + if (m_pSurface) + cairo_surface_destroy(m_pSurface); + + m_pSurface = surface_create_similar_surface(widget_get_surface(m_pWindow), + CAIRO_CONTENT_COLOR_ALPHA, + aFrameSize.getX(), + aFrameSize.getY()); + m_aFrameSize = aFrameSize; + + cairo_surface_set_user_data(m_pSurface, SvpSalGraphics::getDamageKey(), &m_aDamageHandler, nullptr); + SAL_INFO("vcl.gtk3", "allocated Frame size of " << maGeometry.width() << " x " << maGeometry.height()); + + if (m_pGraphics) + m_pGraphics->setSurface(m_pSurface, m_aFrameSize); +} + +void GtkSalFrame::SetPosSize( tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight, sal_uInt16 nFlags ) +{ + if( !m_pWindow || isChild( true, false ) ) + return; + + if( (nFlags & ( SAL_FRAME_POSSIZE_WIDTH | SAL_FRAME_POSSIZE_HEIGHT )) && + (nWidth > 0 && nHeight > 0 ) // sometimes stupid things happen + ) + { + m_bDefaultSize = false; + + maGeometry.setSize({ nWidth, nHeight }); + + if (isChild(false) || GTK_IS_POPOVER(m_pWindow)) + widget_set_size_request(nWidth, nHeight); + else if( ! ( m_nState & GDK_TOPLEVEL_STATE_MAXIMIZED ) ) + window_resize(nWidth, nHeight); + + setMinMaxSize(); + } + else if( m_bDefaultSize ) + SetDefaultSize(); + + m_bDefaultSize = false; + + if( nFlags & ( SAL_FRAME_POSSIZE_X | SAL_FRAME_POSSIZE_Y ) ) + { + if( m_pParent ) + { + if( AllSettings::GetLayoutRTL() ) + nX = m_pParent->maGeometry.width()-m_nWidthRequest-1-nX; + nX += m_pParent->maGeometry.x(); + nY += m_pParent->maGeometry.y(); + } + + if (nFlags & SAL_FRAME_POSSIZE_X) + maGeometry.setX(nX); + if (nFlags & SAL_FRAME_POSSIZE_Y) + maGeometry.setY(nY); + m_bGeometryIsProvisional = true; + + m_bDefaultPos = false; + + moveWindow(maGeometry.x(), maGeometry.y()); + + updateScreenNumber(); + } + else if( m_bDefaultPos ) + Center(); + + m_bDefaultPos = false; +} + +void GtkSalFrame::GetClientSize( tools::Long& rWidth, tools::Long& rHeight ) +{ + if( m_pWindow && !(m_nState & GDK_TOPLEVEL_STATE_MINIMIZED) ) + { + rWidth = maGeometry.width(); + rHeight = maGeometry.height(); + } + else + rWidth = rHeight = 0; +} + +void GtkSalFrame::GetWorkArea( AbsoluteScreenPixelRectangle& rRect ) +{ + GdkRectangle aRect; +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkScreen *pScreen = gtk_widget_get_screen(m_pWindow); + AbsoluteScreenPixelRectangle aRetRect; + int max = gdk_screen_get_n_monitors (pScreen); + for (int i = 0; i < max; ++i) + { + gdk_screen_get_monitor_workarea(pScreen, i, &aRect); + AbsoluteScreenPixelRectangle aMonitorRect(aRect.x, aRect.y, aRect.x+aRect.width, aRect.y+aRect.height); + aRetRect.Union(aMonitorRect); + } + rRect = aRetRect; +#else + GdkDisplay* pDisplay = gtk_widget_get_display(m_pWindow); + GdkSurface* gdkWindow = widget_get_surface(m_pWindow); + GdkMonitor* pMonitor = gdk_display_get_monitor_at_surface(pDisplay, gdkWindow); + gdk_monitor_get_geometry(pMonitor, &aRect); + rRect = AbsoluteScreenPixelRectangle(aRect.x, aRect.y, aRect.x+aRect.width, aRect.y+aRect.height); +#endif +} + +SalFrame* GtkSalFrame::GetParent() const +{ + return m_pParent; +} + +void GtkSalFrame::SetWindowState(const vcl::WindowData* pState) +{ + if( ! m_pWindow || ! pState || isChild( true, false ) ) + return; + + const vcl::WindowDataMask nMaxGeometryMask = vcl::WindowDataMask::PosSize | + vcl::WindowDataMask::MaximizedX | vcl::WindowDataMask::MaximizedY | + vcl::WindowDataMask::MaximizedWidth | vcl::WindowDataMask::MaximizedHeight; + + if( (pState->mask() & vcl::WindowDataMask::State) && + ! ( m_nState & GDK_TOPLEVEL_STATE_MAXIMIZED ) && + (pState->state() & vcl::WindowState::Maximized) && + (pState->mask() & nMaxGeometryMask) == nMaxGeometryMask ) + { + resizeWindow(pState->width(), pState->height()); + moveWindow(pState->x(), pState->y()); + m_bDefaultPos = m_bDefaultSize = false; + + updateScreenNumber(); + + m_nState = GdkToplevelState(m_nState | GDK_TOPLEVEL_STATE_MAXIMIZED); + m_aRestorePosSize = pState->posSize(); + } + else if (pState->mask() & vcl::WindowDataMask::PosSize) + { + sal_uInt16 nPosSizeFlags = 0; + tools::Long nX = pState->x() - (m_pParent ? m_pParent->maGeometry.x() : 0); + tools::Long nY = pState->y() - (m_pParent ? m_pParent->maGeometry.y() : 0); + if (pState->mask() & vcl::WindowDataMask::X) + nPosSizeFlags |= SAL_FRAME_POSSIZE_X; + else + nX = maGeometry.x() - (m_pParent ? m_pParent->maGeometry.x() : 0); + if (pState->mask() & vcl::WindowDataMask::Y) + nPosSizeFlags |= SAL_FRAME_POSSIZE_Y; + else + nY = maGeometry.y() - (m_pParent ? m_pParent->maGeometry.y() : 0); + if (pState->mask() & vcl::WindowDataMask::Width) + nPosSizeFlags |= SAL_FRAME_POSSIZE_WIDTH; + if (pState->mask() & vcl::WindowDataMask::Height) + nPosSizeFlags |= SAL_FRAME_POSSIZE_HEIGHT; + SetPosSize(nX, nY, pState->width(), pState->height(), nPosSizeFlags); + } + + if (pState->mask() & vcl::WindowDataMask::State && !isChild()) + { + if (pState->state() & vcl::WindowState::Maximized) + gtk_window_maximize( GTK_WINDOW(m_pWindow) ); + else + gtk_window_unmaximize( GTK_WINDOW(m_pWindow) ); + /* #i42379# there is no rollup state in GDK; and rolled up windows are + * (probably depending on the WM) reported as iconified. If we iconify a + * window here that was e.g. a dialog, then it will be unmapped but still + * not be displayed in the task list, so it's an iconified window that + * the user cannot get out of this state. So do not set the iconified state + * on windows with a parent (that is transient frames) since these tend + * to not be represented in an icon task list. + */ + bool bMinimize = pState->state() & vcl::WindowState::Minimized && !m_pParent; +#if GTK_CHECK_VERSION(4, 0, 0) + if (bMinimize) + gtk_window_minimize(GTK_WINDOW(m_pWindow)); + else + gtk_window_unminimize(GTK_WINDOW(m_pWindow)); +#else + if (bMinimize) + gtk_window_iconify(GTK_WINDOW(m_pWindow)); + else + gtk_window_deiconify(GTK_WINDOW(m_pWindow)); +#endif + } + TriggerPaintEvent(); +} + +namespace +{ + void GetPosAndSize(GtkWindow *pWindow, tools::Long& rX, tools::Long &rY, tools::Long &rWidth, tools::Long &rHeight) + { + gint width, height; +#if !GTK_CHECK_VERSION(4, 0, 0) + gint root_x, root_y; + gtk_window_get_position(GTK_WINDOW(pWindow), &root_x, &root_y); + rX = root_x; + rY = root_y; + + gtk_window_get_size(GTK_WINDOW(pWindow), &width, &height); +#else + rX = 0; + rY = 0; + gtk_window_get_default_size(GTK_WINDOW(pWindow), &width, &height); +#endif + rWidth = width; + rHeight = height; + } + + tools::Rectangle GetPosAndSize(GtkWindow *pWindow) + { + tools::Long nX, nY, nWidth, nHeight; + GetPosAndSize(pWindow, nX, nY, nWidth, nHeight); + return tools::Rectangle(nX, nY, nX + nWidth, nY + nHeight); + } +} + +bool GtkSalFrame::GetWindowState(vcl::WindowData* pState) +{ + pState->setState(vcl::WindowState::Normal); + pState->setMask(vcl::WindowDataMask::PosSizeState); + + // rollup ? gtk 2.2 does not seem to support the shaded state + if( m_nState & GDK_TOPLEVEL_STATE_MINIMIZED ) + pState->rState() |= vcl::WindowState::Minimized; + if( m_nState & GDK_TOPLEVEL_STATE_MAXIMIZED ) + { + pState->rState() |= vcl::WindowState::Maximized; + pState->setPosSize(m_aRestorePosSize); + tools::Rectangle aPosSize = GetPosAndSize(GTK_WINDOW(m_pWindow)); + pState->SetMaximizedX(aPosSize.Left()); + pState->SetMaximizedY(aPosSize.Top()); + pState->SetMaximizedWidth(aPosSize.GetWidth()); + pState->SetMaximizedHeight(aPosSize.GetHeight()); + pState->rMask() |= vcl::WindowDataMask::MaximizedX | + vcl::WindowDataMask::MaximizedY | + vcl::WindowDataMask::MaximizedWidth | + vcl::WindowDataMask::MaximizedHeight; + } + else + pState->setPosSize(GetPosAndSize(GTK_WINDOW(m_pWindow))); + + return true; +} + +void GtkSalFrame::SetScreen( unsigned int nNewScreen, SetType eType, tools::Rectangle const *pSize ) +{ + if( !m_pWindow ) + return; + + if (maGeometry.screen() == nNewScreen && eType == SetType::RetainSize) + return; + +#if !GTK_CHECK_VERSION(4, 0, 0) + int nX = maGeometry.x(), nY = maGeometry.y(), + nWidth = maGeometry.width(), nHeight = maGeometry.height(); + GdkScreen *pScreen = nullptr; + GdkRectangle aNewMonitor; + + bool bSpanAllScreens = nNewScreen == static_cast(-1); + bool bSpanMonitorsWhenFullscreen = bSpanAllScreens && getDisplay()->getSystem()->GetDisplayScreenCount() > 1; + gint nMonitor = -1; + if (bSpanMonitorsWhenFullscreen) //span all screens + { + pScreen = gtk_widget_get_screen( m_pWindow ); + aNewMonitor.x = 0; + aNewMonitor.y = 0; + aNewMonitor.width = gdk_screen_get_width(pScreen); + aNewMonitor.height = gdk_screen_get_height(pScreen); + } + else + { + bool bSameMonitor = false; + + if (!bSpanAllScreens) + { + pScreen = getDisplay()->getSystem()->getScreenMonitorFromIdx( nNewScreen, nMonitor ); + if (!pScreen) + { + g_warning ("Attempt to move GtkSalFrame to invalid screen %d => " + "fallback to current\n", nNewScreen); + } + } + + if (!pScreen) + { + pScreen = gtk_widget_get_screen( m_pWindow ); + bSameMonitor = true; + } + + // Heavy lifting, need to move screen ... + if( pScreen != gtk_widget_get_screen( m_pWindow )) + gtk_window_set_screen( GTK_WINDOW( m_pWindow ), pScreen ); + + gint nOldMonitor = gdk_screen_get_monitor_at_window( + pScreen, widget_get_surface( m_pWindow ) ); + if (bSameMonitor) + nMonitor = nOldMonitor; + + #if OSL_DEBUG_LEVEL > 1 + if( nMonitor == nOldMonitor ) + g_warning( "An apparently pointless SetScreen - should we elide it ?" ); + #endif + + GdkRectangle aOldMonitor; + gdk_screen_get_monitor_geometry( pScreen, nOldMonitor, &aOldMonitor ); + gdk_screen_get_monitor_geometry( pScreen, nMonitor, &aNewMonitor ); + + nX = aNewMonitor.x + nX - aOldMonitor.x; + nY = aNewMonitor.y + nY - aOldMonitor.y; + } + + bool bResize = false; + bool bVisible = gtk_widget_get_mapped( m_pWindow ); + if( bVisible ) + Show( false ); + + if( eType == SetType::Fullscreen ) + { + nX = aNewMonitor.x; + nY = aNewMonitor.y; + nWidth = aNewMonitor.width; + nHeight = aNewMonitor.height; + bResize = true; + + // #i110881# for the benefit of compiz set a max size here + // else setting to fullscreen fails for unknown reasons + m_aMaxSize.setWidth( aNewMonitor.width ); + m_aMaxSize.setHeight( aNewMonitor.height ); + } + + if( pSize && eType == SetType::UnFullscreen ) + { + nX = pSize->Left(); + nY = pSize->Top(); + nWidth = pSize->GetWidth(); + nHeight = pSize->GetHeight(); + bResize = true; + } + + if (bResize) + { + // temporarily re-sizeable + if( !(m_nStyle & SalFrameStyleFlags::SIZEABLE) ) + gtk_window_set_resizable( GTK_WINDOW(m_pWindow), true ); + window_resize(nWidth, nHeight); + } + + gtk_window_move(GTK_WINDOW(m_pWindow), nX, nY); + + GdkFullscreenMode eMode = + bSpanMonitorsWhenFullscreen ? GDK_FULLSCREEN_ON_ALL_MONITORS : GDK_FULLSCREEN_ON_CURRENT_MONITOR; + + gdk_window_set_fullscreen_mode(widget_get_surface(m_pWindow), eMode); + + GtkWidget* pMenuBarContainerWidget = m_pSalMenu ? m_pSalMenu->GetMenuBarContainerWidget() : nullptr; + if( eType == SetType::Fullscreen ) + { + if (pMenuBarContainerWidget) + gtk_widget_hide(pMenuBarContainerWidget); + if (bSpanMonitorsWhenFullscreen) + gtk_window_fullscreen(GTK_WINDOW(m_pWindow)); + else + gtk_window_fullscreen_on_monitor(GTK_WINDOW(m_pWindow), pScreen, nMonitor); + + } + else if( eType == SetType::UnFullscreen ) + { + if (pMenuBarContainerWidget) + gtk_widget_show(pMenuBarContainerWidget); + gtk_window_unfullscreen( GTK_WINDOW( m_pWindow ) ); + } + + if( eType == SetType::UnFullscreen && + !(m_nStyle & SalFrameStyleFlags::SIZEABLE) ) + gtk_window_set_resizable( GTK_WINDOW( m_pWindow ), FALSE ); + + // FIXME: we should really let gtk+ handle our widget hierarchy ... + if( m_pParent && gtk_widget_get_screen( m_pParent->m_pWindow ) != pScreen ) + SetParent( nullptr ); + + std::list< GtkSalFrame* > aChildren = m_aChildren; + for (auto const& child : aChildren) + child->SetScreen( nNewScreen, SetType::RetainSize ); + + m_bDefaultPos = m_bDefaultSize = false; + updateScreenNumber(); + + if( bVisible ) + Show( true ); + +#else + (void)pSize; // assume restore will restore the original size without our help + + bool bSpanMonitorsWhenFullscreen = nNewScreen == static_cast(-1); + + GdkFullscreenMode eMode = + bSpanMonitorsWhenFullscreen ? GDK_FULLSCREEN_ON_ALL_MONITORS : GDK_FULLSCREEN_ON_CURRENT_MONITOR; + + g_object_set(widget_get_surface(m_pWindow), "fullscreen-mode", eMode, nullptr); + + GtkWidget* pMenuBarContainerWidget = m_pSalMenu ? m_pSalMenu->GetMenuBarContainerWidget() : nullptr; + if (eType == SetType::Fullscreen) + { + if (!(m_nStyle & SalFrameStyleFlags::SIZEABLE)) + { + // temp make it resizable, restore when unfullscreened + gtk_window_set_resizable(GTK_WINDOW(m_pWindow), true); + } + + if (pMenuBarContainerWidget) + gtk_widget_hide(pMenuBarContainerWidget); + if (bSpanMonitorsWhenFullscreen) + gtk_window_fullscreen(GTK_WINDOW(m_pWindow)); + else + { + GdkDisplay* pDisplay = gtk_widget_get_display(m_pWindow); + GListModel* pList = gdk_display_get_monitors(pDisplay); + GdkMonitor* pMonitor = static_cast(g_list_model_get_item(pList, nNewScreen)); + if (!pMonitor) + pMonitor = gdk_display_get_monitor_at_surface(pDisplay, widget_get_surface(m_pWindow)); + gtk_window_fullscreen_on_monitor(GTK_WINDOW(m_pWindow), pMonitor); + } + } + else if (eType == SetType::UnFullscreen) + { + if (pMenuBarContainerWidget) + gtk_widget_show(pMenuBarContainerWidget); + gtk_window_unfullscreen(GTK_WINDOW(m_pWindow)); + + if (!(m_nStyle & SalFrameStyleFlags::SIZEABLE)) + { + // restore temp resizability + gtk_window_set_resizable(GTK_WINDOW(m_pWindow), false); + } + } + + for (auto const& child : m_aChildren) + child->SetScreen(nNewScreen, SetType::RetainSize); + + m_bDefaultPos = m_bDefaultSize = false; + updateScreenNumber(); +#endif +} + +void GtkSalFrame::SetScreenNumber( unsigned int nNewScreen ) +{ + SetScreen( nNewScreen, SetType::RetainSize ); +} + +void GtkSalFrame::updateWMClass() +{ + if (!DLSYM_GDK_IS_X11_DISPLAY(getGdkDisplay())) + return; + + if (!gtk_widget_get_realized(m_pWindow)) + return; + + OString aResClass = OUStringToOString(m_sWMClass, RTL_TEXTENCODING_ASCII_US); + const char *pResClass = !aResClass.isEmpty() ? aResClass.getStr() : + SalGenericSystem::getFrameClassName(); + XClassHint* pClass = XAllocClassHint(); + OString aResName = SalGenericSystem::getFrameResName(); + pClass->res_name = const_cast(aResName.getStr()); + pClass->res_class = const_cast(pResClass); + Display *display = gdk_x11_display_get_xdisplay(getGdkDisplay()); + XSetClassHint( display, + GtkSalFrame::GetNativeWindowHandle(m_pWindow), + pClass ); + XFree( pClass ); +} + +void GtkSalFrame::SetApplicationID( const OUString &rWMClass ) +{ + if( rWMClass != m_sWMClass && ! isChild() ) + { + m_sWMClass = rWMClass; + updateWMClass(); + + for (auto const& child : m_aChildren) + child->SetApplicationID(rWMClass); + } +} + +void GtkSalFrame::ShowFullScreen( bool bFullScreen, sal_Int32 nScreen ) +{ + m_bFullscreen = bFullScreen; + + if( !m_pWindow || isChild() ) + return; + + if( bFullScreen ) + { + m_aRestorePosSize = GetPosAndSize(GTK_WINDOW(m_pWindow)); + SetScreen( nScreen, SetType::Fullscreen ); + } + else + { + SetScreen( nScreen, SetType::UnFullscreen, + !m_aRestorePosSize.IsEmpty() ? &m_aRestorePosSize : nullptr ); + m_aRestorePosSize = tools::Rectangle(); + } +} + +void GtkSalFrame::SessionManagerInhibit(bool bStart, ApplicationInhibitFlags eType, std::u16string_view sReason, const char* application_id) +{ + guint nWindow(0); + std::optional aDisplay; + + if (DLSYM_GDK_IS_X11_DISPLAY(getGdkDisplay())) + { + nWindow = GtkSalFrame::GetNativeWindowHandle(m_pWindow); + aDisplay = gdk_x11_display_get_xdisplay(getGdkDisplay()); + } + + m_SessionManagerInhibitor.inhibit(bStart, sReason, eType, + nWindow, aDisplay, application_id); +} + +void GtkSalFrame::StartPresentation( bool bStart ) +{ + SessionManagerInhibit(bStart, APPLICATION_INHIBIT_IDLE, u"presentation", nullptr); +} + +void GtkSalFrame::SetAlwaysOnTop( bool bOnTop ) +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + if( m_pWindow ) + gtk_window_set_keep_above( GTK_WINDOW( m_pWindow ), bOnTop ); +#else + (void)bOnTop; +#endif +} + +static guint32 nLastUserInputTime = GDK_CURRENT_TIME; + +guint32 GtkSalFrame::GetLastInputEventTime() +{ + return nLastUserInputTime; +} + +void GtkSalFrame::UpdateLastInputEventTime(guint32 nUserInputTime) +{ + //gtk3 can generate a synthetic crossing event with a useless 0 + //(GDK_CURRENT_TIME) timestamp on showing a menu from the main + //menubar, which is unhelpful, so ignore the 0 timestamps + if (nUserInputTime == GDK_CURRENT_TIME) + return; + nLastUserInputTime = nUserInputTime; +} + +void GtkSalFrame::ToTop( SalFrameToTop nFlags ) +{ + if( !m_pWindow ) + return; + + if( isChild( false ) ) + GrabFocus(); + else if( gtk_widget_get_mapped( m_pWindow ) ) + { + auto nTimestamp = GetLastInputEventTime(); +#ifdef GDK_WINDOWING_X11 + GdkDisplay *pDisplay = GtkSalFrame::getGdkDisplay(); + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + nTimestamp = gdk_x11_display_get_user_time(pDisplay); +#endif + if (!(nFlags & SalFrameToTop::GrabFocusOnly)) + gtk_window_present_with_time(GTK_WINDOW(m_pWindow), nTimestamp); +#if !GTK_CHECK_VERSION(4, 0, 0) + else + gdk_window_focus(widget_get_surface(m_pWindow), nTimestamp); +#endif + GrabFocus(); + } + else + { + if( nFlags & SalFrameToTop::RestoreWhenMin ) + gtk_window_present( GTK_WINDOW(m_pWindow) ); + } +} + +void GtkSalFrame::SetPointer( PointerStyle ePointerStyle ) +{ + if( !m_pWindow || ePointerStyle == m_ePointerStyle ) + return; + + m_ePointerStyle = ePointerStyle; + GdkCursor *pCursor = getDisplay()->getCursor( ePointerStyle ); + widget_set_cursor(GTK_WIDGET(m_pWindow), pCursor); +} + +void GtkSalFrame::grabPointer( bool bGrab, bool bKeyboardAlso, bool bOwnerEvents ) +{ + if (bGrab) + { + // tdf#135779 move focus back inside usual input window out of any + // other gtk widgets before grabbing the pointer + GrabFocus(); + } + + static const char* pEnv = getenv( "SAL_NO_MOUSEGRABS" ); + if (pEnv && *pEnv) + return; + + if (!m_pWindow) + return; + +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkSeat* pSeat = gdk_display_get_default_seat(getGdkDisplay()); + if (bGrab) + { + GdkSeatCapabilities eCapability = bKeyboardAlso ? GDK_SEAT_CAPABILITY_ALL : GDK_SEAT_CAPABILITY_ALL_POINTING; + gdk_seat_grab(pSeat, widget_get_surface(getMouseEventWidget()), eCapability, + bOwnerEvents, nullptr, nullptr, nullptr, nullptr); + } + else + { + gdk_seat_ungrab(pSeat); + } +#else + (void)bKeyboardAlso; + (void)bOwnerEvents; +#endif +} + +void GtkSalFrame::CaptureMouse( bool bCapture ) +{ + getDisplay()->CaptureMouse( bCapture ? this : nullptr ); +} + +void GtkSalFrame::SetPointerPos( tools::Long nX, tools::Long nY ) +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkSalFrame* pFrame = this; + while( pFrame && pFrame->isChild( false ) ) + pFrame = pFrame->m_pParent; + if( ! pFrame ) + return; + + GdkScreen *pScreen = gtk_widget_get_screen(pFrame->m_pWindow); + GdkDisplay *pDisplay = gdk_screen_get_display( pScreen ); + + /* when the application tries to center the mouse in the dialog the + * window isn't mapped already. So use coordinates relative to the root window. + */ + unsigned int nWindowLeft = maGeometry.x() + nX; + unsigned int nWindowTop = maGeometry.y() + nY; + + GdkDeviceManager* pManager = gdk_display_get_device_manager(pDisplay); + gdk_device_warp(gdk_device_manager_get_client_pointer(pManager), pScreen, nWindowLeft, nWindowTop); + + // #i38648# ask for the next motion hint + gint x, y; + GdkModifierType mask; + gdk_window_get_pointer( widget_get_surface(pFrame->m_pWindow) , &x, &y, &mask ); +#else + (void)nX; + (void)nY; +#endif +} + +void GtkSalFrame::Flush() +{ + gdk_display_flush( getGdkDisplay() ); +} + +void GtkSalFrame::KeyCodeToGdkKey(const vcl::KeyCode& rKeyCode, + guint* pGdkKeyCode, GdkModifierType *pGdkModifiers) +{ + if ( pGdkKeyCode == nullptr || pGdkModifiers == nullptr ) + return; + + // Get GDK key modifiers + GdkModifierType nModifiers = GdkModifierType(0); + + if ( rKeyCode.IsShift() ) + nModifiers = static_cast( nModifiers | GDK_SHIFT_MASK ); + + if ( rKeyCode.IsMod1() ) + nModifiers = static_cast( nModifiers | GDK_CONTROL_MASK ); + + if ( rKeyCode.IsMod2() ) + nModifiers = static_cast( nModifiers | GDK_ALT_MASK ); + + *pGdkModifiers = nModifiers; + + // Get GDK keycode. + guint nKeyCode = 0; + + guint nCode = rKeyCode.GetCode(); + + if ( nCode >= KEY_0 && nCode <= KEY_9 ) + nKeyCode = ( nCode - KEY_0 ) + GDK_KEY_0; + else if ( nCode >= KEY_A && nCode <= KEY_Z ) + nKeyCode = ( nCode - KEY_A ) + GDK_KEY_A; + else if ( nCode >= KEY_F1 && nCode <= KEY_F26 ) + nKeyCode = ( nCode - KEY_F1 ) + GDK_KEY_F1; + else + { + switch (nCode) + { + case KEY_DOWN: nKeyCode = GDK_KEY_Down; break; + case KEY_UP: nKeyCode = GDK_KEY_Up; break; + case KEY_LEFT: nKeyCode = GDK_KEY_Left; break; + case KEY_RIGHT: nKeyCode = GDK_KEY_Right; break; + case KEY_HOME: nKeyCode = GDK_KEY_Home; break; + case KEY_END: nKeyCode = GDK_KEY_End; break; + case KEY_PAGEUP: nKeyCode = GDK_KEY_Page_Up; break; + case KEY_PAGEDOWN: nKeyCode = GDK_KEY_Page_Down; break; + case KEY_RETURN: nKeyCode = GDK_KEY_Return; break; + case KEY_ESCAPE: nKeyCode = GDK_KEY_Escape; break; + case KEY_TAB: nKeyCode = GDK_KEY_Tab; break; + case KEY_BACKSPACE: nKeyCode = GDK_KEY_BackSpace; break; + case KEY_SPACE: nKeyCode = GDK_KEY_space; break; + case KEY_INSERT: nKeyCode = GDK_KEY_Insert; break; + case KEY_DELETE: nKeyCode = GDK_KEY_Delete; break; + case KEY_ADD: nKeyCode = GDK_KEY_plus; break; + case KEY_SUBTRACT: nKeyCode = GDK_KEY_minus; break; + case KEY_MULTIPLY: nKeyCode = GDK_KEY_asterisk; break; + case KEY_DIVIDE: nKeyCode = GDK_KEY_slash; break; + case KEY_POINT: nKeyCode = GDK_KEY_period; break; + case KEY_COMMA: nKeyCode = GDK_KEY_comma; break; + case KEY_LESS: nKeyCode = GDK_KEY_less; break; + case KEY_GREATER: nKeyCode = GDK_KEY_greater; break; + case KEY_EQUAL: nKeyCode = GDK_KEY_equal; break; + case KEY_FIND: nKeyCode = GDK_KEY_Find; break; + case KEY_CONTEXTMENU: nKeyCode = GDK_KEY_Menu; break; + case KEY_HELP: nKeyCode = GDK_KEY_Help; break; + case KEY_UNDO: nKeyCode = GDK_KEY_Undo; break; + case KEY_REPEAT: nKeyCode = GDK_KEY_Redo; break; + case KEY_DECIMAL: nKeyCode = GDK_KEY_KP_Decimal; break; + case KEY_TILDE: nKeyCode = GDK_KEY_asciitilde; break; + case KEY_QUOTELEFT: nKeyCode = GDK_KEY_quoteleft; break; + case KEY_BRACKETLEFT: nKeyCode = GDK_KEY_bracketleft; break; + case KEY_BRACKETRIGHT: nKeyCode = GDK_KEY_bracketright; break; + case KEY_SEMICOLON: nKeyCode = GDK_KEY_semicolon; break; + case KEY_QUOTERIGHT: nKeyCode = GDK_KEY_quoteright; break; + case KEY_RIGHTCURLYBRACKET: nKeyCode = GDK_KEY_braceright; break; + case KEY_NUMBERSIGN: nKeyCode = GDK_KEY_numbersign; break; + case KEY_XF86FORWARD: nKeyCode = GDK_KEY_Forward; break; + case KEY_XF86BACK: nKeyCode = GDK_KEY_Back; break; + case KEY_COLON: nKeyCode = GDK_KEY_colon; break; + + // Special cases + case KEY_COPY: nKeyCode = GDK_KEY_Copy; break; + case KEY_CUT: nKeyCode = GDK_KEY_Cut; break; + case KEY_PASTE: nKeyCode = GDK_KEY_Paste; break; + case KEY_OPEN: nKeyCode = GDK_KEY_Open; break; + } + } + + *pGdkKeyCode = nKeyCode; +} + +OUString GtkSalFrame::GetKeyName( sal_uInt16 nKeyCode ) +{ + guint nGtkKeyCode; + GdkModifierType nGtkModifiers; + KeyCodeToGdkKey(nKeyCode, &nGtkKeyCode, &nGtkModifiers ); + + gchar* pName = gtk_accelerator_get_label(nGtkKeyCode, nGtkModifiers); + OUString aRet = OStringToOUString(pName, RTL_TEXTENCODING_UTF8); + g_free(pName); + return aRet; +} + +GdkDisplay *GtkSalFrame::getGdkDisplay() +{ + return GetGtkSalData()->GetGdkDisplay(); +} + +GtkSalDisplay *GtkSalFrame::getDisplay() +{ + return GetGtkSalData()->GetGtkDisplay(); +} + +SalFrame::SalPointerState GtkSalFrame::GetPointerState() +{ + SalPointerState aState; +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkScreen* pScreen; + gint x, y; + GdkModifierType aMask; + gdk_display_get_pointer( getGdkDisplay(), &pScreen, &x, &y, &aMask ); + aState.maPos = Point( x - maGeometry.x(), y - maGeometry.y() ); + aState.mnState = GetMouseModCode( aMask ); +#endif + return aState; +} + +KeyIndicatorState GtkSalFrame::GetIndicatorState() +{ + KeyIndicatorState nState = KeyIndicatorState::NONE; + +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkKeymap *pKeyMap = gdk_keymap_get_for_display(getGdkDisplay()); + + if (gdk_keymap_get_caps_lock_state(pKeyMap)) + nState |= KeyIndicatorState::CAPSLOCK; + if (gdk_keymap_get_num_lock_state(pKeyMap)) + nState |= KeyIndicatorState::NUMLOCK; + if (gdk_keymap_get_scroll_lock_state(pKeyMap)) + nState |= KeyIndicatorState::SCROLLLOCK; +#endif + + return nState; +} + +void GtkSalFrame::SimulateKeyPress( sal_uInt16 nKeyCode ) +{ + g_warning ("missing simulate keypress %d", nKeyCode); +} + +void GtkSalFrame::SetInputContext( SalInputContext* pContext ) +{ + if( ! pContext ) + return; + + if( ! (pContext->mnOptions & InputContextFlags::Text) ) + return; + + // create a new im context + if( ! m_pIMHandler ) + m_pIMHandler.reset( new IMHandler( this ) ); +} + +void GtkSalFrame::EndExtTextInput( EndExtTextInputFlags nFlags ) +{ + if( m_pIMHandler ) + m_pIMHandler->endExtTextInput( nFlags ); +} + +bool GtkSalFrame::MapUnicodeToKeyCode( sal_Unicode , LanguageType , vcl::KeyCode& ) +{ + // not supported yet + return false; +} + +LanguageType GtkSalFrame::GetInputLanguage() +{ + return LANGUAGE_DONTKNOW; +} + +void GtkSalFrame::UpdateSettings( AllSettings& rSettings ) +{ + if( ! m_pWindow ) + return; + + GtkSalGraphics* pGraphics = m_pGraphics.get(); + bool bFreeGraphics = false; + if( ! pGraphics ) + { + pGraphics = static_cast(AcquireGraphics()); + if ( !pGraphics ) + { + SAL_WARN("vcl.gtk3", "Could not get graphics - unable to update settings"); + return; + } + bFreeGraphics = true; + } + + pGraphics->UpdateSettings( rSettings ); + + if( bFreeGraphics ) + ReleaseGraphics( pGraphics ); +} + +void GtkSalFrame::Beep() +{ + gdk_display_beep( getGdkDisplay() ); +} + +const SystemEnvData* GtkSalFrame::GetSystemData() const +{ + return &m_aSystemData; +} + +void GtkSalFrame::ResolveWindowHandle(SystemEnvData& rData) const +{ + if (!rData.pWidget) + return; + SAL_WARN("vcl.gtk3", "its undesirable to need the NativeWindowHandle, see tdf#139609"); + rData.SetWindowHandle(GetNativeWindowHandle(static_cast(rData.pWidget))); +} + +void GtkSalFrame::SetParent( SalFrame* pNewParent ) +{ + GtkWindow* pWindow = GTK_IS_WINDOW(m_pWindow) ? GTK_WINDOW(m_pWindow) : nullptr; + if (m_pParent) + { + if (pWindow && GTK_IS_WINDOW(m_pParent->m_pWindow)) + gtk_window_group_remove_window(gtk_window_get_group(GTK_WINDOW(m_pParent->m_pWindow)), pWindow); + m_pParent->m_aChildren.remove(this); + } + m_pParent = static_cast(pNewParent); + if (m_pParent) + { + m_pParent->m_aChildren.push_back(this); + if (pWindow && GTK_IS_WINDOW(m_pParent->m_pWindow)) + gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(m_pParent->m_pWindow)), pWindow); + } + if (!isChild() && pWindow) + gtk_window_set_transient_for( pWindow, + (m_pParent && ! m_pParent->isChild(true,false)) ? GTK_WINDOW(m_pParent->m_pWindow) : nullptr + ); +} + +void GtkSalFrame::SetPluginParent( SystemParentData* ) +{ + //FIXME: no SetPluginParent impl. for gtk3 +} + +void GtkSalFrame::ResetClipRegion() +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + if( m_pWindow ) + gdk_window_shape_combine_region( widget_get_surface( m_pWindow ), nullptr, 0, 0 ); +#endif +} + +void GtkSalFrame::BeginSetClipRegion( sal_uInt32 ) +{ + if( m_pRegion ) + cairo_region_destroy( m_pRegion ); + m_pRegion = cairo_region_create(); +} + +void GtkSalFrame::UnionClipRegion( tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight ) +{ + if( m_pRegion ) + { + GdkRectangle aRect; + aRect.x = nX; + aRect.y = nY; + aRect.width = nWidth; + aRect.height = nHeight; + cairo_region_union_rectangle( m_pRegion, &aRect ); + } +} + +void GtkSalFrame::EndSetClipRegion() +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + if( m_pWindow && m_pRegion ) + gdk_window_shape_combine_region( widget_get_surface(m_pWindow), m_pRegion, 0, 0 ); +#endif +} + +void GtkSalFrame::PositionByToolkit(const tools::Rectangle& rRect, FloatWinPopupFlags nFlags) +{ + if ( ImplGetSVData()->maNWFData.mbCanDetermineWindowPosition && + // tdf#152155 cannot determine window positions of popup listboxes on sidebar + nFlags != LISTBOX_FLOATWINPOPUPFLAGS ) + { + return; + } + + m_aFloatRect = rRect; + m_nFloatFlags = nFlags; + m_bFloatPositioned = true; +} + +void GtkSalFrame::SetModal(bool bModal) +{ + if (!m_pWindow) + return; + gtk_window_set_modal(GTK_WINDOW(m_pWindow), bModal); +} + +bool GtkSalFrame::GetModal() const +{ + if (!m_pWindow) + return false; + return gtk_window_get_modal(GTK_WINDOW(m_pWindow)); +} + +gboolean GtkSalFrame::signalTooltipQuery(GtkWidget*, gint /*x*/, gint /*y*/, + gboolean /*keyboard_mode*/, GtkTooltip *tooltip, + gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + if (pThis->m_aTooltip.isEmpty() || pThis->m_bTooltipBlocked) + return false; + gtk_tooltip_set_text(tooltip, + OUStringToOString(pThis->m_aTooltip, RTL_TEXTENCODING_UTF8).getStr()); + GdkRectangle aHelpArea; + aHelpArea.x = pThis->m_aHelpArea.Left(); + aHelpArea.y = pThis->m_aHelpArea.Top(); + aHelpArea.width = pThis->m_aHelpArea.GetWidth(); + aHelpArea.height = pThis->m_aHelpArea.GetHeight(); + if (AllSettings::GetLayoutRTL()) + aHelpArea.x = pThis->maGeometry.width()-aHelpArea.width-1-aHelpArea.x; + gtk_tooltip_set_tip_area(tooltip, &aHelpArea); + return true; +} + +bool GtkSalFrame::ShowTooltip(const OUString& rHelpText, const tools::Rectangle& rHelpArea) +{ + m_aTooltip = rHelpText; + m_aHelpArea = rHelpArea; + gtk_widget_trigger_tooltip_query(getMouseEventWidget()); + return true; +} + +void GtkSalFrame::BlockTooltip() +{ + m_bTooltipBlocked = true; +} + +void GtkSalFrame::UnblockTooltip() +{ + m_bTooltipBlocked = false; +} + +void GtkSalFrame::HideTooltip() +{ + m_aTooltip.clear(); + GtkWidget* pEventWidget = getMouseEventWidget(); + gtk_widget_trigger_tooltip_query(pEventWidget); +} + +namespace +{ + void set_pointing_to(GtkPopover *pPopOver, vcl::Window* pParent, const tools::Rectangle& rHelpArea, const SalFrameGeometry& rGeometry) + { + GdkRectangle aRect; + aRect.x = FloatingWindow::ImplConvertToAbsPos(pParent, rHelpArea).Left() - rGeometry.x(); + aRect.y = rHelpArea.Top(); + aRect.width = 1; + aRect.height = 1; + + GtkPositionType ePos = gtk_popover_get_position(pPopOver); + switch (ePos) + { + case GTK_POS_BOTTOM: + case GTK_POS_TOP: + aRect.width = rHelpArea.GetWidth(); + break; + case GTK_POS_RIGHT: + case GTK_POS_LEFT: + aRect.height = rHelpArea.GetHeight(); + break; + } + + gtk_popover_set_pointing_to(pPopOver, &aRect); + } +} + +void* GtkSalFrame::ShowPopover(const OUString& rHelpText, vcl::Window* pParent, const tools::Rectangle& rHelpArea, QuickHelpFlags nFlags) +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget *pWidget = gtk_popover_new(getMouseEventWidget()); +#else + GtkWidget *pWidget = gtk_popover_new(); + gtk_widget_set_parent(pWidget, getMouseEventWidget()); +#endif + OString sUTF = OUStringToOString(rHelpText, RTL_TEXTENCODING_UTF8); + GtkWidget *pLabel = gtk_label_new(sUTF.getStr()); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_add(GTK_CONTAINER(pWidget), pLabel); +#else + gtk_popover_set_child(GTK_POPOVER(pWidget), pLabel); +#endif + + if (nFlags & QuickHelpFlags::Top) + gtk_popover_set_position(GTK_POPOVER(pWidget), GTK_POS_BOTTOM); + else if (nFlags & QuickHelpFlags::Bottom) + gtk_popover_set_position(GTK_POPOVER(pWidget), GTK_POS_TOP); + else if (nFlags & QuickHelpFlags::Left) + gtk_popover_set_position(GTK_POPOVER(pWidget), GTK_POS_RIGHT); + else if (nFlags & QuickHelpFlags::Right) + gtk_popover_set_position(GTK_POPOVER(pWidget), GTK_POS_LEFT); + + set_pointing_to(GTK_POPOVER(pWidget), pParent, rHelpArea, maGeometry); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_popover_set_modal(GTK_POPOVER(pWidget), false); +#else + gtk_popover_set_autohide(GTK_POPOVER(pWidget), false); +#endif + + gtk_widget_show(pLabel); + gtk_widget_show(pWidget); + + return pWidget; +} + +bool GtkSalFrame::UpdatePopover(void* nId, const OUString& rHelpText, vcl::Window* pParent, const tools::Rectangle& rHelpArea) +{ + GtkWidget *pWidget = static_cast(nId); + + set_pointing_to(GTK_POPOVER(pWidget), pParent, rHelpArea, maGeometry); + +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget *pLabel = gtk_bin_get_child(GTK_BIN(pWidget)); +#else + GtkWidget *pLabel = gtk_popover_get_child(GTK_POPOVER(pWidget)); +#endif + OString sUTF = OUStringToOString(rHelpText, RTL_TEXTENCODING_UTF8); + gtk_label_set_text(GTK_LABEL(pLabel), sUTF.getStr()); + + return true; +} + +bool GtkSalFrame::HidePopover(void* nId) +{ + GtkWidget *pWidget = static_cast(nId); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_destroy(pWidget); +#else + g_clear_pointer(&pWidget, gtk_widget_unparent); +#endif + return true; +} + +void GtkSalFrame::addGrabLevel() +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + if (m_nGrabLevel == 0) + gtk_grab_add(getMouseEventWidget()); +#endif + ++m_nGrabLevel; +} + +void GtkSalFrame::removeGrabLevel() +{ + if (m_nGrabLevel > 0) + { + --m_nGrabLevel; +#if !GTK_CHECK_VERSION(4, 0, 0) + if (m_nGrabLevel == 0) + gtk_grab_remove(getMouseEventWidget()); +#endif + } +} + +void GtkSalFrame::closePopup() +{ + if (!m_nFloats) + return; + ImplSVData* pSVData = ImplGetSVData(); + if (!pSVData->mpWinData->mpFirstFloat) + return; + if (pSVData->mpWinData->mpFirstFloat->ImplGetFrame() != this) + return; + pSVData->mpWinData->mpFirstFloat->EndPopupMode(FloatWinPopupEndFlags::Cancel | FloatWinPopupEndFlags::CloseAll); +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +namespace +{ + //tdf#117981 translate embedded video window mouse events to parent coordinates + void translate_coords(GdkWindow* pSourceWindow, GtkWidget* pTargetWidget, int& rEventX, int& rEventY) + { + gpointer user_data=nullptr; + gdk_window_get_user_data(pSourceWindow, &user_data); + GtkWidget* pRealEventWidget = static_cast(user_data); + if (pRealEventWidget) + { + gtk_coord fX(0.0), fY(0.0); + gtk_widget_translate_coordinates(pRealEventWidget, pTargetWidget, rEventX, rEventY, &fX, &fY); + rEventX = fX; + rEventY = fY; + } + } +} +#endif + +void GtkSalFrame::GrabFocus() +{ + GtkWidget* pGrabWidget; +#if !GTK_CHECK_VERSION(4, 0, 0) + if (GTK_IS_EVENT_BOX(m_pWindow)) + pGrabWidget = GTK_WIDGET(m_pWindow); + else + pGrabWidget = GTK_WIDGET(m_pFixedContainer); + // m_nSetFocusSignalId is 0 for the DisallowCycleFocusOut case where + // we don't allow focus to enter the toplevel, but expect it to + // stay in some embedded native gtk widget + if (!gtk_widget_get_can_focus(pGrabWidget) && m_nSetFocusSignalId) + gtk_widget_set_can_focus(pGrabWidget, true); +#else + pGrabWidget = GTK_WIDGET(m_pFixedContainer); +#endif + if (!gtk_widget_has_focus(pGrabWidget)) + { + gtk_widget_grab_focus(pGrabWidget); + if (m_pIMHandler) + m_pIMHandler->focusChanged(true); + } +} + +bool GtkSalFrame::DrawingAreaButton(SalEvent nEventType, int nEventX, int nEventY, int nButton, guint32 nTime, guint nState) +{ + UpdateLastInputEventTime(nTime); + + SalMouseEvent aEvent; + switch(nButton) + { + case 1: aEvent.mnButton = MOUSE_LEFT; break; + case 2: aEvent.mnButton = MOUSE_MIDDLE; break; + case 3: aEvent.mnButton = MOUSE_RIGHT; break; + default: return false; + } + + aEvent.mnTime = nTime; + aEvent.mnX = nEventX; + aEvent.mnY = nEventY; + aEvent.mnCode = GetMouseModCode(nState); + + if( AllSettings::GetLayoutRTL() ) + aEvent.mnX = maGeometry.width()-1-aEvent.mnX; + + CallCallbackExc(nEventType, &aEvent); + + return true; +} + +#if !GTK_CHECK_VERSION(4, 0, 0) + +void GtkSalFrame::UpdateGeometryFromEvent(int x_root, int y_root, int nEventX, int nEventY) +{ + //tdf#151509 don't overwrite geometry for system children + if (m_nStyle & SalFrameStyleFlags::SYSTEMCHILD) + return; + + int frame_x = x_root - nEventX; + int frame_y = y_root - nEventY; + if (m_bGeometryIsProvisional || frame_x != maGeometry.x() || frame_y != maGeometry.y()) + { + m_bGeometryIsProvisional = false; + maGeometry.setPos({ frame_x, frame_y }); + ImplSVData* pSVData = ImplGetSVData(); + if (pSVData->maNWFData.mbCanDetermineWindowPosition) + CallCallbackExc(SalEvent::Move, nullptr); + } +} + +gboolean GtkSalFrame::signalButton(GtkWidget*, GdkEventButton* pEvent, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + GtkWidget* pEventWidget = pThis->getMouseEventWidget(); + bool bDifferentEventWindow = pEvent->window != widget_get_surface(pEventWidget); + + if (pEvent->type == GDK_BUTTON_PRESS) + { + // tdf#120764 It isn't allowed under wayland to have two visible popups that share + // the same top level parent. The problem is that since gtk 3.24 tooltips are also + // implemented as popups, which means that we cannot show any popup if there is a + // visible tooltip. In fact, gtk will hide the tooltip by itself after this handler, + // in case of a button press event. But if we intend to show a popup on button press + // it will be too late, so just do it here: + pThis->HideTooltip(); + + // focus on click + if (!bDifferentEventWindow) + pThis->GrabFocus(); + } + + SalEvent nEventType = SalEvent::NONE; + switch( pEvent->type ) + { + case GDK_BUTTON_PRESS: + nEventType = SalEvent::MouseButtonDown; + break; + case GDK_BUTTON_RELEASE: + nEventType = SalEvent::MouseButtonUp; + break; + default: + return false; + } + + vcl::DeletionListener aDel( pThis ); + + if (pThis->isFloatGrabWindow()) + { + //rhbz#1505379 if the window that got the event isn't our one, or there's none + //of our windows under the mouse then close this popup window + if (bDifferentEventWindow || + gdk_device_get_window_at_position(pEvent->device, nullptr, nullptr) == nullptr) + { + if (pEvent->type == GDK_BUTTON_PRESS) + pThis->closePopup(); + else if (pEvent->type == GDK_BUTTON_RELEASE) + return true; + } + } + + int nEventX = pEvent->x; + int nEventY = pEvent->y; + + if (bDifferentEventWindow) + translate_coords(pEvent->window, pEventWidget, nEventX, nEventY); + + if (!aDel.isDeleted()) + pThis->UpdateGeometryFromEvent(pEvent->x_root, pEvent->y_root, nEventX, nEventY); + + bool bRet = false; + if (!aDel.isDeleted()) + { + bRet = pThis->DrawingAreaButton(nEventType, + nEventX, + nEventY, + pEvent->button, + pEvent->time, + pEvent->state); + } + + return bRet; +} +#else +void GtkSalFrame::gesturePressed(GtkGestureClick* pGesture, int /*n_press*/, gdouble x, gdouble y, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + pThis->gestureButton(pGesture, SalEvent::MouseButtonDown, x, y); +} + +void GtkSalFrame::gestureReleased(GtkGestureClick* pGesture, int /*n_press*/, gdouble x, gdouble y, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + pThis->gestureButton(pGesture, SalEvent::MouseButtonUp, x, y); +} + +void GtkSalFrame::gestureButton(GtkGestureClick* pGesture, SalEvent nEventType, gdouble x, gdouble y) +{ + GdkEvent* pEvent = gtk_event_controller_get_current_event(GTK_EVENT_CONTROLLER(pGesture)); + GdkModifierType eType = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(pGesture)); + int nButton = gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(pGesture)); + DrawingAreaButton(nEventType, x, y, nButton, gdk_event_get_time(pEvent), eType); +} +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) +void GtkSalFrame::LaunchAsyncScroll(GdkEvent const * pEvent) +{ + //if we don't match previous pending states, flush that queue now + if (!m_aPendingScrollEvents.empty() && pEvent->scroll.state != m_aPendingScrollEvents.back()->scroll.state) + { + m_aSmoothScrollIdle.Stop(); + m_aSmoothScrollIdle.Invoke(); + assert(m_aPendingScrollEvents.empty()); + } + //add scroll event to queue + m_aPendingScrollEvents.push_back(gdk_event_copy(pEvent)); + if (!m_aSmoothScrollIdle.IsActive()) + m_aSmoothScrollIdle.Start(); +} +#endif + +void GtkSalFrame::DrawingAreaScroll(double delta_x, double delta_y, int nEventX, int nEventY, guint32 nTime, guint nState) +{ + SalWheelMouseEvent aEvent; + + aEvent.mnTime = nTime; + aEvent.mnX = nEventX; + // --- RTL --- (mirror mouse pos) + if (AllSettings::GetLayoutRTL()) + aEvent.mnX = maGeometry.width() - 1 - aEvent.mnX; + aEvent.mnY = nEventY; + aEvent.mnCode = GetMouseModCode(nState); + + // rhbz#1344042 "Traditionally" in gtk3 we took a single up/down event as + // equating to 3 scroll lines and a delta of 120. So scale the delta here + // by 120 where a single mouse wheel click is an incoming delta_x of 1 + // and divide that by 40 to get the number of scroll lines + if (delta_x != 0.0) + { + aEvent.mnDelta = -delta_x * 120; + aEvent.mnNotchDelta = aEvent.mnDelta < 0 ? -1 : +1; + if (aEvent.mnDelta == 0) + aEvent.mnDelta = aEvent.mnNotchDelta; + aEvent.mbHorz = true; + aEvent.mnScrollLines = std::abs(aEvent.mnDelta) / 40.0; + CallCallbackExc(SalEvent::WheelMouse, &aEvent); + } + + if (delta_y != 0.0) + { + aEvent.mnDelta = -delta_y * 120; + aEvent.mnNotchDelta = aEvent.mnDelta < 0 ? -1 : +1; + if (aEvent.mnDelta == 0) + aEvent.mnDelta = aEvent.mnNotchDelta; + aEvent.mbHorz = false; + aEvent.mnScrollLines = std::abs(aEvent.mnDelta) / 40.0; + CallCallbackExc(SalEvent::WheelMouse, &aEvent); + } +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +IMPL_LINK_NOARG(GtkSalFrame, AsyncScroll, Timer *, void) +{ + assert(!m_aPendingScrollEvents.empty()); + + GdkEvent* pEvent = m_aPendingScrollEvents.back(); + auto nEventX = pEvent->scroll.x; + auto nEventY = pEvent->scroll.y; + auto nTime = pEvent->scroll.time; + auto nState = pEvent->scroll.state; + + double delta_x(0.0), delta_y(0.0); + for (auto pSubEvent : m_aPendingScrollEvents) + { + delta_x += pSubEvent->scroll.delta_x; + delta_y += pSubEvent->scroll.delta_y; + gdk_event_free(pSubEvent); + } + m_aPendingScrollEvents.clear(); + + DrawingAreaScroll(delta_x, delta_y, nEventX, nEventY, nTime, nState); +} +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) +SalWheelMouseEvent GtkSalFrame::GetWheelEvent(const GdkEventScroll& rEvent) +{ + SalWheelMouseEvent aEvent; + + aEvent.mnTime = rEvent.time; + aEvent.mnX = static_cast(rEvent.x); + aEvent.mnY = static_cast(rEvent.y); + aEvent.mnCode = GetMouseModCode(rEvent.state); + + switch (rEvent.direction) + { + case GDK_SCROLL_UP: + aEvent.mnDelta = 120; + aEvent.mnNotchDelta = 1; + aEvent.mnScrollLines = 3; + aEvent.mbHorz = false; + break; + + case GDK_SCROLL_DOWN: + aEvent.mnDelta = -120; + aEvent.mnNotchDelta = -1; + aEvent.mnScrollLines = 3; + aEvent.mbHorz = false; + break; + + case GDK_SCROLL_LEFT: + aEvent.mnDelta = 120; + aEvent.mnNotchDelta = 1; + aEvent.mnScrollLines = 3; + aEvent.mbHorz = true; + break; + + case GDK_SCROLL_RIGHT: + aEvent.mnDelta = -120; + aEvent.mnNotchDelta = -1; + aEvent.mnScrollLines = 3; + aEvent.mbHorz = true; + break; + default: + break; + } + + return aEvent; +} + +gboolean GtkSalFrame::signalScroll(GtkWidget*, GdkEvent* pInEvent, gpointer frame) +{ + GdkEventScroll& rEvent = pInEvent->scroll; + + UpdateLastInputEventTime(rEvent.time); + + GtkSalFrame* pThis = static_cast(frame); + + if (rEvent.direction == GDK_SCROLL_SMOOTH) + { + pThis->LaunchAsyncScroll(pInEvent); + return true; + } + + //if we have smooth scrolling previous pending states, flush that queue now + if (!pThis->m_aPendingScrollEvents.empty()) + { + pThis->m_aSmoothScrollIdle.Stop(); + pThis->m_aSmoothScrollIdle.Invoke(); + assert(pThis->m_aPendingScrollEvents.empty()); + } + + SalWheelMouseEvent aEvent(GetWheelEvent(rEvent)); + + // --- RTL --- (mirror mouse pos) + if (AllSettings::GetLayoutRTL()) + aEvent.mnX = pThis->maGeometry.width() - 1 - aEvent.mnX; + + pThis->CallCallbackExc(SalEvent::WheelMouse, &aEvent); + + return true; +} +#else +gboolean GtkSalFrame::signalScroll(GtkEventControllerScroll* pController, double delta_x, double delta_y, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + + GdkEvent* pEvent = gtk_event_controller_get_current_event(GTK_EVENT_CONTROLLER(pController)); + GdkModifierType eState = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(pController)); + + auto nTime = gdk_event_get_time(pEvent); + + UpdateLastInputEventTime(nTime); + + double nEventX(0.0), nEventY(0.0); + gdk_event_get_position(pEvent, &nEventX, &nEventY); + + pThis->DrawingAreaScroll(delta_x, delta_y, nEventX, nEventY, nTime, eState); + + return true; +} + +gboolean GtkSalFrame::event_controller_scroll_forward(GtkEventControllerScroll* pController, double delta_x, double delta_y) +{ + return GtkSalFrame::signalScroll(pController, delta_x, delta_y, this); +} + +#endif + +void GtkSalFrame::gestureSwipe(GtkGestureSwipe* gesture, gdouble velocity_x, gdouble velocity_y, gpointer frame) +{ + gdouble x, y; + GdkEventSequence *sequence = gtk_gesture_single_get_current_sequence(GTK_GESTURE_SINGLE(gesture)); + //I feel I want the first point of the sequence, not the last point which + //the docs say this gives, but for the moment assume we start and end + //within the same vcl window + if (gtk_gesture_get_point(GTK_GESTURE(gesture), sequence, &x, &y)) + { + SalGestureSwipeEvent aEvent; + aEvent.mnVelocityX = velocity_x; + aEvent.mnVelocityY = velocity_y; + aEvent.mnX = x; + aEvent.mnY = y; + + GtkSalFrame* pThis = static_cast(frame); + pThis->CallCallbackExc(SalEvent::GestureSwipe, &aEvent); + } +} + +void GtkSalFrame::gestureLongPress(GtkGestureLongPress* gesture, gdouble x, gdouble y, gpointer frame) +{ + GdkEventSequence *sequence = gtk_gesture_single_get_current_sequence(GTK_GESTURE_SINGLE(gesture)); + if (gtk_gesture_get_point(GTK_GESTURE(gesture), sequence, &x, &y)) + { + SalGestureLongPressEvent aEvent; + aEvent.mnX = x; + aEvent.mnY = y; + + GtkSalFrame* pThis = static_cast(frame); + pThis->CallCallbackExc(SalEvent::GestureLongPress, &aEvent); + } +} + +void GtkSalFrame::DrawingAreaMotion(int nEventX, int nEventY, guint32 nTime, guint nState) +{ + UpdateLastInputEventTime(nTime); + + SalMouseEvent aEvent; + aEvent.mnTime = nTime; + aEvent.mnX = nEventX; + aEvent.mnY = nEventY; + aEvent.mnCode = GetMouseModCode(nState); + aEvent.mnButton = 0; + + if( AllSettings::GetLayoutRTL() ) + aEvent.mnX = maGeometry.width() - 1 - aEvent.mnX; + + CallCallbackExc(SalEvent::MouseMove, &aEvent); +} + +#if GTK_CHECK_VERSION(4, 0, 0) +void GtkSalFrame::signalMotion(GtkEventControllerMotion *pController, double x, double y, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + GdkEvent* pEvent = gtk_event_controller_get_current_event(GTK_EVENT_CONTROLLER(pController)); + GdkModifierType eType = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(pController)); + pThis->DrawingAreaMotion(x, y, gdk_event_get_time(pEvent), eType); +} +#else +gboolean GtkSalFrame::signalMotion( GtkWidget*, GdkEventMotion* pEvent, gpointer frame ) +{ + GtkSalFrame* pThis = static_cast(frame); + GtkWidget* pEventWidget = pThis->getMouseEventWidget(); + bool bDifferentEventWindow = pEvent->window != widget_get_surface(pEventWidget); + + //If a menu, e.g. font name dropdown, is open, then under wayland moving the + //mouse in the top left corner of the toplevel window in a + //0,0,float-width,float-height area generates motion events which are + //delivered to the dropdown + if (pThis->isFloatGrabWindow() && bDifferentEventWindow) + return true; + + vcl::DeletionListener aDel( pThis ); + + int nEventX = pEvent->x; + int nEventY = pEvent->y; + + if (bDifferentEventWindow) + translate_coords(pEvent->window, pEventWidget, nEventX, nEventY); + + pThis->UpdateGeometryFromEvent(pEvent->x_root, pEvent->y_root, nEventX, nEventY); + + if (!aDel.isDeleted()) + pThis->DrawingAreaMotion(nEventX, nEventY, pEvent->time, pEvent->state); + + if (!aDel.isDeleted()) + { + // ask for the next hint + gint x, y; + GdkModifierType mask; + gdk_window_get_pointer( widget_get_surface(GTK_WIDGET(pThis->m_pWindow)), &x, &y, &mask ); + } + + return true; +} +#endif + +void GtkSalFrame::DrawingAreaCrossing(SalEvent nEventType, int nEventX, int nEventY, guint32 nTime, guint nState) +{ + UpdateLastInputEventTime(nTime); + + SalMouseEvent aEvent; + aEvent.mnTime = nTime; + aEvent.mnX = nEventX; + aEvent.mnY = nEventY; + aEvent.mnCode = GetMouseModCode(nState); + aEvent.mnButton = 0; + + if (AllSettings::GetLayoutRTL()) + aEvent.mnX = maGeometry.width()-1-aEvent.mnX; + + CallCallbackExc(nEventType, &aEvent); +} + +#if GTK_CHECK_VERSION(4, 0, 0) +void GtkSalFrame::signalEnter(GtkEventControllerMotion *pController, double x, double y, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + GdkEvent* pEvent = gtk_event_controller_get_current_event(GTK_EVENT_CONTROLLER(pController)); + GdkModifierType eType = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(pController)); + pThis->DrawingAreaCrossing(SalEvent::MouseMove, x, y, pEvent ? gdk_event_get_time(pEvent) : GDK_CURRENT_TIME, eType); +} + +void GtkSalFrame::signalLeave(GtkEventControllerMotion *pController, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + GdkEvent* pEvent = gtk_event_controller_get_current_event(GTK_EVENT_CONTROLLER(pController)); + GdkModifierType eType = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(pController)); + pThis->DrawingAreaCrossing(SalEvent::MouseLeave, -1, -1, pEvent ? gdk_event_get_time(pEvent) : GDK_CURRENT_TIME, eType); +} +#else +gboolean GtkSalFrame::signalCrossing( GtkWidget*, GdkEventCrossing* pEvent, gpointer frame ) +{ + GtkSalFrame* pThis = static_cast(frame); + pThis->DrawingAreaCrossing((pEvent->type == GDK_ENTER_NOTIFY) ? SalEvent::MouseMove : SalEvent::MouseLeave, + pEvent->x, + pEvent->y, + pEvent->time, + pEvent->state); + return true; +} +#endif + +cairo_t* GtkSalFrame::getCairoContext() const +{ + cairo_t* cr = cairo_create(m_pSurface); + assert(cr); + return cr; +} + +void GtkSalFrame::damaged(sal_Int32 nExtentsX, sal_Int32 nExtentsY, + sal_Int32 nExtentsWidth, sal_Int32 nExtentsHeight) const +{ +#if OSL_DEBUG_LEVEL > 0 + if (dumpframes) + { + static int frame; + OString tmp("/tmp/frame" + OString::number(frame++) + ".png"); + cairo_t* cr = getCairoContext(); + cairo_surface_write_to_png(cairo_get_target(cr), tmp.getStr()); + cairo_destroy(cr); + } +#endif + + // quite a bit of noise in RTL mode with negative widths + if (nExtentsWidth <= 0 || nExtentsHeight <= 0) + return; + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_queue_draw_area(GTK_WIDGET(m_pDrawingArea), + nExtentsX, nExtentsY, + nExtentsWidth, nExtentsHeight); +#else + gtk_widget_queue_draw(GTK_WIDGET(m_pDrawingArea)); + (void)nExtentsX; + (void)nExtentsY; +#endif +} + +// blit our backing cairo surface to the target cairo context +void GtkSalFrame::DrawingAreaDraw(cairo_t *cr) +{ + cairo_set_source_surface(cr, m_pSurface, 0, 0); + cairo_paint(cr); +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +gboolean GtkSalFrame::signalDraw(GtkWidget*, cairo_t *cr, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + pThis->DrawingAreaDraw(cr); + return false; +} +#else +void GtkSalFrame::signalDraw(GtkDrawingArea*, cairo_t *cr, int /*width*/, int /*height*/, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + pThis->DrawingAreaDraw(cr); +} +#endif + +void GtkSalFrame::DrawingAreaResized(GtkWidget* pWidget, int nWidth, int nHeight) +{ + // ignore size-allocations that occur during configuring an embedded SalObject + if (m_bSalObjectSetPosSize) + return; + maGeometry.setSize({ nWidth, nHeight }); + bool bRealized = gtk_widget_get_realized(pWidget); + if (bRealized) + AllocateFrame(); + CallCallbackExc( SalEvent::Resize, nullptr ); + if (bRealized) + TriggerPaintEvent(); +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +void GtkSalFrame::sizeAllocated(GtkWidget* pWidget, GdkRectangle *pAllocation, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + pThis->DrawingAreaResized(pWidget, pAllocation->width, pAllocation->height); +} +#else +void GtkSalFrame::sizeAllocated(GtkWidget* pWidget, int nWidth, int nHeight, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + pThis->DrawingAreaResized(pWidget, nWidth, nHeight); +} +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) +namespace { + +void swapDirection(GdkGravity& gravity) +{ + if (gravity == GDK_GRAVITY_NORTH_WEST) + gravity = GDK_GRAVITY_NORTH_EAST; + else if (gravity == GDK_GRAVITY_NORTH_EAST) + gravity = GDK_GRAVITY_NORTH_WEST; + else if (gravity == GDK_GRAVITY_SOUTH_WEST) + gravity = GDK_GRAVITY_SOUTH_EAST; + else if (gravity == GDK_GRAVITY_SOUTH_EAST) + gravity = GDK_GRAVITY_SOUTH_WEST; +} + +} +#endif + +void GtkSalFrame::signalRealize(GtkWidget*, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + pThis->AllocateFrame(); + if (pThis->m_bSalObjectSetPosSize) + return; + pThis->TriggerPaintEvent(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!pThis->m_bFloatPositioned) + return; + + static auto window_move_to_rect = reinterpret_cast( + dlsym(nullptr, "gdk_window_move_to_rect")); + if (!window_move_to_rect) + return; + + GdkGravity rect_anchor = GDK_GRAVITY_SOUTH_WEST, menu_anchor = GDK_GRAVITY_NORTH_WEST; + + if (pThis->m_nFloatFlags & FloatWinPopupFlags::Left) + { + rect_anchor = GDK_GRAVITY_NORTH_WEST; + menu_anchor = GDK_GRAVITY_NORTH_EAST; + } + else if (pThis->m_nFloatFlags & FloatWinPopupFlags::Up) + { + rect_anchor = GDK_GRAVITY_NORTH_WEST; + menu_anchor = GDK_GRAVITY_SOUTH_WEST; + } + else if (pThis->m_nFloatFlags & FloatWinPopupFlags::Right) + { + rect_anchor = GDK_GRAVITY_NORTH_EAST; + } + + VclPtr pVclParent = pThis->GetWindow()->GetParent(); + if (pVclParent->GetOutDev()->HasMirroredGraphics() && pVclParent->IsRTLEnabled()) + { + swapDirection(rect_anchor); + swapDirection(menu_anchor); + } + + AbsoluteScreenPixelRectangle aFloatRect = FloatingWindow::ImplConvertToAbsPos(pVclParent, pThis->m_aFloatRect); + if (gdk_window_get_window_type(widget_get_surface(pThis->m_pParent->m_pWindow)) != GDK_WINDOW_TOPLEVEL) + { + // See tdf#152155 for an example + gtk_coord nX(0), nY(0.0); + gtk_widget_translate_coordinates(pThis->m_pParent->m_pWindow, widget_get_toplevel(pThis->m_pParent->m_pWindow), 0, 0, &nX, &nY); + aFloatRect.Move(nX, nY); + } + + GdkRectangle rect {static_cast(aFloatRect.Left()), static_cast(aFloatRect.Top()), + static_cast(aFloatRect.GetWidth()), static_cast(aFloatRect.GetHeight())}; + + GdkSurface* gdkWindow = widget_get_surface(pThis->m_pWindow); + window_move_to_rect(gdkWindow, &rect, rect_anchor, menu_anchor, static_cast(GDK_ANCHOR_FLIP | GDK_ANCHOR_SLIDE), 0, 0); +#endif +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +gboolean GtkSalFrame::signalConfigure(GtkWidget*, GdkEventConfigure* pEvent, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + + bool bMoved = false; + int x = pEvent->x, y = pEvent->y; + + /* #i31785# claims we cannot trust the x,y members of the event; + * they are e.g. not set correctly on maximize/demaximize; + * yet the gdkdisplay-x11.c code handling configure_events has + * done this XTranslateCoordinates work since the day ~zero. + */ + if (pThis->m_bGeometryIsProvisional || x != pThis->maGeometry.x() || y != pThis->maGeometry.y() ) + { + bMoved = true; + pThis->m_bGeometryIsProvisional = false; + pThis->maGeometry.setPos({ x, y }); + } + + // update decoration hints + GdkRectangle aRect; + gdk_window_get_frame_extents( widget_get_surface(GTK_WIDGET(pThis->m_pWindow)), &aRect ); + pThis->maGeometry.setTopDecoration(y - aRect.y); + pThis->maGeometry.setBottomDecoration(aRect.y + aRect.height - y - pEvent->height); + pThis->maGeometry.setLeftDecoration(x - aRect.x); + pThis->maGeometry.setRightDecoration(aRect.x + aRect.width - x - pEvent->width); + pThis->updateScreenNumber(); + + if (bMoved) + { + ImplSVData* pSVData = ImplGetSVData(); + if (pSVData->maNWFData.mbCanDetermineWindowPosition) + pThis->CallCallbackExc(SalEvent::Move, nullptr); + } + + return false; +} +#endif + +void GtkSalFrame::queue_draw() +{ + gtk_widget_queue_draw(GTK_WIDGET(m_pDrawingArea)); +} + +void GtkSalFrame::TriggerPaintEvent() +{ + //Under gtk2 we can basically paint directly into the XWindow and on + //additional "expose-event" events we can re-render the missing pieces + // + //Under gtk3 we have to keep our own buffer up to date and flush it into + //the given cairo context on "draw". So we emit a paint event on + //opportune resize trigger events to initially fill our backbuffer and then + //keep it up to date with our direct paints and tell gtk those regions + //have changed and then blit them into the provided cairo context when + //we get the "draw" + // + //The other alternative was to always paint everything on "draw", but + //that duplicates the amount of drawing and is hideously slow + SAL_INFO("vcl.gtk3", "force painting" << 0 << "," << 0 << " " << maGeometry.width() << "x" << maGeometry.height()); + SalPaintEvent aPaintEvt(0, 0, maGeometry.width(), maGeometry.height(), true); + CallCallbackExc(SalEvent::Paint, &aPaintEvt); + queue_draw(); +} + +void GtkSalFrame::DrawingAreaFocusInOut(SalEvent nEventType) +{ + SalGenericInstance* pSalInstance = GetGenericInstance(); + + // check if printers have changed (analogous to salframe focus handler) + pSalInstance->updatePrinterUpdate(); + + if (nEventType == SalEvent::LoseFocus) + m_nKeyModifiers = ModKeyFlags::NONE; + + if (m_pIMHandler) + { + bool bFocusInAnotherGtkWidget = false; + if (GTK_IS_WINDOW(m_pWindow)) + { + GtkWidget* pFocusWindow = gtk_window_get_focus(GTK_WINDOW(m_pWindow)); + bFocusInAnotherGtkWidget = pFocusWindow && pFocusWindow != GTK_WIDGET(m_pFixedContainer); + } + if (!bFocusInAnotherGtkWidget) + m_pIMHandler->focusChanged(nEventType == SalEvent::GetFocus); + } + + // ask for changed printers like generic implementation + if (nEventType == SalEvent::GetFocus && pSalInstance->isPrinterInit()) + pSalInstance->updatePrinterUpdate(); + + CallCallbackExc(nEventType, nullptr); +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +gboolean GtkSalFrame::signalFocus( GtkWidget*, GdkEventFocus* pEvent, gpointer frame ) +{ + GtkSalFrame* pThis = static_cast(frame); + + SalGenericInstance *pSalInstance = GetGenericInstance(); + + // check if printers have changed (analogous to salframe focus handler) + pSalInstance->updatePrinterUpdate(); + + if( !pEvent->in ) + pThis->m_nKeyModifiers = ModKeyFlags::NONE; + + if( pThis->m_pIMHandler ) + { + bool bFocusInAnotherGtkWidget = false; + if (GTK_IS_WINDOW(pThis->m_pWindow)) + { + GtkWidget* pFocusWindow = gtk_window_get_focus(GTK_WINDOW(pThis->m_pWindow)); + bFocusInAnotherGtkWidget = pFocusWindow && pFocusWindow != GTK_WIDGET(pThis->m_pFixedContainer); + } + if (!bFocusInAnotherGtkWidget) + pThis->m_pIMHandler->focusChanged( pEvent->in != 0 ); + } + + // ask for changed printers like generic implementation + if( pEvent->in && pSalInstance->isPrinterInit() ) + pSalInstance->updatePrinterUpdate(); + + // FIXME: find out who the hell steals the focus from our frame + // while we have the pointer grabbed, this should not come from + // the window manager. Is this an event that was still queued ? + // The focus does not seem to get set inside our process + // in the meantime do not propagate focus get/lose if floats are open + if( m_nFloats == 0 ) + { + GtkWidget* pGrabWidget; + if (GTK_IS_EVENT_BOX(pThis->m_pWindow)) + pGrabWidget = GTK_WIDGET(pThis->m_pWindow); + else + pGrabWidget = GTK_WIDGET(pThis->m_pFixedContainer); + bool bHasFocus = gtk_widget_has_focus(pGrabWidget); + pThis->CallCallbackExc(bHasFocus ? SalEvent::GetFocus : SalEvent::LoseFocus, nullptr); + } + + return false; +} +#else +void GtkSalFrame::signalFocusEnter(GtkEventControllerFocus*, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + pThis->DrawingAreaFocusInOut(SalEvent::GetFocus); +} + +void GtkSalFrame::signalFocusLeave(GtkEventControllerFocus*, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + pThis->DrawingAreaFocusInOut(SalEvent::LoseFocus); +} +#endif + +// change of focus between native widgets within the toplevel +#if !GTK_CHECK_VERSION(4, 0, 0) +void GtkSalFrame::signalSetFocus(GtkWindow*, GtkWidget* pWidget, gpointer frame) +#else +void GtkSalFrame::signalSetFocus(GtkWindow*, GParamSpec*, gpointer frame) +#endif +{ + GtkSalFrame* pThis = static_cast(frame); + + GtkWidget* pGrabWidget = GTK_WIDGET(pThis->m_pFixedContainer); + + GtkWidget* pTopLevel = widget_get_toplevel(pGrabWidget); + // see commentary in GtkSalObjectWidgetClip::Show + if (pTopLevel && g_object_get_data(G_OBJECT(pTopLevel), "g-lo-BlockFocusChange")) + return; + +#if GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pWidget = gtk_window_get_focus(GTK_WINDOW(pThis->m_pWindow)); +#endif + + // tdf#129634 interpret losing focus as focus passing explicitly to another widget + bool bLoseFocus = pWidget && pWidget != pGrabWidget; + + // do not propagate focus get/lose if floats are open + pThis->CallCallbackExc(bLoseFocus ? SalEvent::LoseFocus : SalEvent::GetFocus, nullptr); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_can_focus(GTK_WIDGET(pThis->m_pFixedContainer), !bLoseFocus); +#endif +} + +void GtkSalFrame::WindowMap() +{ + if (m_bIconSetWhileUnmapped) + SetIcon(gtk_window_get_icon_name(GTK_WINDOW(m_pWindow))); + + CallCallbackExc( SalEvent::Resize, nullptr ); + TriggerPaintEvent(); +} + +void GtkSalFrame::WindowUnmap() +{ + CallCallbackExc( SalEvent::Resize, nullptr ); + + if (m_bFloatPositioned) + { + // Unrealize is needed for cases where we reuse the same popup + // (e.g. the font name control), making the realize signal fire + // again on next show. + gtk_widget_unrealize(m_pWindow); + m_bFloatPositioned = false; + } +} + +#if GTK_CHECK_VERSION(4, 0, 0) +void GtkSalFrame::signalMap(GtkWidget*, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + pThis->WindowMap(); +} + +void GtkSalFrame::signalUnmap(GtkWidget*, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + pThis->WindowUnmap(); +} +#else +gboolean GtkSalFrame::signalMap(GtkWidget*, GdkEvent*, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + pThis->WindowMap(); + return false; +} + +gboolean GtkSalFrame::signalUnmap(GtkWidget*, GdkEvent*, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + pThis->WindowUnmap(); + return false; +} +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + +static bool key_forward(GdkEventKey* pEvent, GtkWindow* pDest) +{ + gpointer pClass = g_type_class_ref(GTK_TYPE_WINDOW); + GtkWidgetClass* pWindowClass = GTK_WIDGET_CLASS(pClass); + bool bHandled = pEvent->type == GDK_KEY_PRESS + ? pWindowClass->key_press_event(GTK_WIDGET(pDest), pEvent) + : pWindowClass->key_release_event(GTK_WIDGET(pDest), pEvent); + g_type_class_unref(pClass); + return bHandled; +} + +static bool activate_menubar_mnemonic(GtkWidget* pWidget, guint nKeyval) +{ + const char* pLabel = gtk_menu_item_get_label(GTK_MENU_ITEM(pWidget)); + gunichar cAccelChar = 0; + if (!pango_parse_markup(pLabel, -1, '_', nullptr, nullptr, &cAccelChar, nullptr)) + return false; + if (!cAccelChar) + return false; + auto nMnemonicKeyval = gdk_keyval_to_lower(gdk_unicode_to_keyval(cAccelChar)); + if (nKeyval == nMnemonicKeyval) + return gtk_widget_mnemonic_activate(pWidget, false); + return false; +} + +bool GtkSalFrame::HandleMenubarMnemonic(guint eState, guint nKeyval) +{ + bool bUsedInMenuBar = false; + if (eState & GDK_ALT_MASK) + { + if (GtkWidget* pMenuBar = m_pSalMenu ? m_pSalMenu->GetMenuBarWidget() : nullptr) + { + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(pMenuBar)); + for (GList* pChild = g_list_first(pChildren); pChild; pChild = g_list_next(pChild)) + { + bUsedInMenuBar = activate_menubar_mnemonic(static_cast(pChild->data), nKeyval); + if (bUsedInMenuBar) + break; + } + g_list_free(pChildren); + } + } + return bUsedInMenuBar; +} + +gboolean GtkSalFrame::signalKey(GtkWidget* pWidget, GdkEventKey* pEvent, gpointer frame) +{ + UpdateLastInputEventTime(pEvent->time); + + GtkSalFrame* pThis = static_cast(frame); + + bool bFocusInAnotherGtkWidget = false; + + VclPtr xTopLevelInterimWindow; + + if (GTK_IS_WINDOW(pThis->m_pWindow)) + { + GtkWidget* pFocusWindow = gtk_window_get_focus(GTK_WINDOW(pThis->m_pWindow)); + bFocusInAnotherGtkWidget = pFocusWindow && pFocusWindow != GTK_WIDGET(pThis->m_pFixedContainer); + if (bFocusInAnotherGtkWidget) + { + if (!gtk_widget_get_realized(pFocusWindow)) + return true; + + // if the focus is not in our main widget, see if there is a handler + // for this key stroke in GtkWindow first + if (key_forward(pEvent, GTK_WINDOW(pThis->m_pWindow))) + return true; + + // Is focus inside an InterimItemWindow? In which case find that + // InterimItemWindow and send unconsumed keystrokes to it to + // support ctrl-q etc shortcuts. Only bother to search for the + // InterimItemWindow if it is a toplevel that fills its frame, or + // the keystroke is sufficiently special its worth passing on, + // e.g. F6 to switch between task-panels or F5 to close a navigator + if (pThis->IsCycleFocusOutDisallowed() || IsFunctionKeyVal(pEvent->keyval)) + { + GtkWidget* pSearch = pFocusWindow; + while (pSearch) + { + void* pData = g_object_get_data(G_OBJECT(pSearch), "InterimWindowGlue"); + if (pData) + { + xTopLevelInterimWindow = static_cast(pData); + break; + } + pSearch = gtk_widget_get_parent(pSearch); + } + } + } + } + + if (pThis->isFloatGrabWindow()) + return signalKey(pWidget, pEvent, pThis->m_pParent); + + vcl::DeletionListener aDel( pThis ); + + if (!bFocusInAnotherGtkWidget && pThis->m_pIMHandler && pThis->m_pIMHandler->handleKeyEvent(pEvent)) + return true; + + bool bStopProcessingKey = false; + + // handle modifiers + if( pEvent->keyval == GDK_KEY_Shift_L || pEvent->keyval == GDK_KEY_Shift_R || + pEvent->keyval == GDK_KEY_Control_L || pEvent->keyval == GDK_KEY_Control_R || + pEvent->keyval == GDK_KEY_Alt_L || pEvent->keyval == GDK_KEY_Alt_R || + pEvent->keyval == GDK_KEY_Meta_L || pEvent->keyval == GDK_KEY_Meta_R || + pEvent->keyval == GDK_KEY_Super_L || pEvent->keyval == GDK_KEY_Super_R ) + { + sal_uInt16 nModCode = GetKeyModCode( pEvent->state ); + ModKeyFlags nExtModMask = ModKeyFlags::NONE; + sal_uInt16 nModMask = 0; + // pressing just the ctrl key leads to a keysym of XK_Control but + // the event state does not contain ControlMask. In the release + // event it's the other way round: it does contain the Control mask. + // The modifier mode therefore has to be adapted manually. + switch( pEvent->keyval ) + { + case GDK_KEY_Control_L: + nExtModMask = ModKeyFlags::LeftMod1; + nModMask = KEY_MOD1; + break; + case GDK_KEY_Control_R: + nExtModMask = ModKeyFlags::RightMod1; + nModMask = KEY_MOD1; + break; + case GDK_KEY_Alt_L: + nExtModMask = ModKeyFlags::LeftMod2; + nModMask = KEY_MOD2; + break; + case GDK_KEY_Alt_R: + nExtModMask = ModKeyFlags::RightMod2; + nModMask = KEY_MOD2; + break; + case GDK_KEY_Shift_L: + nExtModMask = ModKeyFlags::LeftShift; + nModMask = KEY_SHIFT; + break; + case GDK_KEY_Shift_R: + nExtModMask = ModKeyFlags::RightShift; + nModMask = KEY_SHIFT; + break; + // Map Meta/Super to MOD3 modifier on all Unix systems + // except macOS + case GDK_KEY_Meta_L: + case GDK_KEY_Super_L: + nExtModMask = ModKeyFlags::LeftMod3; + nModMask = KEY_MOD3; + break; + case GDK_KEY_Meta_R: + case GDK_KEY_Super_R: + nExtModMask = ModKeyFlags::RightMod3; + nModMask = KEY_MOD3; + break; + } + + SalKeyModEvent aModEvt; + aModEvt.mbDown = pEvent->type == GDK_KEY_PRESS; + + if( pEvent->type == GDK_KEY_RELEASE ) + { + aModEvt.mnModKeyCode = pThis->m_nKeyModifiers; + aModEvt.mnCode = nModCode & ~nModMask; + pThis->m_nKeyModifiers &= ~nExtModMask; + } + else + { + aModEvt.mnCode = nModCode | nModMask; + pThis->m_nKeyModifiers |= nExtModMask; + aModEvt.mnModKeyCode = pThis->m_nKeyModifiers; + } + + pThis->CallCallbackExc( SalEvent::KeyModChange, &aModEvt ); + } + else + { + bool bRestoreDisallowCycleFocusOut = false; + + VclPtr xOrigFrameFocusWin; + VclPtr xOrigFocusWin; + if (xTopLevelInterimWindow) + { + // Focus is inside an InterimItemWindow so send unconsumed + // keystrokes to by setting it as the mpFocusWin + VclPtr xVclWindow = pThis->GetWindow(); + ImplFrameData* pFrameData = xVclWindow->ImplGetWindowImpl()->mpFrameData; + xOrigFrameFocusWin = pFrameData->mpFocusWin; + pFrameData->mpFocusWin = xTopLevelInterimWindow; + + ImplSVData* pSVData = ImplGetSVData(); + xOrigFocusWin = pSVData->mpWinData->mpFocusWin; + pSVData->mpWinData->mpFocusWin = xTopLevelInterimWindow; + + if (pEvent->keyval == GDK_KEY_F6 && pThis->IsCycleFocusOutDisallowed()) + { + // For F6, allow the focus to leave the InterimItemWindow + pThis->AllowCycleFocusOut(); + bRestoreDisallowCycleFocusOut = true; + } + } + + bStopProcessingKey = pThis->doKeyCallback(pEvent->state, + pEvent->keyval, + pEvent->hardware_keycode, + pEvent->group, + sal_Unicode(gdk_keyval_to_unicode( pEvent->keyval )), + (pEvent->type == GDK_KEY_PRESS), + false); + + // tdf#144846 If this is registered as a menubar mnemonic then ensure + // that any other widget won't be considered as a candidate by taking + // over the task of launch the menubar menu outself + // The code was moved here from its original position at beginning + // of this function in order to resolve tdf#146174. + if (!bStopProcessingKey && // module key handler did not process key + pEvent->type == GDK_KEY_PRESS && // module key handler handles only GDK_KEY_PRESS + GTK_IS_WINDOW(pThis->m_pWindow) && + pThis->HandleMenubarMnemonic(pEvent->state, pEvent->keyval)) + { + return true; + } + + if (!aDel.isDeleted()) + { + pThis->m_nKeyModifiers = ModKeyFlags::NONE; + + if (xTopLevelInterimWindow) + { + // Focus was inside an InterimItemWindow, restore the original + // focus win, unless the focus was changed away from the + // InterimItemWindow which should only be possible with F6 + VclPtr xVclWindow = pThis->GetWindow(); + ImplFrameData* pFrameData = xVclWindow->ImplGetWindowImpl()->mpFrameData; + if (pFrameData->mpFocusWin == xTopLevelInterimWindow) + pFrameData->mpFocusWin = xOrigFrameFocusWin; + + ImplSVData* pSVData = ImplGetSVData(); + if (pSVData->mpWinData->mpFocusWin == xTopLevelInterimWindow) + pSVData->mpWinData->mpFocusWin = xOrigFocusWin; + + if (bRestoreDisallowCycleFocusOut) + { + // undo the above AllowCycleFocusOut for F6 + pThis->DisallowCycleFocusOut(); + } + } + } + + } + + if (!bFocusInAnotherGtkWidget && !aDel.isDeleted() && pThis->m_pIMHandler) + pThis->m_pIMHandler->updateIMSpotLocation(); + + return bStopProcessingKey; +} +#else + +bool GtkSalFrame::DrawingAreaKey(GtkEventControllerKey* pController, SalEvent nEventType, guint keyval, guint keycode, guint state) +{ + guint32 nTime = gdk_event_get_time(gtk_event_controller_get_current_event(GTK_EVENT_CONTROLLER(pController))); + UpdateLastInputEventTime(nTime); + + bool bFocusInAnotherGtkWidget = false; + + VclPtr xTopLevelInterimWindow; + + if (GTK_IS_WINDOW(m_pWindow)) + { + GtkWidget* pFocusWindow = gtk_window_get_focus(GTK_WINDOW(m_pWindow)); + bFocusInAnotherGtkWidget = pFocusWindow && pFocusWindow != GTK_WIDGET(m_pFixedContainer); + if (bFocusInAnotherGtkWidget) + { + if (!gtk_widget_get_realized(pFocusWindow)) + return true; + // if the focus is not in our main widget, see if there is a handler + // for this key stroke in GtkWindow first + bool bHandled = gtk_event_controller_key_forward(pController, m_pWindow); + if (bHandled) + return true; + + // Is focus inside an InterimItemWindow? In which case find that + // InterimItemWindow and send unconsumed keystrokes to it to + // support ctrl-q etc shortcuts. Only bother to search for the + // InterimItemWindow if it is a toplevel that fills its frame, or + // the keystroke is sufficiently special its worth passing on, + // e.g. F6 to switch between task-panels or F5 to close a navigator + if (IsCycleFocusOutDisallowed() || IsFunctionKeyVal(keyval)) + { + GtkWidget* pSearch = pFocusWindow; + while (pSearch) + { + void* pData = g_object_get_data(G_OBJECT(pSearch), "InterimWindowGlue"); + if (pData) + { + xTopLevelInterimWindow = static_cast(pData); + break; + } + pSearch = gtk_widget_get_parent(pSearch); + } + } + } + } + + vcl::DeletionListener aDel(this); + + bool bStopProcessingKey = false; + + // handle modifiers + if( keyval == GDK_KEY_Shift_L || keyval == GDK_KEY_Shift_R || + keyval == GDK_KEY_Control_L || keyval == GDK_KEY_Control_R || + keyval == GDK_KEY_Alt_L || keyval == GDK_KEY_Alt_R || + keyval == GDK_KEY_Meta_L || keyval == GDK_KEY_Meta_R || + keyval == GDK_KEY_Super_L || keyval == GDK_KEY_Super_R ) + { + sal_uInt16 nModCode = GetKeyModCode(state); + ModKeyFlags nExtModMask = ModKeyFlags::NONE; + sal_uInt16 nModMask = 0; + // pressing just the ctrl key leads to a keysym of XK_Control but + // the event state does not contain ControlMask. In the release + // event it's the other way round: it does contain the Control mask. + // The modifier mode therefore has to be adapted manually. + switch (keyval) + { + case GDK_KEY_Control_L: + nExtModMask = ModKeyFlags::LeftMod1; + nModMask = KEY_MOD1; + break; + case GDK_KEY_Control_R: + nExtModMask = ModKeyFlags::RightMod1; + nModMask = KEY_MOD1; + break; + case GDK_KEY_Alt_L: + nExtModMask = ModKeyFlags::LeftMod2; + nModMask = KEY_MOD2; + break; + case GDK_KEY_Alt_R: + nExtModMask = ModKeyFlags::RightMod2; + nModMask = KEY_MOD2; + break; + case GDK_KEY_Shift_L: + nExtModMask = ModKeyFlags::LeftShift; + nModMask = KEY_SHIFT; + break; + case GDK_KEY_Shift_R: + nExtModMask = ModKeyFlags::RightShift; + nModMask = KEY_SHIFT; + break; + // Map Meta/Super to MOD3 modifier on all Unix systems + // except macOS + case GDK_KEY_Meta_L: + case GDK_KEY_Super_L: + nExtModMask = ModKeyFlags::LeftMod3; + nModMask = KEY_MOD3; + break; + case GDK_KEY_Meta_R: + case GDK_KEY_Super_R: + nExtModMask = ModKeyFlags::RightMod3; + nModMask = KEY_MOD3; + break; + } + + SalKeyModEvent aModEvt; + aModEvt.mbDown = nEventType == SalEvent::KeyInput; + + if (!aModEvt.mbDown) + { + aModEvt.mnModKeyCode = m_nKeyModifiers; + aModEvt.mnCode = nModCode & ~nModMask; + m_nKeyModifiers &= ~nExtModMask; + } + else + { + aModEvt.mnCode = nModCode | nModMask; + m_nKeyModifiers |= nExtModMask; + aModEvt.mnModKeyCode = m_nKeyModifiers; + } + + CallCallbackExc(SalEvent::KeyModChange, &aModEvt); + } + else + { + bool bRestoreDisallowCycleFocusOut = false; + + VclPtr xOrigFrameFocusWin; + VclPtr xOrigFocusWin; + if (xTopLevelInterimWindow) + { + // Focus is inside an InterimItemWindow so send unconsumed + // keystrokes to by setting it as the mpFocusWin + VclPtr xVclWindow = GetWindow(); + ImplFrameData* pFrameData = xVclWindow->ImplGetWindowImpl()->mpFrameData; + xOrigFrameFocusWin = pFrameData->mpFocusWin; + pFrameData->mpFocusWin = xTopLevelInterimWindow; + + ImplSVData* pSVData = ImplGetSVData(); + xOrigFocusWin = pSVData->mpWinData->mpFocusWin; + pSVData->mpWinData->mpFocusWin = xTopLevelInterimWindow; + + if (keyval == GDK_KEY_F6 && IsCycleFocusOutDisallowed()) + { + // For F6, allow the focus to leave the InterimItemWindow + AllowCycleFocusOut(); + bRestoreDisallowCycleFocusOut = true; + } + } + + + bStopProcessingKey = doKeyCallback(state, + keyval, + keycode, + 0, // group + sal_Unicode(gdk_keyval_to_unicode(keyval)), + nEventType == SalEvent::KeyInput, + false); + + if (!aDel.isDeleted()) + { + m_nKeyModifiers = ModKeyFlags::NONE; + + if (xTopLevelInterimWindow) + { + // Focus was inside an InterimItemWindow, restore the original + // focus win, unless the focus was changed away from the + // InterimItemWindow which should only be possible with F6 + VclPtr xVclWindow = GetWindow(); + ImplFrameData* pFrameData = xVclWindow->ImplGetWindowImpl()->mpFrameData; + if (pFrameData->mpFocusWin == xTopLevelInterimWindow) + pFrameData->mpFocusWin = xOrigFrameFocusWin; + + ImplSVData* pSVData = ImplGetSVData(); + if (pSVData->mpWinData->mpFocusWin == xTopLevelInterimWindow) + pSVData->mpWinData->mpFocusWin = xOrigFocusWin; + + if (bRestoreDisallowCycleFocusOut) + { + // undo the above AllowCycleFocusOut for F6 + DisallowCycleFocusOut(); + } + } + } + } + + if (m_pIMHandler) + m_pIMHandler->updateIMSpotLocation(); + + return bStopProcessingKey; +} + +gboolean GtkSalFrame::signalKeyPressed(GtkEventControllerKey* pController, guint keyval, guint keycode, GdkModifierType state, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + return pThis->DrawingAreaKey(pController, SalEvent::KeyInput, keyval, keycode, state); +} + +gboolean GtkSalFrame::signalKeyReleased(GtkEventControllerKey* pController, guint keyval, guint keycode, GdkModifierType state, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + return pThis->DrawingAreaKey(pController, SalEvent::KeyUp, keyval, keycode, state); +} +#endif + +bool GtkSalFrame::WindowCloseRequest() +{ + CallCallbackExc(SalEvent::Close, nullptr); + return true; +} + +#if GTK_CHECK_VERSION(4, 0, 0) +gboolean GtkSalFrame::signalDelete(GtkWidget*, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + return pThis->WindowCloseRequest(); +} +#else +gboolean GtkSalFrame::signalDelete(GtkWidget*, GdkEvent*, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + return pThis->WindowCloseRequest(); +} +#endif + +const cairo_font_options_t* GtkSalFrame::get_font_options() +{ + GtkWidget* pWidget = getMouseEventWidget(); +#if GTK_CHECK_VERSION(4, 0, 0) + PangoContext* pContext = gtk_widget_get_pango_context(pWidget); + assert(pContext); + return pango_cairo_context_get_font_options(pContext); +#else + return gdk_screen_get_font_options(gtk_widget_get_screen(pWidget)); +#endif +} + +#if GTK_CHECK_VERSION(4, 0, 0) +void GtkSalFrame::signalStyleUpdated(GtkWidget*, const gchar* /*pSetting*/, gpointer frame) +#else +void GtkSalFrame::signalStyleUpdated(GtkWidget*, gpointer frame) +#endif +{ + GtkSalFrame* pThis = static_cast(frame); + + // note: settings changed for multiple frames is avoided in winproc.cxx ImplHandleSettings + GtkSalFrame::getDisplay()->SendInternalEvent( pThis, nullptr, SalEvent::SettingsChanged ); + + // a plausible alternative might be to send SalEvent::FontChanged if pSetting starts with "gtk-xft" + + // fire off font-changed when the system cairo font hints change + GtkInstance *pInstance = GetGtkInstance(); + const cairo_font_options_t* pLastCairoFontOptions = pInstance->GetLastSeenCairoFontOptions(); + const cairo_font_options_t* pCurrentCairoFontOptions = pThis->get_font_options(); + bool bFontSettingsChanged = true; + if (pLastCairoFontOptions && pCurrentCairoFontOptions) + bFontSettingsChanged = !cairo_font_options_equal(pLastCairoFontOptions, pCurrentCairoFontOptions); + else if (!pLastCairoFontOptions && !pCurrentCairoFontOptions) + bFontSettingsChanged = false; + if (bFontSettingsChanged) + { + pInstance->ResetLastSeenCairoFontOptions(pCurrentCairoFontOptions); + GtkSalFrame::getDisplay()->SendInternalEvent( pThis, nullptr, SalEvent::FontChanged ); + } +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +gboolean GtkSalFrame::signalWindowState( GtkWidget*, GdkEvent* pEvent, gpointer frame ) +{ + GtkSalFrame* pThis = static_cast(frame); + if( (pThis->m_nState & GDK_TOPLEVEL_STATE_MINIMIZED) != (pEvent->window_state.new_window_state & GDK_TOPLEVEL_STATE_MINIMIZED) ) + { + GtkSalFrame::getDisplay()->SendInternalEvent( pThis, nullptr, SalEvent::Resize ); + pThis->TriggerPaintEvent(); + } + + if ((pEvent->window_state.new_window_state & GDK_TOPLEVEL_STATE_MAXIMIZED) && + !(pThis->m_nState & GDK_TOPLEVEL_STATE_MAXIMIZED)) + { + pThis->m_aRestorePosSize = GetPosAndSize(GTK_WINDOW(pThis->m_pWindow)); + } + + if ((pEvent->window_state.new_window_state & GDK_WINDOW_STATE_WITHDRAWN) && + !(pThis->m_nState & GDK_WINDOW_STATE_WITHDRAWN)) + { + if (pThis->isFloatGrabWindow()) + pThis->closePopup(); + } + + pThis->m_nState = pEvent->window_state.new_window_state; + + return false; +} +#else +void GtkSalFrame::signalWindowState(GdkToplevel* pSurface, GParamSpec*, gpointer frame) +{ + GdkToplevelState eNewWindowState = gdk_toplevel_get_state(pSurface); + + GtkSalFrame* pThis = static_cast(frame); + if( (pThis->m_nState & GDK_TOPLEVEL_STATE_MINIMIZED) != (eNewWindowState & GDK_TOPLEVEL_STATE_MINIMIZED) ) + { + GtkSalFrame::getDisplay()->SendInternalEvent( pThis, nullptr, SalEvent::Resize ); + pThis->TriggerPaintEvent(); + } + + if ((eNewWindowState & GDK_TOPLEVEL_STATE_MAXIMIZED) && + !(pThis->m_nState & GDK_TOPLEVEL_STATE_MAXIMIZED)) + { + pThis->m_aRestorePosSize = GetPosAndSize(GTK_WINDOW(pThis->m_pWindow)); + } + + pThis->m_nState = eNewWindowState; +} +#endif + +namespace +{ + bool handleSignalZoom(GtkGesture* gesture, GdkEventSequence* sequence, gpointer frame, + GestureEventZoomType eEventType) + { + gdouble x = 0; + gdouble y = 0; + gtk_gesture_get_point(gesture, sequence, &x, &y); + + SalGestureZoomEvent aEvent; + aEvent.meEventType = eEventType; + aEvent.mnX = x; + aEvent.mnY = y; + aEvent.mfScaleDelta = gtk_gesture_zoom_get_scale_delta(GTK_GESTURE_ZOOM(gesture)); + GtkSalFrame* pThis = static_cast(frame); + pThis->CallCallbackExc(SalEvent::GestureZoom, &aEvent); + return true; + } + + bool handleSignalRotate(GtkGesture* gesture, GdkEventSequence* sequence, gpointer frame, + GestureEventRotateType eEventType) + { + gdouble x = 0; + gdouble y = 0; + gtk_gesture_get_point(gesture, sequence, &x, &y); + + SalGestureRotateEvent aEvent; + aEvent.meEventType = eEventType; + aEvent.mnX = x; + aEvent.mnY = y; + aEvent.mfAngleDelta = gtk_gesture_rotate_get_angle_delta(GTK_GESTURE_ROTATE(gesture)); + GtkSalFrame* pThis = static_cast(frame); + pThis->CallCallbackExc(SalEvent::GestureRotate, &aEvent); + return true; + } +} + +bool GtkSalFrame::signalZoomBegin(GtkGesture* gesture, GdkEventSequence* sequence, gpointer frame) +{ + return handleSignalZoom(gesture, sequence, frame, GestureEventZoomType::Begin); +} + +bool GtkSalFrame::signalZoomUpdate(GtkGesture* gesture, GdkEventSequence* sequence, gpointer frame) +{ + return handleSignalZoom(gesture, sequence, frame, GestureEventZoomType::Update); +} + +bool GtkSalFrame::signalZoomEnd(GtkGesture* gesture, GdkEventSequence* sequence, gpointer frame) +{ + return handleSignalZoom(gesture, sequence, frame, GestureEventZoomType::End); +} + +bool GtkSalFrame::signalRotateBegin(GtkGesture* gesture, GdkEventSequence* sequence, + gpointer frame) +{ + return handleSignalRotate(gesture, sequence, frame, GestureEventRotateType::Begin); +} + +bool GtkSalFrame::signalRotateUpdate(GtkGesture* gesture, GdkEventSequence* sequence, + gpointer frame) +{ + return handleSignalRotate(gesture, sequence, frame, GestureEventRotateType::Update); +} + +bool GtkSalFrame::signalRotateEnd(GtkGesture* gesture, GdkEventSequence* sequence, + gpointer frame) +{ + return handleSignalRotate(gesture, sequence, frame, GestureEventRotateType::End); +} + +namespace +{ + GdkDragAction VclToGdk(sal_Int8 dragOperation) + { + GdkDragAction eRet(static_cast(0)); + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_COPY) + eRet = static_cast(eRet | GDK_ACTION_COPY); + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_MOVE) + eRet = static_cast(eRet | GDK_ACTION_MOVE); + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_LINK) + eRet = static_cast(eRet | GDK_ACTION_LINK); + return eRet; + } + + sal_Int8 GdkToVcl(GdkDragAction dragOperation) + { + sal_Int8 nRet(0); + if (dragOperation & GDK_ACTION_COPY) + nRet |= css::datatransfer::dnd::DNDConstants::ACTION_COPY; + if (dragOperation & GDK_ACTION_MOVE) + nRet |= css::datatransfer::dnd::DNDConstants::ACTION_MOVE; + if (dragOperation & GDK_ACTION_LINK) + nRet |= css::datatransfer::dnd::DNDConstants::ACTION_LINK; + return nRet; + } +} + +namespace +{ + GdkDragAction getPreferredDragAction(sal_Int8 dragOperation) + { + GdkDragAction eAct(static_cast(0)); + + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_MOVE) + eAct = GDK_ACTION_MOVE; + else if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_COPY) + eAct = GDK_ACTION_COPY; + else if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_LINK) + eAct = GDK_ACTION_LINK; + + return eAct; + } +} + +static bool g_DropSuccessSet = false; +static bool g_DropSuccess = false; + +namespace { + +#if GTK_CHECK_VERSION(4, 0, 0) + +void read_drop_async_completed(GObject* source, GAsyncResult* res, gpointer user_data) +{ + GdkDrop* drop = GDK_DROP(source); + read_transfer_result* pRes = static_cast(user_data); + + GInputStream* pResult = gdk_drop_read_finish(drop, res, nullptr, nullptr); + + if (!pResult) + { + pRes->bDone = true; + g_main_context_wakeup(nullptr); + return; + } + + pRes->aVector.resize(read_transfer_result::BlockSize); + + g_input_stream_read_async(pResult, + pRes->aVector.data(), + pRes->aVector.size(), + G_PRIORITY_DEFAULT, + nullptr, + read_transfer_result::read_block_async_completed, + user_data); +} +#endif + +class GtkDropTargetDropContext : public cppu::WeakImplHelper +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkDragContext *m_pContext; + guint m_nTime; +#else + GdkDrop* m_pDrop; +#endif +public: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkDropTargetDropContext(GdkDragContext* pContext, guint nTime) + : m_pContext(pContext) + , m_nTime(nTime) +#else + GtkDropTargetDropContext(GdkDrop* pDrop) + : m_pDrop(pDrop) +#endif + { + } + + // XDropTargetDropContext + virtual void SAL_CALL acceptDrop(sal_Int8 dragOperation) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gdk_drag_status(m_pContext, getPreferredDragAction(dragOperation), m_nTime); +#else + GdkDragAction eDragAction = getPreferredDragAction(dragOperation); + gdk_drop_status(m_pDrop, + static_cast(eDragAction | gdk_drop_get_actions(m_pDrop)), + eDragAction); +#endif + } + + virtual void SAL_CALL rejectDrop() override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gdk_drag_status(m_pContext, static_cast(0), m_nTime); +#else + gdk_drop_status(m_pDrop, gdk_drop_get_actions(m_pDrop), static_cast(0)); +#endif + } + + virtual void SAL_CALL dropComplete(sal_Bool bSuccess) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_drag_finish(m_pContext, bSuccess, false, m_nTime); +#else + // should we do something better here + gdk_drop_finish(m_pDrop, bSuccess + ? gdk_drop_get_actions(m_pDrop) + : static_cast(0)); +#endif + if (GtkInstDragSource::g_ActiveDragSource) + { + g_DropSuccessSet = true; + g_DropSuccess = bSuccess; + } + } +}; + +} + +class GtkDnDTransferable : public GtkTransferable +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkDragContext *m_pContext; + guint m_nTime; + GtkWidget *m_pWidget; + GtkInstDropTarget* m_pDropTarget; +#else + GdkDrop* m_pDrop; +#endif +#if !GTK_CHECK_VERSION(4, 0, 0) + GMainLoop *m_pLoop; + GtkSelectionData *m_pData; +#endif +public: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkDnDTransferable(GdkDragContext *pContext, guint nTime, GtkWidget *pWidget, GtkInstDropTarget *pDropTarget) + : m_pContext(pContext) + , m_nTime(nTime) + , m_pWidget(pWidget) + , m_pDropTarget(pDropTarget) +#else + GtkDnDTransferable(GdkDrop *pDrop) + : m_pDrop(pDrop) +#endif +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_pLoop(nullptr) + , m_pData(nullptr) +#endif + { + } + + virtual css::uno::Any SAL_CALL getTransferData(const css::datatransfer::DataFlavor& rFlavor) override + { + css::datatransfer::DataFlavor aFlavor(rFlavor); + if (aFlavor.MimeType == "text/plain;charset=utf-16") + aFlavor.MimeType = "text/plain;charset=utf-8"; + + auto it = m_aMimeTypeToGtkType.find(aFlavor.MimeType); + if (it == m_aMimeTypeToGtkType.end()) + return css::uno::Any(); + + css::uno::Any aRet; + +#if !GTK_CHECK_VERSION(4, 0, 0) + /* like gtk_clipboard_wait_for_contents run a sub loop + * waiting for drag-data-received triggered from + * gtk_drag_get_data + */ + { + m_pLoop = g_main_loop_new(nullptr, true); + m_pDropTarget->SetFormatConversionRequest(this); + + gtk_drag_get_data(m_pWidget, m_pContext, it->second, m_nTime); + + if (g_main_loop_is_running(m_pLoop)) + main_loop_run(m_pLoop); + + g_main_loop_unref(m_pLoop); + m_pLoop = nullptr; + m_pDropTarget->SetFormatConversionRequest(nullptr); + } + + if (aFlavor.MimeType == "text/plain;charset=utf-8") + { + OUString aStr; + gchar *pText = reinterpret_cast(gtk_selection_data_get_text(m_pData)); + if (pText) + aStr = OStringToOUString(pText, RTL_TEXTENCODING_UTF8); + g_free(pText); + aRet <<= aStr.replaceAll("\r\n", "\n"); + } + else + { + gint length(0); + const guchar *rawdata = gtk_selection_data_get_data_with_length(m_pData, + &length); + // seen here was rawhide == nullptr and length set to -1 + if (rawdata) + { + css::uno::Sequence aSeq(reinterpret_cast(rawdata), length); + aRet <<= aSeq; + } + } + + gtk_selection_data_free(m_pData); +#else + SalInstance* pInstance = GetSalInstance(); + read_transfer_result aRes; + const char *mime_types[] = { it->second.getStr(), nullptr }; + + gdk_drop_read_async(m_pDrop, + mime_types, + G_PRIORITY_DEFAULT, + nullptr, + read_drop_async_completed, + &aRes); + + while (!aRes.bDone) + pInstance->DoYield(true, false); + + if (aFlavor.MimeType == "text/plain;charset=utf-8") + aRet <<= aRes.get_as_string(); + else + aRet <<= aRes.get_as_sequence(); +#endif + return aRet; + } + + virtual std::vector getTransferDataFlavorsAsVector() override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + std::vector targets; + for (GList* l = gdk_drag_context_list_targets(m_pContext); l; l = l->next) + targets.push_back(static_cast(l->data)); + return GtkTransferable::getTransferDataFlavorsAsVector(targets.data(), targets.size()); +#else + GdkContentFormats* pFormats = gdk_drop_get_formats(m_pDrop); + gsize n_targets; + const char * const *targets = gdk_content_formats_get_mime_types(pFormats, &n_targets); + return GtkTransferable::getTransferDataFlavorsAsVector(targets, n_targets); +#endif + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + void LoopEnd(GtkSelectionData *pData) + { + m_pData = pData; + g_main_loop_quit(m_pLoop); + } +#endif +}; + +// For LibreOffice internal D&D we provide the Transferable without Gtk +// intermediaries as a shortcut, see tdf#100097 for how dbaccess depends on this +GtkInstDragSource* GtkInstDragSource::g_ActiveDragSource; + +#if GTK_CHECK_VERSION(4, 0, 0) + +gboolean GtkSalFrame::signalDragDrop(GtkDropTargetAsync* context, GdkDrop* drop, double x, double y, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + if (!pThis->m_pDropTarget) + return false; + return pThis->m_pDropTarget->signalDragDrop(context, drop, x, y); +} +#else +gboolean GtkSalFrame::signalDragDrop(GtkWidget* pWidget, GdkDragContext* context, gint x, gint y, guint time, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + if (!pThis->m_pDropTarget) + return false; + return pThis->m_pDropTarget->signalDragDrop(pWidget, context, x, y, time); +} +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) +gboolean GtkInstDropTarget::signalDragDrop(GtkWidget* pWidget, GdkDragContext* context, gint x, gint y, guint time) +#else +gboolean GtkInstDropTarget::signalDragDrop(GtkDropTargetAsync* context, GdkDrop* drop, double x, double y) +#endif +{ + // remove the deferred dragExit, as we'll do a drop +#ifndef NDEBUG + bool res = +#endif + g_idle_remove_by_data(this); +#ifndef NDEBUG +#if !GTK_CHECK_VERSION(4, 0, 0) + assert(res); +#else + (void)res; +#endif +#endif + + css::datatransfer::dnd::DropTargetDropEvent aEvent; + aEvent.Source = static_cast(this); +#if !GTK_CHECK_VERSION(4, 0, 0) + aEvent.Context = new GtkDropTargetDropContext(context, time); +#else + aEvent.Context = new GtkDropTargetDropContext(drop); +#endif + aEvent.LocationX = x; + aEvent.LocationY = y; +#if !GTK_CHECK_VERSION(4, 0, 0) + aEvent.DropAction = GdkToVcl(gdk_drag_context_get_selected_action(context)); +#else + aEvent.DropAction = GdkToVcl(getPreferredDragAction(GdkToVcl(gdk_drop_get_actions(drop)))); +#endif + // ACTION_DEFAULT is documented as... + // 'This means the user did not press any key during the Drag and Drop operation + // and the action that was combined with ACTION_DEFAULT is the system default action' + // in tdf#107031 writer won't insert a link when a heading is dragged from the + // navigator unless this is set. Its unclear really what ACTION_DEFAULT means, + // there is a deprecated 'GDK_ACTION_DEFAULT Means nothing, and should not be used' + // possible equivalent in gtk. + // So (tdf#109227) set ACTION_DEFAULT if no modifier key is held down +#if !GTK_CHECK_VERSION(4,0,0) + aEvent.SourceActions = GdkToVcl(gdk_drag_context_get_actions(context)); + GdkModifierType mask; + gdk_window_get_pointer(widget_get_surface(pWidget), nullptr, nullptr, &mask); +#else + aEvent.SourceActions = GdkToVcl(gdk_drop_get_actions(drop)); + GdkModifierType mask = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(context)); +#endif + if (!(mask & (GDK_CONTROL_MASK | GDK_SHIFT_MASK))) + aEvent.DropAction |= css::datatransfer::dnd::DNDConstants::ACTION_DEFAULT; + + css::uno::Reference xTransferable; + // For LibreOffice internal D&D we provide the Transferable without Gtk + // intermediaries as a shortcut, see tdf#100097 for how dbaccess depends on this + if (GtkInstDragSource::g_ActiveDragSource) + xTransferable = GtkInstDragSource::g_ActiveDragSource->GetTransferable(); + else + { +#if GTK_CHECK_VERSION(4,0,0) + xTransferable = new GtkDnDTransferable(drop); +#else + xTransferable = new GtkDnDTransferable(context, time, pWidget, this); +#endif + } + aEvent.Transferable = xTransferable; + + fire_drop(aEvent); + + return true; +} + +namespace { + +class GtkDropTargetDragContext : public cppu::WeakImplHelper +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkDragContext *m_pContext; + guint m_nTime; +#else + GdkDrop* m_pDrop; +#endif +public: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkDropTargetDragContext(GdkDragContext *pContext, guint nTime) + : m_pContext(pContext) + , m_nTime(nTime) +#else + GtkDropTargetDragContext(GdkDrop* pDrop) + : m_pDrop(pDrop) +#endif + { + } + + virtual void SAL_CALL acceptDrag(sal_Int8 dragOperation) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gdk_drag_status(m_pContext, getPreferredDragAction(dragOperation), m_nTime); +#else + gdk_drop_status(m_pDrop, gdk_drop_get_actions(m_pDrop), getPreferredDragAction(dragOperation)); +#endif + } + + virtual void SAL_CALL rejectDrag() override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gdk_drag_status(m_pContext, static_cast(0), m_nTime); +#else + gdk_drop_status(m_pDrop, gdk_drop_get_actions(m_pDrop), static_cast(0)); +#endif + } +}; + +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +void GtkSalFrame::signalDragDropReceived(GtkWidget* pWidget, GdkDragContext* context, gint x, gint y, GtkSelectionData* data, guint ttype, guint time, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + if (!pThis->m_pDropTarget) + return; + pThis->m_pDropTarget->signalDragDropReceived(pWidget, context, x, y, data, ttype, time); +} + +void GtkInstDropTarget::signalDragDropReceived(GtkWidget* /*pWidget*/, GdkDragContext * /*context*/, gint /*x*/, gint /*y*/, GtkSelectionData* data, guint /*ttype*/, guint /*time*/) +{ + /* + * If we get a drop, then we will call like gtk_clipboard_wait_for_contents + * with a loop inside a loop to get the right format, so if this is the + * case return to the outer loop here with a copy of the desired data + * + * don't look at me like that. + */ + if (!m_pFormatConversionRequest) + return; + + m_pFormatConversionRequest->LoopEnd(gtk_selection_data_copy(data)); +} +#endif + +#if GTK_CHECK_VERSION(4,0,0) +GdkDragAction GtkSalFrame::signalDragMotion(GtkDropTargetAsync *dest, GdkDrop *drop, double x, double y, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + if (!pThis->m_pDropTarget) + return GdkDragAction(0); + return pThis->m_pDropTarget->signalDragMotion(dest, drop, x, y); +} +#else +gboolean GtkSalFrame::signalDragMotion(GtkWidget *pWidget, GdkDragContext *context, gint x, gint y, guint time, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + if (!pThis->m_pDropTarget) + return false; + return pThis->m_pDropTarget->signalDragMotion(pWidget, context, x, y, time); +} +#endif + +#if !GTK_CHECK_VERSION(4,0,0) +gboolean GtkInstDropTarget::signalDragMotion(GtkWidget *pWidget, GdkDragContext *context, gint x, gint y, guint time) +#else +GdkDragAction GtkInstDropTarget::signalDragMotion(GtkDropTargetAsync *context, GdkDrop *pDrop, double x, double y) +#endif +{ + if (!m_bInDrag) + { +#if !GTK_CHECK_VERSION(4,0,0) + GtkWidget* pHighlightWidget = m_pFrame ? GTK_WIDGET(m_pFrame->getFixedContainer()) : pWidget; + gtk_drag_highlight(pHighlightWidget); +#else + GtkWidget* pHighlightWidget = m_pFrame ? GTK_WIDGET(m_pFrame->getFixedContainer()) : + gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(context)); + gtk_widget_set_state_flags(pHighlightWidget, GTK_STATE_FLAG_DROP_ACTIVE, false); +#endif + } + + css::datatransfer::dnd::DropTargetDragEnterEvent aEvent; + aEvent.Source = static_cast(this); +#if !GTK_CHECK_VERSION(4,0,0) + rtl::Reference pContext = new GtkDropTargetDragContext(context, time); +#else + rtl::Reference pContext = new GtkDropTargetDragContext(pDrop); +#endif + //preliminary accept the Drag and select the preferred action, the fire_* will + //inform the original caller of our choice and the callsite can decide + //to overrule this choice. i.e. typically here we default to ACTION_MOVE +#if !GTK_CHECK_VERSION(4,0,0) + sal_Int8 nSourceActions = GdkToVcl(gdk_drag_context_get_actions(context)); + GdkModifierType mask; + gdk_window_get_pointer(widget_get_surface(pWidget), nullptr, nullptr, &mask); +#else + sal_Int8 nSourceActions = GdkToVcl(gdk_drop_get_actions(pDrop)); + GdkModifierType mask = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(context)); +#endif + + // tdf#124411 default to move if drag originates within LO itself, default + // to copy if it comes from outside, this is similar to srcAndDestEqual + // in macosx DropTarget::determineDropAction equivalent + sal_Int8 nNewDropAction = GtkInstDragSource::g_ActiveDragSource ? + css::datatransfer::dnd::DNDConstants::ACTION_MOVE : + css::datatransfer::dnd::DNDConstants::ACTION_COPY; + + // tdf#109227 if a modifier is held down, default to the matching + // action for that modifier combo, otherwise pick the preferred + // default from the possible source actions + if ((mask & GDK_SHIFT_MASK) && !(mask & GDK_CONTROL_MASK)) + nNewDropAction = css::datatransfer::dnd::DNDConstants::ACTION_MOVE; + else if ((mask & GDK_CONTROL_MASK) && !(mask & GDK_SHIFT_MASK)) + nNewDropAction = css::datatransfer::dnd::DNDConstants::ACTION_COPY; + else if ((mask & GDK_SHIFT_MASK) && (mask & GDK_CONTROL_MASK) ) + nNewDropAction = css::datatransfer::dnd::DNDConstants::ACTION_LINK; + nNewDropAction &= nSourceActions; + + GdkDragAction eAction; + if (!(mask & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)) && !nNewDropAction) + eAction = getPreferredDragAction(nSourceActions); + else + eAction = getPreferredDragAction(nNewDropAction); + +#if !GTK_CHECK_VERSION(4,0,0) + gdk_drag_status(context, eAction, time); +#else + gdk_drop_status(pDrop, + static_cast(eAction | gdk_drop_get_actions(pDrop)), + eAction); +#endif + aEvent.Context = pContext; + aEvent.LocationX = x; + aEvent.LocationY = y; + //under wayland at least, the action selected by gdk_drag_status on the + //context is not immediately available via gdk_drag_context_get_selected_action + //so here we set the DropAction from what we selected on the context, not + //what the context says is selected + aEvent.DropAction = GdkToVcl(eAction); + aEvent.SourceActions = nSourceActions; + + if (!m_bInDrag) + { + css::uno::Reference xTransferable; + // For LibreOffice internal D&D we provide the Transferable without Gtk + // intermediaries as a shortcut, see tdf#100097 for how dbaccess depends on this + if (GtkInstDragSource::g_ActiveDragSource) + xTransferable = GtkInstDragSource::g_ActiveDragSource->GetTransferable(); + else + { +#if !GTK_CHECK_VERSION(4,0,0) + xTransferable = new GtkDnDTransferable(context, time, pWidget, this); +#else + xTransferable = new GtkDnDTransferable(pDrop); +#endif + } + css::uno::Sequence aFormats = xTransferable->getTransferDataFlavors(); + aEvent.SupportedDataFlavors = aFormats; + fire_dragEnter(aEvent); + m_bInDrag = true; + } + else + { + fire_dragOver(aEvent); + } + +#if !GTK_CHECK_VERSION(4,0,0) + return true; +#else + return eAction; +#endif +} + +#if GTK_CHECK_VERSION(4,0,0) +void GtkSalFrame::signalDragLeave(GtkDropTargetAsync* pDest, GdkDrop* /*drop*/, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + if (!pThis->m_pDropTarget) + return; + pThis->m_pDropTarget->signalDragLeave(gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(pDest))); +} +#else +void GtkSalFrame::signalDragLeave(GtkWidget* pWidget, GdkDragContext* /*context*/, guint /*time*/, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + if (!pThis->m_pDropTarget) + return; + pThis->m_pDropTarget->signalDragLeave(pWidget); +} +#endif + +static gboolean lcl_deferred_dragExit(gpointer user_data) +{ + GtkInstDropTarget* pThis = static_cast(user_data); + css::datatransfer::dnd::DropTargetEvent aEvent; + aEvent.Source = static_cast(pThis); + pThis->fire_dragExit(aEvent); + return false; +} + +void GtkInstDropTarget::signalDragLeave(GtkWidget *pWidget) +{ + m_bInDrag = false; + + GtkWidget* pHighlightWidget = m_pFrame ? GTK_WIDGET(m_pFrame->getFixedContainer()) : pWidget; +#if !GTK_CHECK_VERSION(4,0,0) + gtk_drag_unhighlight(pHighlightWidget); +#else + gtk_widget_unset_state_flags(pHighlightWidget, GTK_STATE_FLAG_DROP_ACTIVE); +#endif + + // defer fire_dragExit, since gtk also sends a drag-leave before the drop, while + // LO expect to either handle the drop or the exit... at least in Writer. + // but since we don't know there will be a drop following the leave, defer the + // exit handling to an idle. + g_idle_add(lcl_deferred_dragExit, this); +} + +void GtkSalFrame::signalDestroy( GtkWidget* pObj, gpointer frame ) +{ + GtkSalFrame* pThis = static_cast(frame); + if( pObj != pThis->m_pWindow ) + return; + + pThis->m_aDamageHandler.damaged = nullptr; + pThis->m_aDamageHandler.handle = nullptr; + if (pThis->m_pSurface) + cairo_surface_set_user_data(pThis->m_pSurface, SvpSalGraphics::getDamageKey(), nullptr, nullptr); + pThis->m_pFixedContainer = nullptr; + pThis->m_pDrawingArea = nullptr; +#if !GTK_CHECK_VERSION(4, 0, 0) + pThis->m_pEventBox = nullptr; +#endif + pThis->m_pTopLevelGrid = nullptr; + pThis->m_pWindow = nullptr; + pThis->m_xFrameWeld.reset(); + pThis->InvalidateGraphics(); +} + +// GtkSalFrame::IMHandler + +GtkSalFrame::IMHandler::IMHandler( GtkSalFrame* pFrame ) +: m_pFrame(pFrame), + m_nPrevKeyPresses( 0 ), + m_pIMContext( nullptr ), + m_bFocused( true ), + m_bPreeditJustChanged( false ) +{ + m_aInputEvent.mpTextAttr = nullptr; + createIMContext(); +} + +GtkSalFrame::IMHandler::~IMHandler() +{ + // cancel an eventual event posted to begin preedit again + GtkSalFrame::getDisplay()->CancelInternalEvent( m_pFrame, &m_aInputEvent, SalEvent::ExtTextInput ); + deleteIMContext(); +} + +void GtkSalFrame::IMHandler::createIMContext() +{ + if( m_pIMContext ) + return; + + m_pIMContext = gtk_im_multicontext_new (); + g_signal_connect( m_pIMContext, "commit", + G_CALLBACK (signalIMCommit), this ); + g_signal_connect( m_pIMContext, "preedit_changed", + G_CALLBACK (signalIMPreeditChanged), this ); + g_signal_connect( m_pIMContext, "retrieve_surrounding", + G_CALLBACK (signalIMRetrieveSurrounding), this ); + g_signal_connect( m_pIMContext, "delete_surrounding", + G_CALLBACK (signalIMDeleteSurrounding), this ); + g_signal_connect( m_pIMContext, "preedit_start", + G_CALLBACK (signalIMPreeditStart), this ); + g_signal_connect( m_pIMContext, "preedit_end", + G_CALLBACK (signalIMPreeditEnd), this ); + + GetGenericUnixSalData()->ErrorTrapPush(); + im_context_set_client_widget(m_pIMContext, m_pFrame->getMouseEventWidget()); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_event_controller_key_set_im_context(m_pFrame->m_pKeyController, m_pIMContext); +#endif + gtk_im_context_focus_in( m_pIMContext ); + GetGenericUnixSalData()->ErrorTrapPop(); + m_bFocused = true; + +} + +void GtkSalFrame::IMHandler::deleteIMContext() +{ + if( !m_pIMContext ) + return; + + // first give IC a chance to deinitialize + GetGenericUnixSalData()->ErrorTrapPush(); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_event_controller_key_set_im_context(m_pFrame->m_pKeyController, nullptr); +#endif + im_context_set_client_widget(m_pIMContext, nullptr); + GetGenericUnixSalData()->ErrorTrapPop(); + // destroy old IC + g_object_unref( m_pIMContext ); + m_pIMContext = nullptr; +} + +void GtkSalFrame::IMHandler::doCallEndExtTextInput() +{ + m_aInputEvent.mpTextAttr = nullptr; + m_pFrame->CallCallbackExc( SalEvent::EndExtTextInput, nullptr ); +} + +void GtkSalFrame::IMHandler::updateIMSpotLocation() +{ + SalExtTextInputPosEvent aPosEvent; + m_pFrame->CallCallbackExc( SalEvent::ExtTextInputPos, static_cast(&aPosEvent) ); + GdkRectangle aArea; + aArea.x = aPosEvent.mnX; + aArea.y = aPosEvent.mnY; + aArea.width = aPosEvent.mnWidth; + aArea.height = aPosEvent.mnHeight; + GetGenericUnixSalData()->ErrorTrapPush(); + gtk_im_context_set_cursor_location( m_pIMContext, &aArea ); + GetGenericUnixSalData()->ErrorTrapPop(); +} + +void GtkSalFrame::IMHandler::sendEmptyCommit() +{ + vcl::DeletionListener aDel( m_pFrame ); + + SalExtTextInputEvent aEmptyEv; + aEmptyEv.mpTextAttr = nullptr; + aEmptyEv.maText.clear(); + aEmptyEv.mnCursorPos = 0; + aEmptyEv.mnCursorFlags = 0; + m_pFrame->CallCallbackExc( SalEvent::ExtTextInput, static_cast(&aEmptyEv) ); + if( ! aDel.isDeleted() ) + m_pFrame->CallCallbackExc( SalEvent::EndExtTextInput, nullptr ); +} + +void GtkSalFrame::IMHandler::endExtTextInput( EndExtTextInputFlags /*nFlags*/ ) +{ + gtk_im_context_reset ( m_pIMContext ); + + if( !m_aInputEvent.mpTextAttr ) + return; + + vcl::DeletionListener aDel( m_pFrame ); + // delete preedit in sal (commit an empty string) + sendEmptyCommit(); + if( ! aDel.isDeleted() ) + { + // mark previous preedit state again (will e.g. be sent at focus gain) + m_aInputEvent.mpTextAttr = m_aInputFlags.data(); + if( m_bFocused ) + { + // begin preedit again + GtkSalFrame::getDisplay()->SendInternalEvent( m_pFrame, &m_aInputEvent, SalEvent::ExtTextInput ); + } + } +} + +void GtkSalFrame::IMHandler::focusChanged( bool bFocusIn ) +{ + m_bFocused = bFocusIn; + if( bFocusIn ) + { + GetGenericUnixSalData()->ErrorTrapPush(); + gtk_im_context_focus_in( m_pIMContext ); + GetGenericUnixSalData()->ErrorTrapPop(); + if( m_aInputEvent.mpTextAttr ) + { + sendEmptyCommit(); + // begin preedit again + GtkSalFrame::getDisplay()->SendInternalEvent( m_pFrame, &m_aInputEvent, SalEvent::ExtTextInput ); + } + } + else + { + GetGenericUnixSalData()->ErrorTrapPush(); + gtk_im_context_focus_out( m_pIMContext ); + GetGenericUnixSalData()->ErrorTrapPop(); + // cancel an eventual event posted to begin preedit again + GtkSalFrame::getDisplay()->CancelInternalEvent( m_pFrame, &m_aInputEvent, SalEvent::ExtTextInput ); + } +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +bool GtkSalFrame::IMHandler::handleKeyEvent( GdkEventKey* pEvent ) +{ + vcl::DeletionListener aDel( m_pFrame ); + + if( pEvent->type == GDK_KEY_PRESS ) + { + // Add this key press event to the list of previous key presses + // to which we compare key release events. If a later key release + // event has a matching key press event in this list, we swallow + // the key release because some GTK Input Methods don't swallow it + // for us. + m_aPrevKeyPresses.emplace_back(pEvent ); + m_nPrevKeyPresses++; + + // Also pop off the earliest key press event if there are more than 10 + // already. + while (m_nPrevKeyPresses > 10) + { + m_aPrevKeyPresses.pop_front(); + m_nPrevKeyPresses--; + } + + GObject* pRef = G_OBJECT( g_object_ref( G_OBJECT( m_pIMContext ) ) ); + + // #i51353# update spot location on every key input since we cannot + // know which key may activate a preedit choice window + updateIMSpotLocation(); + if( aDel.isDeleted() ) + return true; + + bool bResult = gtk_im_context_filter_keypress( m_pIMContext, pEvent ); + g_object_unref( pRef ); + + if( aDel.isDeleted() ) + return true; + + m_bPreeditJustChanged = false; + + if( bResult ) + return true; + else + { + SAL_WARN_IF( m_nPrevKeyPresses <= 0, "vcl.gtk3", "key press has vanished !" ); + if( ! m_aPrevKeyPresses.empty() ) // sanity check + { + // event was not swallowed, do not filter a following + // key release event + // note: this relies on gtk_im_context_filter_keypress + // returning without calling a handler (in the "not swallowed" + // case ) which might change the previous key press list so + // we would pop the wrong event here + m_aPrevKeyPresses.pop_back(); + m_nPrevKeyPresses--; + } + } + } + + // Determine if we got an earlier key press event corresponding to this key release + if (pEvent->type == GDK_KEY_RELEASE) + { + GObject* pRef = G_OBJECT( g_object_ref( G_OBJECT( m_pIMContext ) ) ); + bool bResult = gtk_im_context_filter_keypress( m_pIMContext, pEvent ); + g_object_unref( pRef ); + + if( aDel.isDeleted() ) + return true; + + m_bPreeditJustChanged = false; + + auto iter = std::find(m_aPrevKeyPresses.begin(), m_aPrevKeyPresses.end(), pEvent); + // If we found a corresponding previous key press event, swallow the release + // and remove the earlier key press from our list + if (iter != m_aPrevKeyPresses.end()) + { + m_aPrevKeyPresses.erase(iter); + m_nPrevKeyPresses--; + return true; + } + + if( bResult ) + return true; + } + + return false; +} + +/* FIXME: +* #122282# still more hacking: some IMEs never start a preedit but simply commit +* in this case we cannot commit a single character. Workaround: do not do the +* single key hack for enter or space if the unicode committed does not match +*/ + +static bool checkSingleKeyCommitHack( guint keyval, sal_Unicode cCode ) +{ + bool bRet = true; + switch( keyval ) + { + case GDK_KEY_KP_Enter: + case GDK_KEY_Return: + if( cCode != '\n' && cCode != '\r' ) + bRet = false; + break; + case GDK_KEY_space: + case GDK_KEY_KP_Space: + if( cCode != ' ' ) + bRet = false; + break; + default: + break; + } + return bRet; +} + +void GtkSalFrame::IMHandler::signalIMCommit( GtkIMContext* /*pContext*/, gchar* pText, gpointer im_handler ) +{ + GtkSalFrame::IMHandler* pThis = static_cast(im_handler); + + SolarMutexGuard aGuard; + vcl::DeletionListener aDel( pThis->m_pFrame ); + { + const bool bWasPreedit = + (pThis->m_aInputEvent.mpTextAttr != nullptr) || + pThis->m_bPreeditJustChanged; + + pThis->m_aInputEvent.mpTextAttr = nullptr; + pThis->m_aInputEvent.maText = OUString( pText, strlen(pText), RTL_TEXTENCODING_UTF8 ); + pThis->m_aInputEvent.mnCursorPos = pThis->m_aInputEvent.maText.getLength(); + pThis->m_aInputEvent.mnCursorFlags = 0; + + pThis->m_aInputFlags.clear(); + + /* necessary HACK: all keyboard input comes in here as soon as an IMContext is set + * which is logical and consequent. But since even simple input like + * comes through the commit signal instead of signalKey + * and all kinds of windows only implement KeyInput (e.g. PushButtons, + * RadioButtons and a lot of other Controls), will send a single + * KeyInput/KeyUp sequence instead of an ExtText event if there + * never was a preedit and the text is only one character. + * + * In this case there the last ExtText event must have been + * SalEvent::EndExtTextInput, either because of a regular commit + * or because there never was a preedit. + */ + bool bSingleCommit = false; + if( ! bWasPreedit + && pThis->m_aInputEvent.maText.getLength() == 1 + && ! pThis->m_aPrevKeyPresses.empty() + ) + { + const PreviousKeyPress& rKP = pThis->m_aPrevKeyPresses.back(); + sal_Unicode aOrigCode = pThis->m_aInputEvent.maText[0]; + + if( checkSingleKeyCommitHack( rKP.keyval, aOrigCode ) ) + { + pThis->m_pFrame->doKeyCallback( rKP.state, rKP.keyval, rKP.hardware_keycode, rKP.group, aOrigCode, true, true ); + bSingleCommit = true; + } + } + if( ! bSingleCommit ) + { + pThis->m_pFrame->CallCallbackExc( SalEvent::ExtTextInput, static_cast(&pThis->m_aInputEvent)); + if( ! aDel.isDeleted() ) + pThis->doCallEndExtTextInput(); + } + if( ! aDel.isDeleted() ) + { + // reset input event + pThis->m_aInputEvent.maText.clear(); + pThis->m_aInputEvent.mnCursorPos = 0; + pThis->updateIMSpotLocation(); + } + } +} +#else +void GtkSalFrame::IMHandler::signalIMCommit( GtkIMContext* /*pContext*/, gchar* pText, gpointer im_handler ) +{ + GtkSalFrame::IMHandler* pThis = static_cast(im_handler); + + SolarMutexGuard aGuard; + vcl::DeletionListener aDel( pThis->m_pFrame ); + { +#if 0 + const bool bWasPreedit = + (pThis->m_aInputEvent.mpTextAttr != nullptr) || + pThis->m_bPreeditJustChanged; +#endif + + pThis->m_aInputEvent.mpTextAttr = nullptr; + pThis->m_aInputEvent.maText = OUString( pText, strlen(pText), RTL_TEXTENCODING_UTF8 ); + pThis->m_aInputEvent.mnCursorPos = pThis->m_aInputEvent.maText.getLength(); + pThis->m_aInputEvent.mnCursorFlags = 0; + + pThis->m_aInputFlags.clear(); + + /* necessary HACK: all keyboard input comes in here as soon as an IMContext is set + * which is logical and consequent. But since even simple input like + * comes through the commit signal instead of signalKey + * and all kinds of windows only implement KeyInput (e.g. PushButtons, + * RadioButtons and a lot of other Controls), will send a single + * KeyInput/KeyUp sequence instead of an ExtText event if there + * never was a preedit and the text is only one character. + * + * In this case there the last ExtText event must have been + * SalEvent::EndExtTextInput, either because of a regular commit + * or because there never was a preedit. + */ + bool bSingleCommit = false; +#if 0 + // TODO this needs a rethink to work again if necessary + if( ! bWasPreedit + && pThis->m_aInputEvent.maText.getLength() == 1 + && ! pThis->m_aPrevKeyPresses.empty() + ) + { + const PreviousKeyPress& rKP = pThis->m_aPrevKeyPresses.back(); + sal_Unicode aOrigCode = pThis->m_aInputEvent.maText[0]; + + if( checkSingleKeyCommitHack( rKP.keyval, aOrigCode ) ) + { + pThis->m_pFrame->doKeyCallback( rKP.state, rKP.keyval, rKP.hardware_keycode, rKP.group, aOrigCode, true, true ); + bSingleCommit = true; + } + } +#endif + if( ! bSingleCommit ) + { + pThis->m_pFrame->CallCallbackExc( SalEvent::ExtTextInput, static_cast(&pThis->m_aInputEvent)); + if( ! aDel.isDeleted() ) + pThis->doCallEndExtTextInput(); + } + if( ! aDel.isDeleted() ) + { + // reset input event + pThis->m_aInputEvent.maText.clear(); + pThis->m_aInputEvent.mnCursorPos = 0; + pThis->updateIMSpotLocation(); + } + } +} +#endif + +OUString GtkSalFrame::GetPreeditDetails(GtkIMContext* pIMContext, std::vector& rInputFlags, sal_Int32& rCursorPos, sal_uInt8& rCursorFlags) +{ + char* pText = nullptr; + PangoAttrList* pAttrs = nullptr; + gint nCursorPos = 0; + + gtk_im_context_get_preedit_string( pIMContext, + &pText, + &pAttrs, + &nCursorPos ); + + gint nUtf8Len = pText ? strlen(pText) : 0; + OUString sText = pText ? OUString(pText, nUtf8Len, RTL_TEXTENCODING_UTF8) : OUString(); + + std::vector aUtf16Offsets; + for (sal_Int32 nUtf16Offset = 0; nUtf16Offset < sText.getLength(); sText.iterateCodePoints(&nUtf16Offset)) + aUtf16Offsets.push_back(nUtf16Offset); + + sal_Int32 nUtf32Len = aUtf16Offsets.size(); + // from the above loop filling aUtf16Offsets, we know that its size() fits into sal_Int32 + aUtf16Offsets.push_back(sText.getLength()); + + // sanitize the CurPos which is in utf-32 + if (nCursorPos < 0) + nCursorPos = 0; + else if (nCursorPos > nUtf32Len) + nCursorPos = nUtf32Len; + + rCursorPos = aUtf16Offsets[nCursorPos]; + rCursorFlags = 0; + + rInputFlags.resize(std::max(1, static_cast(sText.getLength())), ExtTextInputAttr::NONE); + + PangoAttrIterator *iter = pango_attr_list_get_iterator(pAttrs); + do + { + GSList *attr_list = nullptr; + GSList *tmp_list = nullptr; + gint nUtf8Start, nUtf8End; + ExtTextInputAttr sal_attr = ExtTextInputAttr::NONE; + + // docs say... "Get the range of the current segment ... the stored + // return values are signed, not unsigned like the values in + // PangoAttribute", which implies that the units are otherwise the same + // as that of PangoAttribute whose docs state these units are "in + // bytes" + // so this is the utf8 range + pango_attr_iterator_range(iter, &nUtf8Start, &nUtf8End); + + // sanitize the utf8 range + nUtf8Start = std::min(nUtf8Start, nUtf8Len); + nUtf8End = std::min(nUtf8End, nUtf8Len); + if (nUtf8Start >= nUtf8End) + continue; + + // get the utf32 range + sal_Int32 nUtf32Start = g_utf8_pointer_to_offset(pText, pText + nUtf8Start); + sal_Int32 nUtf32End = g_utf8_pointer_to_offset(pText, pText + nUtf8End); + + // sanitize the utf32 range + nUtf32Start = std::min(nUtf32Start, nUtf32Len); + nUtf32End = std::min(nUtf32End, nUtf32Len); + if (nUtf32Start >= nUtf32End) + continue; + + tmp_list = attr_list = pango_attr_iterator_get_attrs (iter); + while (tmp_list) + { + PangoAttribute *pango_attr = static_cast(tmp_list->data); + + switch (pango_attr->klass->type) + { + case PANGO_ATTR_BACKGROUND: + sal_attr |= ExtTextInputAttr::Highlight; + rCursorFlags |= EXTTEXTINPUT_CURSOR_INVISIBLE; + break; + case PANGO_ATTR_UNDERLINE: + { + PangoAttrInt* pango_underline = reinterpret_cast(pango_attr); + switch (pango_underline->value) + { + case PANGO_UNDERLINE_NONE: + break; + case PANGO_UNDERLINE_DOUBLE: + sal_attr |= ExtTextInputAttr::DoubleUnderline; + break; + default: + sal_attr |= ExtTextInputAttr::Underline; + break; + } + break; + } + case PANGO_ATTR_STRIKETHROUGH: + sal_attr |= ExtTextInputAttr::RedText; + break; + default: + break; + } + pango_attribute_destroy (pango_attr); + tmp_list = tmp_list->next; + } + if (!attr_list) + sal_attr |= ExtTextInputAttr::Underline; + g_slist_free (attr_list); + + // Set the sal attributes on our text + // rhbz#1648281 apply over our utf-16 range derived from the input utf-32 range + for (sal_Int32 i = aUtf16Offsets[nUtf32Start]; i < aUtf16Offsets[nUtf32End]; ++i) + { + SAL_WARN_IF(i >= static_cast(rInputFlags.size()), + "vcl.gtk3", "pango attrib out of range. Broken range: " + << aUtf16Offsets[nUtf32Start] << "," << aUtf16Offsets[nUtf32End] << " Legal range: 0," + << rInputFlags.size()); + if (i >= static_cast(rInputFlags.size())) + continue; + rInputFlags[i] |= sal_attr; + } + } while (pango_attr_iterator_next (iter)); + pango_attr_iterator_destroy(iter); + + g_free( pText ); + pango_attr_list_unref( pAttrs ); + + return sText; +} + +void GtkSalFrame::IMHandler::signalIMPreeditChanged( GtkIMContext* pIMContext, gpointer im_handler ) +{ + GtkSalFrame::IMHandler* pThis = static_cast(im_handler); + + sal_Int32 nCursorPos(0); + sal_uInt8 nCursorFlags(0); + std::vector aInputFlags; + OUString sText = GtkSalFrame::GetPreeditDetails(pIMContext, aInputFlags, nCursorPos, nCursorFlags); + if (sText.isEmpty() && pThis->m_aInputEvent.maText.isEmpty()) + { + // change from nothing to nothing -> do not start preedit + // e.g. this will activate input into a calc cell without + // user input + return; + } + + pThis->m_bPreeditJustChanged = true; + + bool bEndPreedit = sText.isEmpty() && pThis->m_aInputEvent.mpTextAttr != nullptr; + pThis->m_aInputEvent.maText = sText; + pThis->m_aInputEvent.mnCursorPos = nCursorPos; + pThis->m_aInputEvent.mnCursorFlags = nCursorFlags; + pThis->m_aInputFlags = aInputFlags; + pThis->m_aInputEvent.mpTextAttr = pThis->m_aInputFlags.data(); + + SolarMutexGuard aGuard; + vcl::DeletionListener aDel( pThis->m_pFrame ); + + pThis->m_pFrame->CallCallbackExc( SalEvent::ExtTextInput, static_cast(&pThis->m_aInputEvent)); + if( bEndPreedit && ! aDel.isDeleted() ) + pThis->doCallEndExtTextInput(); + if( ! aDel.isDeleted() ) + pThis->updateIMSpotLocation(); +} + +void GtkSalFrame::IMHandler::signalIMPreeditStart( GtkIMContext*, gpointer /*im_handler*/ ) +{ +} + +void GtkSalFrame::IMHandler::signalIMPreeditEnd( GtkIMContext*, gpointer im_handler ) +{ + GtkSalFrame::IMHandler* pThis = static_cast(im_handler); + + pThis->m_bPreeditJustChanged = true; + + SolarMutexGuard aGuard; + vcl::DeletionListener aDel( pThis->m_pFrame ); + pThis->doCallEndExtTextInput(); + if( ! aDel.isDeleted() ) + pThis->updateIMSpotLocation(); +} + +gboolean GtkSalFrame::IMHandler::signalIMRetrieveSurrounding( GtkIMContext* pContext, gpointer im_handler ) +{ + GtkSalFrame::IMHandler* pThis = static_cast(im_handler); + + SalSurroundingTextRequestEvent aEvt; + aEvt.maText.clear(); + aEvt.mnStart = aEvt.mnEnd = 0; + + SolarMutexGuard aGuard; + pThis->m_pFrame->CallCallback(SalEvent::SurroundingTextRequest, &aEvt); + + OString sUTF = OUStringToOString(aEvt.maText, RTL_TEXTENCODING_UTF8); + std::u16string_view sCursorText(aEvt.maText.subView(0, aEvt.mnStart)); + gtk_im_context_set_surrounding(pContext, sUTF.getStr(), sUTF.getLength(), + OUStringToOString(sCursorText, RTL_TEXTENCODING_UTF8).getLength()); + return true; +} + +gboolean GtkSalFrame::IMHandler::signalIMDeleteSurrounding( GtkIMContext*, gint offset, gint nchars, + gpointer im_handler ) +{ + GtkSalFrame::IMHandler* pThis = static_cast(im_handler); + + // First get the surrounding text + SalSurroundingTextRequestEvent aSurroundingTextEvt; + aSurroundingTextEvt.maText.clear(); + aSurroundingTextEvt.mnStart = aSurroundingTextEvt.mnEnd = 0; + + SolarMutexGuard aGuard; + pThis->m_pFrame->CallCallback(SalEvent::SurroundingTextRequest, &aSurroundingTextEvt); + + // Turn offset, nchars into a utf-16 selection + Selection aSelection = SalFrame::CalcDeleteSurroundingSelection(aSurroundingTextEvt.maText, + aSurroundingTextEvt.mnStart, + offset, nchars); + Selection aInvalid(SAL_MAX_UINT32, SAL_MAX_UINT32); + if (aSelection == aInvalid) + return false; + + SalSurroundingTextSelectionChangeEvent aEvt; + aEvt.mnStart = aSelection.Min(); + aEvt.mnEnd = aSelection.Max(); + + pThis->m_pFrame->CallCallback(SalEvent::DeleteSurroundingTextRequest, &aEvt); + + aSelection = Selection(aEvt.mnStart, aEvt.mnEnd); + if (aSelection == aInvalid) + return false; + + return true; +} + +AbsoluteScreenPixelSize GtkSalDisplay::GetScreenSize( int nDisplayScreen ) +{ + AbsoluteScreenPixelRectangle aRect = m_pSys->GetDisplayScreenPosSizePixel( nDisplayScreen ); + return AbsoluteScreenPixelSize( aRect.GetWidth(), aRect.GetHeight() ); +} + +sal_uIntPtr GtkSalFrame::GetNativeWindowHandle(GtkWidget *pWidget) +{ + GdkSurface* pSurface = widget_get_surface(pWidget); + GdkDisplay *pDisplay = getGdkDisplay(); + +#if defined(GDK_WINDOWING_X11) + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + { + return gdk_x11_surface_get_xid(pSurface); + } +#endif + +#if defined(GDK_WINDOWING_WAYLAND) + if (DLSYM_GDK_IS_WAYLAND_DISPLAY(pDisplay)) + { + return reinterpret_cast(gdk_wayland_surface_get_wl_surface(pSurface)); + } +#endif + + return 0; +} + +void GtkInstDragSource::set_datatransfer(const css::uno::Reference& rTrans, + const css::uno::Reference& rListener) +{ + m_xListener = rListener; + m_xTrans = rTrans; +} + +void GtkInstDragSource::setActiveDragSource() +{ + // For LibreOffice internal D&D we provide the Transferable without Gtk + // intermediaries as a shortcut, see tdf#100097 for how dbaccess depends on this + g_ActiveDragSource = this; + g_DropSuccessSet = false; + g_DropSuccess = false; +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +std::vector GtkInstDragSource::FormatsToGtk(const css::uno::Sequence &rFormats) +{ + return m_aConversionHelper.FormatsToGtk(rFormats); +} +#endif + +void GtkInstDragSource::startDrag(const datatransfer::dnd::DragGestureEvent& rEvent, + sal_Int8 sourceActions, sal_Int32 /*cursor*/, sal_Int32 /*image*/, + const css::uno::Reference& rTrans, + const css::uno::Reference& rListener) +{ + set_datatransfer(rTrans, rListener); + + if (m_pFrame) + { + setActiveDragSource(); + + m_pFrame->startDrag(rEvent, rTrans, m_aConversionHelper ,VclToGdk(sourceActions)); + } + else + dragFailed(); +} + +void GtkSalFrame::startDrag(const css::datatransfer::dnd::DragGestureEvent& rEvent, + const css::uno::Reference& rTrans, + VclToGtkHelper& rConversionHelper, + GdkDragAction sourceActions) +{ + SolarMutexGuard aGuard; + + assert(m_pDragSource); + +#if GTK_CHECK_VERSION(4, 0, 0) + + GdkSeat *pSeat = gdk_display_get_default_seat(getGdkDisplay()); + GdkDrag* pDrag = gdk_drag_begin(widget_get_surface(getMouseEventWidget()), + gdk_seat_get_pointer(pSeat), + transerable_content_new(&rConversionHelper, rTrans.get()), + sourceActions, + rEvent.DragOriginX, rEvent.DragOriginY); + + g_signal_connect(G_OBJECT(pDrag), "drop-performed", G_CALLBACK(signalDragEnd), this); + g_signal_connect(G_OBJECT(pDrag), "cancel", G_CALLBACK(signalDragFailed), this); + g_signal_connect(G_OBJECT(pDrag), "dnd-finished", G_CALLBACK(signalDragDelete), this); + +#else + + auto aFormats = rTrans->getTransferDataFlavors(); + auto aGtkTargets = rConversionHelper.FormatsToGtk(aFormats); + + GtkTargetList *pTargetList = gtk_target_list_new(aGtkTargets.data(), aGtkTargets.size()); + + gint nDragButton = 1; // default to left button + css::awt::MouseEvent aEvent; + if (rEvent.Event >>= aEvent) + { + if (aEvent.Buttons & css::awt::MouseButton::LEFT ) + nDragButton = 1; + else if (aEvent.Buttons & css::awt::MouseButton::RIGHT) + nDragButton = 3; + else if (aEvent.Buttons & css::awt::MouseButton::MIDDLE) + nDragButton = 2; + } + + GdkEvent aFakeEvent; + memset(&aFakeEvent, 0, sizeof(GdkEvent)); + aFakeEvent.type = GDK_BUTTON_PRESS; + aFakeEvent.button.window = widget_get_surface(getMouseEventWidget()); + aFakeEvent.button.time = GDK_CURRENT_TIME; + + aFakeEvent.button.device = gtk_get_current_event_device(); + // if no current event to determine device, or (tdf#140272) the device will be unsuitable then find an + // appropriate device to use. + if (!aFakeEvent.button.device || !gdk_device_get_window_at_position(aFakeEvent.button.device, nullptr, nullptr)) + { + GdkDeviceManager* pDeviceManager = gdk_display_get_device_manager(getGdkDisplay()); + GList* pDevices = gdk_device_manager_list_devices(pDeviceManager, GDK_DEVICE_TYPE_MASTER); + for (GList* pEntry = pDevices; pEntry; pEntry = pEntry->next) + { + GdkDevice* pDevice = static_cast(pEntry->data); + if (gdk_device_get_source(pDevice) == GDK_SOURCE_KEYBOARD) + continue; + if (gdk_device_get_window_at_position(pDevice, nullptr, nullptr)) + { + aFakeEvent.button.device = pDevice; + break; + } + } + g_list_free(pDevices); + } + + GdkDragContext *pDrag; + if (!aFakeEvent.button.device || !gdk_device_get_window_at_position(aFakeEvent.button.device, nullptr, nullptr)) + pDrag = nullptr; + else + pDrag = gtk_drag_begin_with_coordinates(getMouseEventWidget(), + pTargetList, + sourceActions, + nDragButton, + &aFakeEvent, + rEvent.DragOriginX, + rEvent.DragOriginY); + + gtk_target_list_unref(pTargetList); + + for (auto &a : aGtkTargets) + g_free(a.target); +#endif + + if (!pDrag) + m_pDragSource->dragFailed(); +} + +void GtkInstDragSource::dragFailed() +{ + if (m_xListener.is()) + { + datatransfer::dnd::DragSourceDropEvent aEv; + aEv.DropAction = datatransfer::dnd::DNDConstants::ACTION_NONE; + aEv.DropSuccess = false; + auto xListener = m_xListener; + m_xListener.clear(); + xListener->dragDropEnd(aEv); + } +} + +#if GTK_CHECK_VERSION(4, 0, 0) +void GtkSalFrame::signalDragFailed(GdkDrag* /*drag*/, GdkDragCancelReason /*reason*/, gpointer frame) +#else +gboolean GtkSalFrame::signalDragFailed(GtkWidget* /*widget*/, GdkDragContext* /*context*/, GtkDragResult /*result*/, gpointer frame) +#endif +{ + GtkSalFrame* pThis = static_cast(frame); + if (pThis->m_pDragSource) + pThis->m_pDragSource->dragFailed(); +#if !GTK_CHECK_VERSION(4, 0, 0) + return false; +#endif +} + +void GtkInstDragSource::dragDelete() +{ + if (m_xListener.is()) + { + datatransfer::dnd::DragSourceDropEvent aEv; + aEv.DropAction = datatransfer::dnd::DNDConstants::ACTION_MOVE; + aEv.DropSuccess = true; + auto xListener = m_xListener; + m_xListener.clear(); + xListener->dragDropEnd(aEv); + } +} + +#if GTK_CHECK_VERSION(4, 0, 0) +void GtkSalFrame::signalDragDelete(GdkDrag* /*context*/, gpointer frame) +#else +void GtkSalFrame::signalDragDelete(GtkWidget* /*widget*/, GdkDragContext* /*context*/, gpointer frame) +#endif +{ + GtkSalFrame* pThis = static_cast(frame); + if (!pThis->m_pDragSource) + return; + pThis->m_pDragSource->dragDelete(); +} + +#if GTK_CHECK_VERSION(4, 0, 0) +void GtkInstDragSource::dragEnd(GdkDrag* context) +#else +void GtkInstDragSource::dragEnd(GdkDragContext* context) +#endif +{ + if (m_xListener.is()) + { + datatransfer::dnd::DragSourceDropEvent aEv; +#if GTK_CHECK_VERSION(4, 0, 0) + aEv.DropAction = GdkToVcl(gdk_drag_get_selected_action(context)); +#else + aEv.DropAction = GdkToVcl(gdk_drag_context_get_selected_action(context)); +#endif + // an internal drop can accept the drop but fail with dropComplete( false ) + // this is different than the GTK API + if (g_DropSuccessSet) + aEv.DropSuccess = g_DropSuccess; + else + aEv.DropSuccess = true; + auto xListener = m_xListener; + m_xListener.clear(); + xListener->dragDropEnd(aEv); + } + g_ActiveDragSource = nullptr; +} + +#if GTK_CHECK_VERSION(4, 0, 0) +void GtkSalFrame::signalDragEnd(GdkDrag* context, gpointer frame) +#else +void GtkSalFrame::signalDragEnd(GtkWidget* /*widget*/, GdkDragContext* context, gpointer frame) +#endif +{ + GtkSalFrame* pThis = static_cast(frame); + if (!pThis->m_pDragSource) + return; + pThis->m_pDragSource->dragEnd(context); +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +void GtkInstDragSource::dragDataGet(GtkSelectionData *data, guint info) +{ + m_aConversionHelper.setSelectionData(m_xTrans, data, info); +} + +void GtkSalFrame::signalDragDataGet(GtkWidget* /*widget*/, GdkDragContext* /*context*/, GtkSelectionData *data, guint info, + guint /*time*/, gpointer frame) +{ + GtkSalFrame* pThis = static_cast(frame); + if (!pThis->m_pDragSource) + return; + pThis->m_pDragSource->dragDataGet(data, info); +} +#endif + +bool GtkSalFrame::CallCallbackExc(SalEvent nEvent, const void* pEvent) const +{ + SolarMutexGuard aGuard; + bool nRet = false; + try + { + nRet = CallCallback(nEvent, pEvent); + } + catch (...) + { + GetGtkSalData()->setException(std::current_exception()); + } + return nRet; +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +void GtkSalFrame::nopaint_container_resize_children(GtkContainer *pContainer) +{ + bool bOrigSalObjectSetPosSize = m_bSalObjectSetPosSize; + m_bSalObjectSetPosSize = true; + gtk_container_resize_children(pContainer); + m_bSalObjectSetPosSize = bOrigSalObjectSetPosSize; +} +#endif + +GdkEvent* GtkSalFrame::makeFakeKeyPress(GtkWidget* pWidget) +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkEvent *event = gdk_event_new(GDK_KEY_PRESS); + event->key.window = GDK_WINDOW(g_object_ref(widget_get_surface(pWidget))); + + GdkSeat *seat = gdk_display_get_default_seat(gtk_widget_get_display(pWidget)); + gdk_event_set_device(event, gdk_seat_get_keyboard(seat)); + + event->key.send_event = 1 /* TRUE */; + event->key.time = gtk_get_current_event_time(); + event->key.state = 0; + event->key.keyval = 0; + event->key.length = 0; + event->key.string = nullptr; + event->key.hardware_keycode = 0; + event->key.group = 0; + event->key.is_modifier = false; + return event; +#else + (void)pWidget; + return nullptr; +#endif +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtkinst.cxx b/vcl/unx/gtk3/gtkinst.cxx new file mode 100644 index 0000000000..c76d6291ce --- /dev/null +++ b/vcl/unx/gtk3/gtkinst.cxx @@ -0,0 +1,24855 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#if !GTK_CHECK_VERSION(4, 0, 0) +#include "a11y/atkwrapper.hxx" +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "customcellrenderer.hxx" +#include +#include +#include +#include +#include + +using namespace com::sun::star; +using namespace com::sun::star::uno; +using namespace com::sun::star::lang; + +extern "C" +{ + #define GET_YIELD_MUTEX() static_cast(GetSalInstance()->GetYieldMutex()) +#if !GTK_CHECK_VERSION(4, 0, 0) + static void GdkThreadsEnter() + { + GtkYieldMutex *pYieldMutex = GET_YIELD_MUTEX(); + pYieldMutex->ThreadsEnter(); + } + static void GdkThreadsLeave() + { + GtkYieldMutex *pYieldMutex = GET_YIELD_MUTEX(); + pYieldMutex->ThreadsLeave(); + } +#endif + + VCLPLUG_GTK_PUBLIC SalInstance* create_SalInstance() + { + SAL_INFO( + "vcl.gtk", + "create vcl plugin instance with gtk version " << gtk_get_major_version() + << " " << gtk_get_minor_version() << " " << gtk_get_micro_version()); + + if (gtk_get_major_version() == 3 && gtk_get_minor_version() < 18) + { + g_warning("require gtk >= 3.18 for theme expectations"); + return nullptr; + } + + // for gtk2 it is always built with X support, so this is always called + // for gtk3 it is normally built with X and Wayland support, if + // X is supported GDK_WINDOWING_X11 is defined and this is always + // called, regardless of if we're running under X or Wayland. + // We can't use (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) to only do it under + // X, because we need to do it earlier than we have a display +#if defined(GDK_WINDOWING_X11) + /* #i92121# workaround deadlocks in the X11 implementation + */ + static const char* pNoXInitThreads = getenv( "SAL_NO_XINITTHREADS" ); + /* #i90094# + from now on we know that an X connection will be + established, so protect X against itself + */ + if( ! ( pNoXInitThreads && *pNoXInitThreads ) ) + XInitThreads(); +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + // init gdk thread protection + bool const sup = g_thread_supported(); + // extracted from the 'if' to avoid Clang -Wunreachable-code + if ( !sup ) + g_thread_init( nullptr ); + + gdk_threads_set_lock_functions (GdkThreadsEnter, GdkThreadsLeave); + SAL_INFO("vcl.gtk", "Hooked gdk threads locks"); +#endif + + auto pYieldMutex = std::make_unique(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gdk_threads_init(); +#endif + + GtkInstance* pInstance = new GtkInstance( std::move(pYieldMutex) ); + SAL_INFO("vcl.gtk", "creating GtkInstance " << pInstance); + + // Create SalData, this does not leak + new GtkSalData(); + + return pInstance; + } +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +static VclInputFlags categorizeEvent(const GdkEvent *pEvent) +{ + VclInputFlags nType = VclInputFlags::NONE; + switch (gdk_event_get_event_type(pEvent)) + { + case GDK_MOTION_NOTIFY: + case GDK_BUTTON_PRESS: +#if !GTK_CHECK_VERSION(4, 0, 0) + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: +#endif + case GDK_BUTTON_RELEASE: + case GDK_ENTER_NOTIFY: + case GDK_LEAVE_NOTIFY: + case GDK_SCROLL: + nType = VclInputFlags::MOUSE; + break; + case GDK_KEY_PRESS: + // case GDK_KEY_RELEASE: //similar to the X11SalInstance one + nType = VclInputFlags::KEYBOARD; + break; +#if !GTK_CHECK_VERSION(4, 0, 0) + case GDK_EXPOSE: + nType = VclInputFlags::PAINT; + break; +#endif + default: + nType = VclInputFlags::OTHER; + break; + } + return nType; +} +#endif + +GtkInstance::GtkInstance( std::unique_ptr pMutex ) + : SvpSalInstance( std::move(pMutex) ) + , m_pTimer(nullptr) + , bNeedsInit(true) + , m_pLastCairoFontOptions(nullptr) +{ + m_bSupportsOpenGL = true; +} + +//We want to defer initializing gtk until we are after uno has been +//bootstrapped so we can ask the config what the UI language is so that we can +//force that in as $LANGUAGE to get gtk to render widgets RTL if we have a RTL +//UI in a LTR locale +void GtkInstance::AfterAppInit() +{ + EnsureInit(); +} + +void GtkInstance::EnsureInit() +{ + if (!bNeedsInit) + return; + // initialize SalData + GtkSalData *pSalData = GetGtkSalData(); + pSalData->Init(); + GtkSalData::initNWF(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + InitAtkBridge(); +#endif + + ImplSVData* pSVData = ImplGetSVData(); +#ifdef GTK_TOOLKIT_NAME + pSVData->maAppData.mxToolkitName = OUString(GTK_TOOLKIT_NAME); +#else + pSVData->maAppData.mxToolkitName = OUString("gtk3"); +#endif + + bNeedsInit = false; +} + +GtkInstance::~GtkInstance() +{ + assert( nullptr == m_pTimer ); +#if !GTK_CHECK_VERSION(4, 0, 0) + DeInitAtkBridge(); +#endif + ResetLastSeenCairoFontOptions(nullptr); +} + +SalFrame* GtkInstance::CreateFrame( SalFrame* pParent, SalFrameStyleFlags nStyle ) +{ + EnsureInit(); + return new GtkSalFrame( pParent, nStyle ); +} + +SalFrame* GtkInstance::CreateChildFrame( SystemParentData* pParentData, SalFrameStyleFlags ) +{ + EnsureInit(); + return new GtkSalFrame( pParentData ); +} + +SalObject* GtkInstance::CreateObject( SalFrame* pParent, SystemWindowData* pWindowData, bool bShow ) +{ + EnsureInit(); + //FIXME: Missing CreateObject functionality ... + if (pWindowData && pWindowData->bClipUsingNativeWidget) + return new GtkSalObjectWidgetClip(static_cast(pParent), bShow); + return new GtkSalObject(static_cast(pParent), bShow); +} + +extern "C" +{ + typedef void*(* getDefaultFnc)(); + typedef void(* addItemFnc)(void *, const char *); +} + +void GtkInstance::AddToRecentDocumentList(const OUString& rFileUrl, const OUString&, const OUString&) +{ + EnsureInit(); + OString sGtkURL; + rtl_TextEncoding aSystemEnc = osl_getThreadTextEncoding(); + if ((aSystemEnc == RTL_TEXTENCODING_UTF8) || !rFileUrl.startsWith( "file://" )) + sGtkURL = OUStringToOString(rFileUrl, RTL_TEXTENCODING_UTF8); + else + { + //Non-utf8 locales are a bad idea if trying to work with non-ascii filenames + //Decode %XX components + OUString sDecodedUri = rtl::Uri::decode(rFileUrl.copy(7), rtl_UriDecodeToIuri, RTL_TEXTENCODING_UTF8); + //Convert back to system locale encoding + OString sSystemUrl = OUStringToOString(sDecodedUri, aSystemEnc); + //Encode to an escaped ASCII-encoded URI + gchar *g_uri = g_filename_to_uri(sSystemUrl.getStr(), nullptr, nullptr); + sGtkURL = OString(g_uri); + g_free(g_uri); + } + GtkRecentManager *manager = gtk_recent_manager_get_default (); + gtk_recent_manager_add_item (manager, sGtkURL.getStr()); +} + +SalInfoPrinter* GtkInstance::CreateInfoPrinter( SalPrinterQueueInfo* pQueueInfo, + ImplJobSetup* pSetupData ) +{ + EnsureInit(); + mbPrinterInit = true; + // create and initialize SalInfoPrinter + PspSalInfoPrinter* pPrinter = new PspSalInfoPrinter; + configurePspInfoPrinter(pPrinter, pQueueInfo, pSetupData); + return pPrinter; +} + +std::unique_ptr GtkInstance::CreatePrinter( SalInfoPrinter* pInfoPrinter ) +{ + EnsureInit(); + mbPrinterInit = true; + return std::unique_ptr(new PspSalPrinter(pInfoPrinter)); +} + +/* + * These methods always occur in pairs + * A ThreadsEnter is followed by a ThreadsLeave + * We need to queue up the recursive lock count + * for each pair, so we can accurately restore + * it later. + */ +thread_local std::stack GtkYieldMutex::yieldCounts; + +void GtkYieldMutex::ThreadsEnter() +{ + acquire(); + if (yieldCounts.empty()) + return; + auto n = yieldCounts.top(); + yieldCounts.pop(); + + const bool bUndoingLeaveWithoutEnter = n == 0; + // if the ThreadsLeave bLeaveWithoutEnter of true condition occurred to + // create this entry then return early undoing the initial acquire of the + // function + if G_UNLIKELY(bUndoingLeaveWithoutEnter) + { + release(); + return; + } + + assert(n > 0); + n--; + if (n > 0) + acquire(n); +} + +void GtkYieldMutex::ThreadsLeave() +{ + const bool bLeaveWithoutEnter = m_nCount == 0; + SAL_WARN_IF(bLeaveWithoutEnter, "vcl.gtk", "gdk_threads_leave without matching gdk_threads_enter"); + yieldCounts.push(m_nCount); + if G_UNLIKELY(bLeaveWithoutEnter) // this ideally shouldn't happen, but can due to the gtk3 file dialog + return; + release(true); +} + +std::unique_ptr GtkInstance::CreateVirtualDevice( SalGraphics &rG, + tools::Long &nDX, tools::Long &nDY, + DeviceFormat /*eFormat*/, + const SystemGraphicsData* pGd ) +{ + EnsureInit(); + SvpSalGraphics *pSvpSalGraphics = dynamic_cast(&rG); + assert(pSvpSalGraphics); + // tdf#127529 see SvpSalInstance::CreateVirtualDevice for the rare case of a non-null pPreExistingTarget + cairo_surface_t* pPreExistingTarget = pGd ? static_cast(pGd->pSurface) : nullptr; + std::unique_ptr xNew(new SvpSalVirtualDevice(pSvpSalGraphics->getSurface(), pPreExistingTarget)); + if (!xNew->SetSize(nDX, nDY)) + xNew.reset(); + return xNew; +} + +std::shared_ptr GtkInstance::CreateSalBitmap() +{ + EnsureInit(); + return SvpSalInstance::CreateSalBitmap(); +} + +std::unique_ptr GtkInstance::CreateMenu( bool bMenuBar, Menu* pVCLMenu ) +{ + EnsureInit(); + GtkSalMenu* pSalMenu = new GtkSalMenu( bMenuBar ); + pSalMenu->SetMenu( pVCLMenu ); + return std::unique_ptr(pSalMenu); +} + +std::unique_ptr GtkInstance::CreateMenuItem( const SalItemParams & rItemData ) +{ + EnsureInit(); + return std::unique_ptr(new GtkSalMenuItem( &rItemData )); +} + +SalTimer* GtkInstance::CreateSalTimer() +{ + EnsureInit(); + assert( nullptr == m_pTimer ); + if ( nullptr == m_pTimer ) + m_pTimer = new GtkSalTimer(); + return m_pTimer; +} + +void GtkInstance::RemoveTimer () +{ + EnsureInit(); + m_pTimer = nullptr; +} + +bool GtkInstance::DoYield(bool bWait, bool bHandleAllCurrentEvents) +{ + EnsureInit(); + return GetGtkSalData()->Yield( bWait, bHandleAllCurrentEvents ); +} + +bool GtkInstance::IsTimerExpired() +{ + EnsureInit(); + return (m_pTimer && m_pTimer->Expired()); +} + +namespace +{ + bool DisplayHasAnyInput() + { + GdkDisplay* pDisplay = gdk_display_get_default(); +#if defined(GDK_WINDOWING_WAYLAND) + if (DLSYM_GDK_IS_WAYLAND_DISPLAY(pDisplay)) + { + bool bRet = false; + wl_display* pWLDisplay = gdk_wayland_display_get_wl_display(pDisplay); + static auto wayland_display_get_fd = reinterpret_cast(dlsym(nullptr, "wl_display_get_fd")); + if (wayland_display_get_fd) + { + GPollFD aPollFD; + aPollFD.fd = wayland_display_get_fd(pWLDisplay); + aPollFD.events = G_IO_IN | G_IO_ERR | G_IO_HUP; + bRet = g_poll(&aPollFD, 1, 0) > 0; + } + return bRet; + } +#endif +#if defined(GDK_WINDOWING_X11) + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + { + GPollFD aPollFD; + aPollFD.fd = ConnectionNumber(gdk_x11_display_get_xdisplay(pDisplay)); + aPollFD.events = G_IO_IN; + return g_poll(&aPollFD, 1, 0) > 0; + } +#endif + return false; + } +} + +bool GtkInstance::AnyInput( VclInputFlags nType ) +{ + EnsureInit(); + if( (nType & VclInputFlags::TIMER) && IsTimerExpired() ) + return true; + + // strip timer bits now + nType = nType & ~VclInputFlags::TIMER; + + static constexpr VclInputFlags ANY_INPUT_EXCLUDING_TIMER = VCL_INPUT_ANY & ~VclInputFlags::TIMER; + + const bool bCheckForAnyInput = nType == ANY_INPUT_EXCLUDING_TIMER; + + bool bRet = false; + + if (bCheckForAnyInput) + bRet = DisplayHasAnyInput(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkDisplay* pDisplay = gdk_display_get_default(); + if (!gdk_display_has_pending(pDisplay)) + return bRet; + + if (bCheckForAnyInput) + return true; + + std::deque aEvents; + GdkEvent *pEvent = nullptr; + while ((pEvent = gdk_display_get_event(pDisplay))) + { + aEvents.push_back(pEvent); + VclInputFlags nEventType = categorizeEvent(pEvent); + if ( (nEventType & nType) || ( nEventType == VclInputFlags::NONE && (nType & VclInputFlags::OTHER) ) ) + { + bRet = true; + } + } + + while (!aEvents.empty()) + { + pEvent = aEvents.front(); + gdk_display_put_event(pDisplay, pEvent); + gdk_event_free(pEvent); + aEvents.pop_front(); + } +#endif + + return bRet; +} + +std::unique_ptr GtkInstance::CreatePrintGraphics() +{ + EnsureInit(); + return std::make_unique(); +} + +const cairo_font_options_t* GtkInstance::GetCairoFontOptions() +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + const cairo_font_options_t* pCairoFontOptions = gdk_screen_get_font_options(gdk_screen_get_default()); +#else + auto pDefaultWin = ImplGetDefaultWindow(); + assert(pDefaultWin); + SalFrame* pDefaultFrame = pDefaultWin->ImplGetFrame(); + GtkSalFrame* pGtkFrame = dynamic_cast(pDefaultFrame); + assert(pGtkFrame); + const cairo_font_options_t* pCairoFontOptions = pGtkFrame->get_font_options(); +#endif + if (!m_pLastCairoFontOptions && pCairoFontOptions) + m_pLastCairoFontOptions = cairo_font_options_copy(pCairoFontOptions); + return pCairoFontOptions; +} + +const cairo_font_options_t* GtkInstance::GetLastSeenCairoFontOptions() const +{ + return m_pLastCairoFontOptions; +} + +void GtkInstance::ResetLastSeenCairoFontOptions(const cairo_font_options_t* pCairoFontOptions) +{ + if (m_pLastCairoFontOptions) + cairo_font_options_destroy(m_pLastCairoFontOptions); + if (pCairoFontOptions) + m_pLastCairoFontOptions = cairo_font_options_copy(pCairoFontOptions); + else + m_pLastCairoFontOptions = nullptr; +} + + +namespace +{ + struct TypeEntry + { + const char* pNativeType; // string corresponding to nAtom for the case of nAtom being uninitialized + const char* pType; // Mime encoding on our side + }; + + const TypeEntry aConversionTab[] = + { + { "ISO10646-1", "text/plain;charset=utf-16" }, + { "UTF8_STRING", "text/plain;charset=utf-8" }, + { "UTF-8", "text/plain;charset=utf-8" }, + { "text/plain;charset=UTF-8", "text/plain;charset=utf-8" }, + // ISO encodings + { "ISO8859-2", "text/plain;charset=iso8859-2" }, + { "ISO8859-3", "text/plain;charset=iso8859-3" }, + { "ISO8859-4", "text/plain;charset=iso8859-4" }, + { "ISO8859-5", "text/plain;charset=iso8859-5" }, + { "ISO8859-6", "text/plain;charset=iso8859-6" }, + { "ISO8859-7", "text/plain;charset=iso8859-7" }, + { "ISO8859-8", "text/plain;charset=iso8859-8" }, + { "ISO8859-9", "text/plain;charset=iso8859-9" }, + { "ISO8859-10", "text/plain;charset=iso8859-10" }, + { "ISO8859-13", "text/plain;charset=iso8859-13" }, + { "ISO8859-14", "text/plain;charset=iso8859-14" }, + { "ISO8859-15", "text/plain;charset=iso8859-15" }, + // asian encodings + { "JISX0201.1976-0", "text/plain;charset=jisx0201.1976-0" }, + { "JISX0208.1983-0", "text/plain;charset=jisx0208.1983-0" }, + { "JISX0208.1990-0", "text/plain;charset=jisx0208.1990-0" }, + { "JISX0212.1990-0", "text/plain;charset=jisx0212.1990-0" }, + { "GB2312.1980-0", "text/plain;charset=gb2312.1980-0" }, + { "KSC5601.1992-0", "text/plain;charset=ksc5601.1992-0" }, + // eastern european encodings + { "KOI8-R", "text/plain;charset=koi8-r" }, + { "KOI8-U", "text/plain;charset=koi8-u" }, + // String (== iso8859-1) + { "STRING", "text/plain;charset=iso8859-1" }, + // special for compound text + { "COMPOUND_TEXT", "text/plain;charset=compound_text" }, + + // PIXMAP + { "PIXMAP", "image/bmp" } + }; + + class DataFlavorEq + { + private: + const css::datatransfer::DataFlavor& m_rData; + public: + explicit DataFlavorEq(const css::datatransfer::DataFlavor& rData) : m_rData(rData) {} + bool operator() (const css::datatransfer::DataFlavor& rData) const + { + return rData.MimeType == m_rData.MimeType && + rData.DataType == m_rData.DataType; + } + }; +} + +#if GTK_CHECK_VERSION(4, 0, 0) +std::vector GtkTransferable::getTransferDataFlavorsAsVector(const char * const *targets, gint n_targets) +#else +std::vector GtkTransferable::getTransferDataFlavorsAsVector(GdkAtom *targets, gint n_targets) +#endif +{ + std::vector aVector; + + bool bHaveText = false, bHaveUTF16 = false; + + for (gint i = 0; i < n_targets; ++i) + { +#if GTK_CHECK_VERSION(4, 0, 0) + const gchar* pName = targets[i]; +#else + gchar* pName = gdk_atom_name(targets[i]); +#endif + const char* pFinalName = pName; + css::datatransfer::DataFlavor aFlavor; + + // omit text/plain;charset=unicode since it is not well defined + if (rtl_str_compare(pName, "text/plain;charset=unicode") == 0) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + g_free(pName); +#endif + continue; + } + + for (size_t j = 0; j < SAL_N_ELEMENTS(aConversionTab); ++j) + { + if (rtl_str_compare(pName, aConversionTab[j].pNativeType) == 0) + { + pFinalName = aConversionTab[j].pType; + break; + } + } + + // There are more non-MIME-types reported that are not translated by + // aConversionTab, like "SAVE_TARGETS", "INTEGER", "ATOM"; just filter + // them out for now before they confuse this code's clients: + if (rtl_str_indexOfChar(pFinalName, '/') == -1) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + g_free(pName); +#endif + continue; + } + + aFlavor.MimeType = OUString(pFinalName, + strlen(pFinalName), + RTL_TEXTENCODING_UTF8); + + m_aMimeTypeToGtkType[aFlavor.MimeType] = targets[i]; + + aFlavor.DataType = cppu::UnoType>::get(); + + sal_Int32 nIndex(0); + if (o3tl::getToken(aFlavor.MimeType, 0, ';', nIndex) == u"text/plain") + { + bHaveText = true; + std::u16string_view aToken(o3tl::getToken(aFlavor.MimeType, 0, ';', nIndex)); + if (aToken == u"charset=utf-16") + { + bHaveUTF16 = true; + aFlavor.DataType = cppu::UnoType::get(); + } + } + aVector.push_back(aFlavor); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_free(pName); +#endif + } + + //If we have text, but no UTF-16 format which is basically the only + //text-format LibreOffice supports for cnp then claim we do and we + //will convert on demand + if (bHaveText && !bHaveUTF16) + { + css::datatransfer::DataFlavor aFlavor; + aFlavor.MimeType = "text/plain;charset=utf-16"; + aFlavor.DataType = cppu::UnoType::get(); + aVector.push_back(aFlavor); + } + + return aVector; +} + +css::uno::Sequence SAL_CALL GtkTransferable::getTransferDataFlavors() +{ + return comphelper::containerToSequence(getTransferDataFlavorsAsVector()); +} + +sal_Bool SAL_CALL GtkTransferable::isDataFlavorSupported(const css::datatransfer::DataFlavor& rFlavor) +{ + const std::vector aAll = + getTransferDataFlavorsAsVector(); + + return std::any_of(aAll.begin(), aAll.end(), DataFlavorEq(rFlavor)); +} + +#if GTK_CHECK_VERSION(4, 0, 0) +void read_transfer_result::read_block_async_completed(GObject* source, GAsyncResult* res, gpointer user_data) +{ + GInputStream* stream = G_INPUT_STREAM(source); + read_transfer_result* pRes = static_cast(user_data); + + gsize bytes_read = g_input_stream_read_finish(stream, res, nullptr); + + bool bFinished = bytes_read == 0; + + if (bFinished) + { + g_object_unref(stream); + pRes->aVector.resize(pRes->nRead); + pRes->bDone = true; + g_main_context_wakeup(nullptr); + return; + } + + pRes->nRead += bytes_read; + + pRes->aVector.resize(pRes->nRead + read_transfer_result::BlockSize); + + g_input_stream_read_async(stream, + pRes->aVector.data() + pRes->nRead, + read_transfer_result::BlockSize, + G_PRIORITY_DEFAULT, + nullptr, + read_block_async_completed, + user_data); +} + +OUString read_transfer_result::get_as_string() const +{ + const char* pStr = reinterpret_cast(aVector.data()); + return OUString(pStr, aVector.size(), RTL_TEXTENCODING_UTF8).replaceAll("\r\n", "\n"); +} + +css::uno::Sequence read_transfer_result::get_as_sequence() const +{ + return css::uno::Sequence(aVector.data(), aVector.size()); +} +#endif + +namespace { + +GdkClipboard* clipboard_get(SelectionType eSelection) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + if (eSelection == SELECTION_CLIPBOARD) + return gdk_display_get_clipboard(gdk_display_get_default()); + return gdk_display_get_primary_clipboard(gdk_display_get_default()); +#else + return gtk_clipboard_get(eSelection == SELECTION_CLIPBOARD ? GDK_SELECTION_CLIPBOARD : GDK_SELECTION_PRIMARY); +#endif +} + +#if GTK_CHECK_VERSION(4, 0, 0) + +void read_clipboard_async_completed(GObject* source, GAsyncResult* res, gpointer user_data) +{ + GdkClipboard* clipboard = GDK_CLIPBOARD(source); + read_transfer_result* pRes = static_cast(user_data); + + GInputStream* pResult = gdk_clipboard_read_finish(clipboard, res, nullptr, nullptr); + + if (!pResult) + { + pRes->bDone = true; + g_main_context_wakeup(nullptr); + return; + } + + pRes->aVector.resize(read_transfer_result::BlockSize); + + g_input_stream_read_async(pResult, + pRes->aVector.data(), + pRes->aVector.size(), + G_PRIORITY_DEFAULT, + nullptr, + read_transfer_result::read_block_async_completed, + user_data); +} + +#endif + +class GtkClipboardTransferable : public GtkTransferable +{ +private: + SelectionType m_eSelection; + +public: + + explicit GtkClipboardTransferable(SelectionType eSelection) + : m_eSelection(eSelection) + { + } + + /* + * XTransferable + */ + + virtual css::uno::Any SAL_CALL getTransferData(const css::datatransfer::DataFlavor& rFlavor) override + { + css::uno::Any aRet; + + css::datatransfer::DataFlavor aFlavor(rFlavor); + if (aFlavor.MimeType == "text/plain;charset=utf-16") + aFlavor.MimeType = "text/plain;charset=utf-8"; + + GdkClipboard* clipboard = clipboard_get(m_eSelection); + +#if !GTK_CHECK_VERSION(4, 0, 0) + if (aFlavor.MimeType == "text/plain;charset=utf-8") + { + gchar *pText = gtk_clipboard_wait_for_text(clipboard); + OUString aStr(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + g_free(pText); + aRet <<= aStr.replaceAll("\r\n", "\n"); + return aRet; + } +#endif + + auto it = m_aMimeTypeToGtkType.find(aFlavor.MimeType); + if (it == m_aMimeTypeToGtkType.end()) + return css::uno::Any(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkSelectionData* data = gtk_clipboard_wait_for_contents(clipboard, + it->second); + if (!data) + { + return css::uno::Any(); + } + gint length; + const guchar *rawdata = gtk_selection_data_get_data_with_length(data, + &length); + Sequence aSeq(reinterpret_cast(rawdata), length); + gtk_selection_data_free(data); + aRet <<= aSeq; +#else + SalInstance* pInstance = GetSalInstance(); + read_transfer_result aRes; + const char *mime_types[] = { it->second.getStr(), nullptr }; + + gdk_clipboard_read_async(clipboard, + mime_types, + G_PRIORITY_DEFAULT, + nullptr, + read_clipboard_async_completed, + &aRes); + + while (!aRes.bDone) + pInstance->DoYield(true, false); + + if (aFlavor.MimeType == "text/plain;charset=utf-8") + aRet <<= aRes.get_as_string(); + else + aRet <<= aRes.get_as_sequence(); +#endif + return aRet; + } + + std::vector getTransferDataFlavorsAsVector() + override + { + std::vector aVector; + + GdkClipboard* clipboard = clipboard_get(m_eSelection); + +#if GTK_CHECK_VERSION(4, 0, 0) + GdkContentFormats* pFormats = gdk_clipboard_get_formats(clipboard); + gsize n_targets; + const char * const *targets = gdk_content_formats_get_mime_types(pFormats, &n_targets); + aVector = GtkTransferable::getTransferDataFlavorsAsVector(targets, n_targets); +#else + GdkAtom *targets; + gint n_targets; + if (gtk_clipboard_wait_for_targets(clipboard, &targets, &n_targets)) + { + aVector = GtkTransferable::getTransferDataFlavorsAsVector(targets, n_targets); + g_free(targets); + } +#endif + + return aVector; + } +}; + +class VclGtkClipboard : + public cppu::WeakComponentImplHelper< + datatransfer::clipboard::XSystemClipboard, + datatransfer::clipboard::XFlushableClipboard, + XServiceInfo> +{ + SelectionType m_eSelection; + osl::Mutex m_aMutex; + gulong m_nOwnerChangedSignalId; + ImplSVEvent* m_pSetClipboardEvent; + Reference m_aContents; + Reference m_aOwner; + std::vector< Reference > m_aListeners; +#if GTK_CHECK_VERSION(4, 0, 0) + std::vector m_aGtkTargets; + TransferableContent* m_pClipboardContent; +#else + std::vector m_aGtkTargets; +#endif + VclToGtkHelper m_aConversionHelper; + + DECL_LINK(AsyncSetGtkClipboard, void*, void); + +#if GTK_CHECK_VERSION(4, 0, 0) + DECL_LINK(DetachClipboard, void*, void); +#endif + +public: + + explicit VclGtkClipboard(SelectionType eSelection); + virtual ~VclGtkClipboard() override; + + /* + * XServiceInfo + */ + + virtual OUString SAL_CALL getImplementationName() override; + virtual sal_Bool SAL_CALL supportsService( const OUString& ServiceName ) override; + virtual Sequence< OUString > SAL_CALL getSupportedServiceNames() override; + + /* + * XClipboard + */ + + virtual Reference< css::datatransfer::XTransferable > SAL_CALL getContents() override; + + virtual void SAL_CALL setContents( + const Reference< css::datatransfer::XTransferable >& xTrans, + const Reference< css::datatransfer::clipboard::XClipboardOwner >& xClipboardOwner ) override; + + virtual OUString SAL_CALL getName() override; + + /* + * XClipboardEx + */ + + virtual sal_Int8 SAL_CALL getRenderingCapabilities() override; + + /* + * XFlushableClipboard + */ + virtual void SAL_CALL flushClipboard() override; + + /* + * XClipboardNotifier + */ + virtual void SAL_CALL addClipboardListener( + const Reference< css::datatransfer::clipboard::XClipboardListener >& listener ) override; + + virtual void SAL_CALL removeClipboardListener( + const Reference< css::datatransfer::clipboard::XClipboardListener >& listener ) override; + +#if !GTK_CHECK_VERSION(4, 0, 0) + void ClipboardGet(GtkSelectionData *selection_data, guint info); +#endif + void OwnerPossiblyChanged(GdkClipboard *clipboard); + void ClipboardClear(); + void SetGtkClipboard(); + void SyncGtkClipboard(); +}; + +} + +OUString VclGtkClipboard::getImplementationName() +{ + return "com.sun.star.datatransfer.VclGtkClipboard"; +} + +Sequence< OUString > VclGtkClipboard::getSupportedServiceNames() +{ + Sequence aRet { "com.sun.star.datatransfer.clipboard.SystemClipboard" }; + return aRet; +} + +sal_Bool VclGtkClipboard::supportsService( const OUString& ServiceName ) +{ + return cppu::supportsService(this, ServiceName); +} + +Reference< css::datatransfer::XTransferable > VclGtkClipboard::getContents() +{ + if (!m_aContents.is()) + { + //tdf#93887 This is the system clipboard/selection. We fetch it when we are not + //the owner of the clipboard and have not already fetched it. + m_aContents = new GtkClipboardTransferable(m_eSelection); +#if GTK_CHECK_VERSION(4, 0, 0) + if (m_pClipboardContent) + transerable_content_set_transferable(m_pClipboardContent, m_aContents.get()); +#endif + } + return m_aContents; +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +void VclGtkClipboard::ClipboardGet(GtkSelectionData *selection_data, guint info) +{ + if (!m_aContents.is()) + return; + // tdf#129809 take a reference in case m_aContents is replaced during this + // call + Reference xCurrentContents(m_aContents); + m_aConversionHelper.setSelectionData(xCurrentContents, selection_data, info); +} + +namespace +{ + const OString& getPID() + { + static OString sPID; + if (!sPID.getLength()) + { + oslProcessIdentifier aProcessId = 0; + oslProcessInfo info; + info.Size = sizeof (oslProcessInfo); + if (osl_getProcessInfo(nullptr, osl_Process_IDENTIFIER, &info) == osl_Process_E_None) + aProcessId = info.Ident; + sPID = OString::number(aProcessId); + } + return sPID; + } + + void ClipboardGetFunc(GdkClipboard* /*clipboard*/, GtkSelectionData *selection_data, + guint info, + gpointer user_data_or_owner) + { + VclGtkClipboard* pThis = static_cast(user_data_or_owner); + pThis->ClipboardGet(selection_data, info); + } + + void ClipboardClearFunc(GdkClipboard* /*clipboard*/, gpointer user_data_or_owner) + { + VclGtkClipboard* pThis = static_cast(user_data_or_owner); + pThis->ClipboardClear(); + } +} +#endif + +namespace +{ +#if GTK_CHECK_VERSION(4, 0, 0) + void handle_owner_change(GdkClipboard *clipboard, gpointer user_data) + { + VclGtkClipboard* pThis = static_cast(user_data); + pThis->OwnerPossiblyChanged(clipboard); + } +#else + void handle_owner_change(GdkClipboard *clipboard, GdkEvent* /*event*/, gpointer user_data) + { + VclGtkClipboard* pThis = static_cast(user_data); + pThis->OwnerPossiblyChanged(clipboard); + } +#endif +} + +void VclGtkClipboard::OwnerPossiblyChanged(GdkClipboard* clipboard) +{ + SyncGtkClipboard(); // tdf#138183 do any pending SetGtkClipboard calls + if (!m_aContents.is()) + return; + +#if GTK_CHECK_VERSION(4, 0, 0) + bool bSelf = gdk_clipboard_is_local(clipboard); +#else + //if gdk_display_supports_selection_notification is not supported, e.g. like + //right now under wayland, then you only get owner-changed notifications at + //opportune times when the selection might have changed. So here + //we see if the selection supports a dummy selection type identifying + //our pid, in which case it's us. + bool bSelf = false; + + //disconnect and reconnect after gtk_clipboard_wait_for_targets to + //avoid possible recursion + g_signal_handler_disconnect(clipboard, m_nOwnerChangedSignalId); + + OString sTunnel = "application/x-libreoffice-internal-id-" + getPID(); + GdkAtom *targets; + gint n_targets; + if (gtk_clipboard_wait_for_targets(clipboard, &targets, &n_targets)) + { + for (gint i = 0; i < n_targets && !bSelf; ++i) + { + gchar* pName = gdk_atom_name(targets[i]); + if (strcmp(pName, sTunnel.getStr()) == 0) + { + bSelf = true; + } + g_free(pName); + } + + g_free(targets); + } + + m_nOwnerChangedSignalId = g_signal_connect(clipboard, "owner-change", + G_CALLBACK(handle_owner_change), this); +#endif + + if (!bSelf) + { + //null out m_aContents to return control to the system-one which + //will be retrieved if getContents is called again + setContents(Reference(), + Reference()); + } +} + +void VclGtkClipboard::ClipboardClear() +{ + if (m_pSetClipboardEvent) + { + Application::RemoveUserEvent(m_pSetClipboardEvent); + m_pSetClipboardEvent = nullptr; + } +#if !GTK_CHECK_VERSION(4, 0, 0) + for (auto &a : m_aGtkTargets) + g_free(a.target); +#endif + m_aGtkTargets.clear(); +} + +#if GTK_CHECK_VERSION(4, 0, 0) +IMPL_LINK_NOARG(VclGtkClipboard, DetachClipboard, void*, void) +{ + ClipboardClear(); +} + +OString VclToGtkHelper::makeGtkTargetEntry(const css::datatransfer::DataFlavor& rFlavor) +{ + OString aEntry = OUStringToOString(rFlavor.MimeType, RTL_TEXTENCODING_UTF8); + auto it = std::find_if(aInfoToFlavor.begin(), aInfoToFlavor.end(), + DataFlavorEq(rFlavor)); + if (it == aInfoToFlavor.end()) + aInfoToFlavor.push_back(rFlavor); + return aEntry; +} +#else +GtkTargetEntry VclToGtkHelper::makeGtkTargetEntry(const css::datatransfer::DataFlavor& rFlavor) +{ + GtkTargetEntry aEntry; + aEntry.target = + g_strdup(OUStringToOString(rFlavor.MimeType, RTL_TEXTENCODING_UTF8).getStr()); + aEntry.flags = 0; + auto it = std::find_if(aInfoToFlavor.begin(), aInfoToFlavor.end(), + DataFlavorEq(rFlavor)); + if (it != aInfoToFlavor.end()) + aEntry.info = std::distance(aInfoToFlavor.begin(), it); + else + { + aEntry.info = aInfoToFlavor.size(); + aInfoToFlavor.push_back(rFlavor); + } + return aEntry; +} +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + +namespace +{ + void write_mime_type_done(GObject* pStream, GAsyncResult* pResult, gpointer pTaskPtr) + { + GTask* pTask = static_cast(pTaskPtr); + + GError* pError = nullptr; + if (!g_output_stream_write_all_finish(G_OUTPUT_STREAM(pStream), + pResult, nullptr, &pError)) + { + g_task_return_error(pTask, pError); + } + else + { + g_task_return_boolean(pTask, true); + } + + g_object_unref(pTask); + } + + class MimeTypeEq + { + private: + const OUString& m_rMimeType; + public: + explicit MimeTypeEq(const OUString& rMimeType) : m_rMimeType(rMimeType) {} + bool operator() (const css::datatransfer::DataFlavor& rData) const + { + return rData.MimeType == m_rMimeType; + } + }; +} + +void VclToGtkHelper::setSelectionData(const Reference &rTrans, + GdkContentProvider* provider, + const char* mime_type, + GOutputStream* stream, + int io_priority, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task = g_task_new(provider, cancellable, callback, user_data); + g_task_set_priority(task, io_priority); + + OUString sMimeType(mime_type, strlen(mime_type), RTL_TEXTENCODING_UTF8); + + auto it = std::find_if(aInfoToFlavor.begin(), aInfoToFlavor.end(), + MimeTypeEq(sMimeType)); + if (it == aInfoToFlavor.end()) + { + SAL_WARN( "vcl.gtk", "unknown mime-type request from clipboard"); + g_task_return_new_error(task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "unknown mime-type “%s” request from clipboard", mime_type); + g_object_unref(task); + return; + } + + css::datatransfer::DataFlavor aFlavor(*it); + if (aFlavor.MimeType == "UTF8_STRING" || aFlavor.MimeType == "STRING") + aFlavor.MimeType = "text/plain;charset=utf-8"; + + Sequence aData; + Any aValue; + + try + { + aValue = rTrans->getTransferData(aFlavor); + } + catch (...) + { + } + + if (aValue.getValueTypeClass() == TypeClass_STRING) + { + OUString aString; + aValue >>= aString; + aData = Sequence< sal_Int8 >( reinterpret_cast(aString.getStr()), aString.getLength() * sizeof( sal_Unicode ) ); + } + else if (aValue.getValueType() == cppu::UnoType>::get()) + { + aValue >>= aData; + } + else if (aFlavor.MimeType == "text/plain;charset=utf-8") + { + //didn't have utf-8, try utf-16 and convert + aFlavor.MimeType = "text/plain;charset=utf-16"; + aFlavor.DataType = cppu::UnoType::get(); + try + { + aValue = rTrans->getTransferData(aFlavor); + } + catch (...) + { + } + OUString aString; + aValue >>= aString; + OString aUTF8String(OUStringToOString(aString, RTL_TEXTENCODING_UTF8)); + + g_output_stream_write_all_async(stream, aUTF8String.getStr(), aUTF8String.getLength(), + io_priority, cancellable, write_mime_type_done, task); + return; + } + + g_output_stream_write_all_async(stream, aData.getArray(), aData.getLength(), + io_priority, cancellable, write_mime_type_done, task); +} +#else +void VclToGtkHelper::setSelectionData(const Reference &rTrans, + GtkSelectionData *selection_data, guint info) +{ + GdkAtom type(gdk_atom_intern(OUStringToOString(aInfoToFlavor[info].MimeType, + RTL_TEXTENCODING_UTF8).getStr(), + false)); + + css::datatransfer::DataFlavor aFlavor(aInfoToFlavor[info]); + if (aFlavor.MimeType == "UTF8_STRING" || aFlavor.MimeType == "STRING") + aFlavor.MimeType = "text/plain;charset=utf-8"; + + Sequence aData; + Any aValue; + + try + { + aValue = rTrans->getTransferData(aFlavor); + } + catch (...) + { + } + + if (aValue.getValueTypeClass() == TypeClass_STRING) + { + OUString aString; + aValue >>= aString; + aData = Sequence< sal_Int8 >( reinterpret_cast(aString.getStr()), aString.getLength() * sizeof( sal_Unicode ) ); + } + else if (aValue.getValueType() == cppu::UnoType>::get()) + { + aValue >>= aData; + } + else if (aFlavor.MimeType == "text/plain;charset=utf-8") + { + //didn't have utf-8, try utf-16 and convert + aFlavor.MimeType = "text/plain;charset=utf-16"; + aFlavor.DataType = cppu::UnoType::get(); + try + { + aValue = rTrans->getTransferData(aFlavor); + } + catch (...) + { + } + OUString aString; + aValue >>= aString; + OString aUTF8String(OUStringToOString(aString, RTL_TEXTENCODING_UTF8)); + gtk_selection_data_set(selection_data, type, 8, + reinterpret_cast(aUTF8String.getStr()), + aUTF8String.getLength()); + return; + } + + gtk_selection_data_set(selection_data, type, 8, + reinterpret_cast(aData.getArray()), + aData.getLength()); +} +#endif + +VclGtkClipboard::VclGtkClipboard(SelectionType eSelection) + : cppu::WeakComponentImplHelper + (m_aMutex) + , m_eSelection(eSelection) + , m_pSetClipboardEvent(nullptr) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_pClipboardContent(nullptr) +#endif +{ + GdkClipboard* clipboard = clipboard_get(m_eSelection); +#if GTK_CHECK_VERSION(4, 0, 0) + m_nOwnerChangedSignalId = g_signal_connect(clipboard, "changed", + G_CALLBACK(handle_owner_change), this); +#else + m_nOwnerChangedSignalId = g_signal_connect(clipboard, "owner-change", + G_CALLBACK(handle_owner_change), this); +#endif +} + +void VclGtkClipboard::flushClipboard() +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + SolarMutexGuard aGuard; + + if (m_eSelection != SELECTION_CLIPBOARD) + return; + + GdkClipboard* clipboard = clipboard_get(m_eSelection); + gtk_clipboard_store(clipboard); +#endif +} + +VclGtkClipboard::~VclGtkClipboard() +{ + GdkClipboard* clipboard = clipboard_get(m_eSelection); + g_signal_handler_disconnect(clipboard, m_nOwnerChangedSignalId); + if (!m_aGtkTargets.empty()) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gdk_clipboard_set_content(clipboard, nullptr); + m_pClipboardContent = nullptr; +#else + gtk_clipboard_clear(clipboard); +#endif + ClipboardClear(); + } + assert(!m_pSetClipboardEvent); + assert(m_aGtkTargets.empty()); +} + +#if GTK_CHECK_VERSION(4, 0, 0) +std::vector VclToGtkHelper::FormatsToGtk(const css::uno::Sequence &rFormats) +#else +std::vector VclToGtkHelper::FormatsToGtk(const css::uno::Sequence &rFormats) +#endif +{ +#if GTK_CHECK_VERSION(4, 0, 0) + std::vector aGtkTargets; +#else + std::vector aGtkTargets; +#endif + + bool bHaveText(false), bHaveUTF8(false); + for (const css::datatransfer::DataFlavor& rFlavor : rFormats) + { + sal_Int32 nIndex(0); + if (o3tl::getToken(rFlavor.MimeType, 0, ';', nIndex) == u"text/plain") + { + bHaveText = true; + std::u16string_view aToken(o3tl::getToken(rFlavor.MimeType, 0, ';', nIndex)); + if (aToken == u"charset=utf-8") + { + bHaveUTF8 = true; + } + } + aGtkTargets.push_back(makeGtkTargetEntry(rFlavor)); + } + + if (bHaveText) + { + css::datatransfer::DataFlavor aFlavor; + aFlavor.DataType = cppu::UnoType>::get(); + if (!bHaveUTF8) + { + aFlavor.MimeType = "text/plain;charset=utf-8"; + aGtkTargets.push_back(makeGtkTargetEntry(aFlavor)); + } + aFlavor.MimeType = "UTF8_STRING"; + aGtkTargets.push_back(makeGtkTargetEntry(aFlavor)); + aFlavor.MimeType = "STRING"; + aGtkTargets.push_back(makeGtkTargetEntry(aFlavor)); + } + + return aGtkTargets; +} + +IMPL_LINK_NOARG(VclGtkClipboard, AsyncSetGtkClipboard, void*, void) +{ + osl::Guard aGuard( m_aMutex ); + m_pSetClipboardEvent = nullptr; + SetGtkClipboard(); +} + +void VclGtkClipboard::SyncGtkClipboard() +{ + osl::Guard aGuard(m_aMutex); + if (m_pSetClipboardEvent) + { + Application::RemoveUserEvent(m_pSetClipboardEvent); + m_pSetClipboardEvent = nullptr; + SetGtkClipboard(); + } +} + +void VclGtkClipboard::SetGtkClipboard() +{ + GdkClipboard* clipboard = clipboard_get(m_eSelection); +#if GTK_CHECK_VERSION(4, 0, 0) + m_pClipboardContent = TRANSFERABLE_CONTENT(transerable_content_new(&m_aConversionHelper, m_aContents.get())); + transerable_content_set_detach_clipboard_link(m_pClipboardContent, LINK(this, VclGtkClipboard, DetachClipboard)); + gdk_clipboard_set_content(clipboard, GDK_CONTENT_PROVIDER(m_pClipboardContent)); +#else + gtk_clipboard_set_with_data(clipboard, m_aGtkTargets.data(), m_aGtkTargets.size(), + ClipboardGetFunc, ClipboardClearFunc, this); + gtk_clipboard_set_can_store(clipboard, m_aGtkTargets.data(), m_aGtkTargets.size()); +#endif +} + +void VclGtkClipboard::setContents( + const Reference< css::datatransfer::XTransferable >& xTrans, + const Reference< css::datatransfer::clipboard::XClipboardOwner >& xClipboardOwner ) +{ + css::uno::Sequence aFormats; + if (xTrans.is()) + { + aFormats = xTrans->getTransferDataFlavors(); + } + + osl::ClearableMutexGuard aGuard( m_aMutex ); + Reference< datatransfer::clipboard::XClipboardOwner > xOldOwner( m_aOwner ); + Reference< datatransfer::XTransferable > xOldContents( m_aContents ); + m_aContents = xTrans; +#if GTK_CHECK_VERSION(4, 0, 0) + if (m_pClipboardContent) + transerable_content_set_transferable(m_pClipboardContent, m_aContents.get()); +#endif + m_aOwner = xClipboardOwner; + + std::vector< Reference< datatransfer::clipboard::XClipboardListener > > aListeners( m_aListeners ); + datatransfer::clipboard::ClipboardEvent aEv; + + GdkClipboard* clipboard = clipboard_get(m_eSelection); + if (!m_aGtkTargets.empty()) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gdk_clipboard_set_content(clipboard, nullptr); + m_pClipboardContent = nullptr; +#else + gtk_clipboard_clear(clipboard); +#endif + ClipboardClear(); + } + assert(m_aGtkTargets.empty()); + if (m_aContents.is()) + { +#if GTK_CHECK_VERSION(4, 0, 0) + std::vector aGtkTargets(m_aConversionHelper.FormatsToGtk(aFormats)); +#else + std::vector aGtkTargets(m_aConversionHelper.FormatsToGtk(aFormats)); +#endif + if (!aGtkTargets.empty()) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkTargetEntry aEntry; + OString sTunnel = "application/x-libreoffice-internal-id-" + getPID(); + aEntry.target = g_strdup(sTunnel.getStr()); + aEntry.flags = 0; + aEntry.info = 0; + aGtkTargets.push_back(aEntry); +#endif + m_aGtkTargets = aGtkTargets; + + if (!m_pSetClipboardEvent) + m_pSetClipboardEvent = Application::PostUserEvent(LINK(this, VclGtkClipboard, AsyncSetGtkClipboard)); + } + } + + aEv.Contents = getContents(); + + aGuard.clear(); + + if (xOldOwner.is() && xOldOwner != xClipboardOwner) + xOldOwner->lostOwnership( this, xOldContents ); + for (auto const& listener : aListeners) + { + listener->changedContents( aEv ); + } +} + +OUString VclGtkClipboard::getName() +{ + return (m_eSelection == SELECTION_CLIPBOARD) ? OUString("CLIPBOARD") : OUString("PRIMARY"); +} + +sal_Int8 VclGtkClipboard::getRenderingCapabilities() +{ + return 0; +} + +void VclGtkClipboard::addClipboardListener( const Reference< datatransfer::clipboard::XClipboardListener >& listener ) +{ + osl::Guard aGuard( m_aMutex ); + + m_aListeners.push_back( listener ); +} + +void VclGtkClipboard::removeClipboardListener( const Reference< datatransfer::clipboard::XClipboardListener >& listener ) +{ + osl::Guard aGuard( m_aMutex ); + + std::erase(m_aListeners, listener); +} + +Reference< XInterface > GtkInstance::CreateClipboard(const Sequence< Any >& arguments) +{ + if ( IsRunningUnitTest() ) + return SalInstance::CreateClipboard( arguments ); + + OUString sel; + if (!arguments.hasElements()) { + sel = "CLIPBOARD"; + } else if (arguments.getLength() != 1 || !(arguments[0] >>= sel)) { + throw css::lang::IllegalArgumentException( + "bad GtkInstance::CreateClipboard arguments", + css::uno::Reference(), -1); + } + + SelectionType eSelection = (sel == "CLIPBOARD") ? SELECTION_CLIPBOARD : SELECTION_PRIMARY; + + if (m_aClipboards[eSelection].is()) + return m_aClipboards[eSelection]; + + Reference xClipboard(getXWeak(new VclGtkClipboard(eSelection))); + m_aClipboards[eSelection] = xClipboard; + return xClipboard; +} + +GtkInstDropTarget::GtkInstDropTarget() + : WeakComponentImplHelper(m_aMutex) + , m_pFrame(nullptr) + , m_pFormatConversionRequest(nullptr) + , m_bActive(false) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_bInDrag(false) +#endif + , m_nDefaultActions(0) +{ +} + +OUString SAL_CALL GtkInstDropTarget::getImplementationName() +{ + return "com.sun.star.datatransfer.dnd.VclGtkDropTarget"; +} + +sal_Bool SAL_CALL GtkInstDropTarget::supportsService(OUString const & ServiceName) +{ + return cppu::supportsService(this, ServiceName); +} + +css::uno::Sequence SAL_CALL GtkInstDropTarget::getSupportedServiceNames() +{ + Sequence aRet { "com.sun.star.datatransfer.dnd.GtkDropTarget" }; + return aRet; +} + +GtkInstDropTarget::~GtkInstDropTarget() +{ + if (m_pFrame) + m_pFrame->deregisterDropTarget(this); +} + +void GtkInstDropTarget::deinitialize() +{ + m_pFrame = nullptr; + m_bActive = false; +} + +void GtkInstDropTarget::initialize(const Sequence& rArguments) +{ + if (rArguments.getLength() < 2) + { + throw RuntimeException("DropTarget::initialize: Cannot install window event handler", + getXWeak()); + } + + sal_IntPtr nFrame = 0; + rArguments.getConstArray()[1] >>= nFrame; + + if (!nFrame) + { + throw RuntimeException("DropTarget::initialize: missing SalFrame", + getXWeak()); + } + + m_pFrame = reinterpret_cast(nFrame); + m_pFrame->registerDropTarget(this); + m_bActive = true; +} + +void GtkInstDropTarget::addDropTargetListener( const Reference< css::datatransfer::dnd::XDropTargetListener >& xListener) +{ + ::osl::Guard< ::osl::Mutex > aGuard( m_aMutex ); + + m_aListeners.push_back( xListener ); +} + +void GtkInstDropTarget::removeDropTargetListener( const Reference< css::datatransfer::dnd::XDropTargetListener >& xListener) +{ + ::osl::Guard< ::osl::Mutex > aGuard( m_aMutex ); + + std::erase(m_aListeners, xListener); +} + +void GtkInstDropTarget::fire_drop(const css::datatransfer::dnd::DropTargetDropEvent& dtde) +{ + osl::ClearableGuard aGuard( m_aMutex ); + std::vector> aListeners(m_aListeners); + aGuard.clear(); + + for (auto const& listener : aListeners) + { + listener->drop( dtde ); + } +} + +void GtkInstDropTarget::fire_dragEnter(const css::datatransfer::dnd::DropTargetDragEnterEvent& dtde) +{ + osl::ClearableGuard< ::osl::Mutex > aGuard( m_aMutex ); + std::vector> aListeners(m_aListeners); + aGuard.clear(); + + for (auto const& listener : aListeners) + { + listener->dragEnter( dtde ); + } +} + +void GtkInstDropTarget::fire_dragOver(const css::datatransfer::dnd::DropTargetDragEvent& dtde) +{ + osl::ClearableGuard< ::osl::Mutex > aGuard( m_aMutex ); + std::vector> aListeners(m_aListeners); + aGuard.clear(); + + for (auto const& listener : aListeners) + { + listener->dragOver( dtde ); + } +} + +void GtkInstDropTarget::fire_dragExit(const css::datatransfer::dnd::DropTargetEvent& dte) +{ + osl::ClearableGuard< ::osl::Mutex > aGuard( m_aMutex ); + std::vector> aListeners(m_aListeners); + aGuard.clear(); + + for (auto const& listener : aListeners) + { + listener->dragExit( dte ); + } +} + +sal_Bool GtkInstDropTarget::isActive() +{ + return m_bActive; +} + +void GtkInstDropTarget::setActive(sal_Bool bActive) +{ + m_bActive = bActive; +} + +sal_Int8 GtkInstDropTarget::getDefaultActions() +{ + return m_nDefaultActions; +} + +void GtkInstDropTarget::setDefaultActions(sal_Int8 nDefaultActions) +{ + m_nDefaultActions = nDefaultActions; +} + +Reference GtkInstance::ImplCreateDropTarget(const SystemEnvData* pSysEnv) +{ + return vcl::X11DnDHelper(new GtkInstDropTarget(), pSysEnv->aShellWindow); +} + +GtkInstDragSource::~GtkInstDragSource() +{ + if (m_pFrame) + m_pFrame->deregisterDragSource(this); + + if (GtkInstDragSource::g_ActiveDragSource == this) + { + SAL_WARN( "vcl.gtk", "dragEnd should have been called on GtkInstDragSource before dtor"); + GtkInstDragSource::g_ActiveDragSource = nullptr; + } +} + +void GtkInstDragSource::deinitialize() +{ + m_pFrame = nullptr; +} + +sal_Bool GtkInstDragSource::isDragImageSupported() +{ + return true; +} + +sal_Int32 GtkInstDragSource::getDefaultCursor( sal_Int8 ) +{ + return 0; +} + +void GtkInstDragSource::initialize(const css::uno::Sequence& rArguments) +{ + if (rArguments.getLength() < 2) + { + throw RuntimeException("DragSource::initialize: Cannot install window event handler", + getXWeak()); + } + + sal_IntPtr nFrame = 0; + rArguments.getConstArray()[1] >>= nFrame; + + if (!nFrame) + { + throw RuntimeException("DragSource::initialize: missing SalFrame", + getXWeak()); + } + + m_pFrame = reinterpret_cast(nFrame); + m_pFrame->registerDragSource(this); +} + +OUString SAL_CALL GtkInstDragSource::getImplementationName() +{ + return "com.sun.star.datatransfer.dnd.VclGtkDragSource"; +} + +sal_Bool SAL_CALL GtkInstDragSource::supportsService(OUString const & ServiceName) +{ + return cppu::supportsService(this, ServiceName); +} + +css::uno::Sequence SAL_CALL GtkInstDragSource::getSupportedServiceNames() +{ + Sequence aRet { "com.sun.star.datatransfer.dnd.GtkDragSource" }; + return aRet; +} + +Reference GtkInstance::ImplCreateDragSource(const SystemEnvData* pSysEnv) +{ + return vcl::X11DnDHelper(new GtkInstDragSource(), pSysEnv->aShellWindow); +} + +namespace { + +class GtkOpenGLContext : public OpenGLContext +{ + GLWindow m_aGLWin; + GtkWidget *m_pGLArea; + GdkGLContext *m_pContext; + gulong m_nDestroySignalId; + gulong m_nRenderSignalId; + guint m_nAreaFrameBuffer; + guint m_nFrameBuffer; + guint m_nRenderBuffer; + guint m_nDepthBuffer; + guint m_nFrameScratchBuffer; + guint m_nRenderScratchBuffer; + guint m_nDepthScratchBuffer; + +public: + GtkOpenGLContext() + : m_pGLArea(nullptr) + , m_pContext(nullptr) + , m_nDestroySignalId(0) + , m_nRenderSignalId(0) + , m_nAreaFrameBuffer(0) + , m_nFrameBuffer(0) + , m_nRenderBuffer(0) + , m_nDepthBuffer(0) + , m_nFrameScratchBuffer(0) + , m_nRenderScratchBuffer(0) + , m_nDepthScratchBuffer(0) + { + } + + virtual void initWindow() override + { + if( !m_pChildWindow ) + { + SystemWindowData winData = generateWinData(mpWindow, mbRequestLegacyContext); + m_pChildWindow = VclPtr::Create(mpWindow, 0, &winData, false); + } + + if (m_pChildWindow) + { + InitChildWindow(m_pChildWindow.get()); + } + } + +private: + virtual const GLWindow& getOpenGLWindow() const override { return m_aGLWin; } + virtual GLWindow& getModifiableOpenGLWindow() override { return m_aGLWin; } + + static void signalDestroy(GtkWidget*, gpointer context) + { + GtkOpenGLContext* pThis = static_cast(context); + pThis->m_pGLArea = nullptr; + pThis->m_nDestroySignalId = 0; + pThis->m_nRenderSignalId = 0; + } + + static gboolean signalRender(GtkGLArea*, GdkGLContext*, gpointer window) + { + GtkOpenGLContext* pThis = static_cast(window); + + int scale = gtk_widget_get_scale_factor(pThis->m_pGLArea); + int width = pThis->m_aGLWin.Width * scale; + int height = pThis->m_aGLWin.Height * scale; + + glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, pThis->m_nAreaFrameBuffer); + glReadBuffer(GL_COLOR_ATTACHMENT0_EXT); + + glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, + GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT, GL_NEAREST); + + gdk_gl_context_make_current(pThis->m_pContext); + return true; + } + + virtual void adjustToNewSize() override + { + if (!m_pGLArea) + return; + + int scale = gtk_widget_get_scale_factor(m_pGLArea); + int width = m_aGLWin.Width * scale; + int height = m_aGLWin.Height * scale; + + // seen in tdf#124729 width/height of 0 leading to GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT + int allocwidth = std::max(width, 1); + int allocheight = std::max(height, 1); + + gtk_gl_area_make_current(GTK_GL_AREA(m_pGLArea)); + if (GError *pError = gtk_gl_area_get_error(GTK_GL_AREA(m_pGLArea))) + { + SAL_WARN("vcl.gtk", "gtk gl area error: " << pError->message); + return; + } + + glBindRenderbuffer(GL_RENDERBUFFER, m_nRenderBuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB8, allocwidth, allocheight); + glBindRenderbuffer(GL_RENDERBUFFER, m_nDepthBuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, allocwidth, allocheight); + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_nAreaFrameBuffer); + + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, + GL_RENDERBUFFER_EXT, m_nRenderBuffer); + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, + GL_RENDERBUFFER_EXT, m_nDepthBuffer); + + gdk_gl_context_make_current(m_pContext); + glBindRenderbuffer(GL_RENDERBUFFER, m_nRenderBuffer); + glBindRenderbuffer(GL_RENDERBUFFER, m_nDepthBuffer); + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_nFrameBuffer); + + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, + GL_RENDERBUFFER_EXT, m_nRenderBuffer); + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, + GL_RENDERBUFFER_EXT, m_nDepthBuffer); + glViewport(0, 0, width, height); + + glBindRenderbuffer(GL_RENDERBUFFER, m_nRenderScratchBuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGB8, allocwidth, allocheight); + glBindRenderbuffer(GL_RENDERBUFFER, m_nDepthScratchBuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, allocwidth, allocheight); + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_nFrameScratchBuffer); + + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, + GL_RENDERBUFFER_EXT, m_nRenderScratchBuffer); + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, + GL_RENDERBUFFER_EXT, m_nDepthScratchBuffer); + + glViewport(0, 0, width, height); + } + + // Use a throw away toplevel to determine the OpenGL version because once + // an GdkGLContext is created for a window then it seems that + // glGenVertexArrays will always be called when the window gets rendered. + static int GetOpenGLVersion() + { + int nMajorGLVersion(0); + + GtkWidget* pWindow; +#if !GTK_CHECK_VERSION(4,0,0) + pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL); +#else + pWindow = gtk_window_new(); +#endif + + gtk_widget_realize(pWindow); + + if (GdkSurface* pSurface = widget_get_surface(pWindow)) + { + if (GdkGLContext* pContext = surface_create_gl_context(pSurface)) + { + if (gdk_gl_context_realize(pContext, nullptr)) + { + OpenGLZone aZone; + gdk_gl_context_make_current(pContext); + gdk_gl_context_get_version(pContext, &nMajorGLVersion, nullptr); + gdk_gl_context_clear_current(); + } + g_object_unref(pContext); + } + } + +#if !GTK_CHECK_VERSION(4,0,0) + gtk_widget_destroy(pWindow); +#else + gtk_window_destroy(GTK_WINDOW(pWindow)); +#endif + return nMajorGLVersion; + } + + virtual bool ImplInit() override + { + static int nOpenGLVersion = GetOpenGLVersion(); + if (nOpenGLVersion < 3) + { + SAL_WARN("vcl.gtk", "gtk GL requires glGenVertexArrays which is OpenGL 3, while system provides: " << nOpenGLVersion); + return false; + } + + const SystemEnvData* pEnvData = m_pChildWindow->GetSystemData(); + GtkWidget *pParent = static_cast(pEnvData->pWidget); + m_pGLArea = gtk_gl_area_new(); + m_nDestroySignalId = g_signal_connect(G_OBJECT(m_pGLArea), "destroy", G_CALLBACK(signalDestroy), this); + m_nRenderSignalId = g_signal_connect(G_OBJECT(m_pGLArea), "render", G_CALLBACK(signalRender), this); + gtk_gl_area_set_has_depth_buffer(GTK_GL_AREA(m_pGLArea), true); + gtk_gl_area_set_auto_render(GTK_GL_AREA(m_pGLArea), false); + gtk_widget_set_hexpand(m_pGLArea, true); + gtk_widget_set_vexpand(m_pGLArea, true); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_add(GTK_CONTAINER(pParent), m_pGLArea); + gtk_widget_show_all(pParent); +#else + gtk_grid_attach(GTK_GRID(pParent), m_pGLArea, 0, 0, 1, 1); + gtk_widget_show(pParent); + gtk_widget_show(m_pGLArea); +#endif + + gtk_gl_area_make_current(GTK_GL_AREA(m_pGLArea)); + if (GError *pError = gtk_gl_area_get_error(GTK_GL_AREA(m_pGLArea))) + { + SAL_WARN("vcl.gtk", "gtk gl area error: " << pError->message); + return false; + } + + gtk_gl_area_attach_buffers(GTK_GL_AREA(m_pGLArea)); + glGenFramebuffersEXT(1, &m_nAreaFrameBuffer); + + GdkSurface* pWindow = widget_get_surface(pParent); + m_pContext = surface_create_gl_context(pWindow); + if (!m_pContext) + return false; + + if (!gdk_gl_context_realize(m_pContext, nullptr)) + return false; + + gdk_gl_context_make_current(m_pContext); + glGenFramebuffersEXT(1, &m_nFrameBuffer); + glGenRenderbuffersEXT(1, &m_nRenderBuffer); + glGenRenderbuffersEXT(1, &m_nDepthBuffer); + glGenFramebuffersEXT(1, &m_nFrameScratchBuffer); + glGenRenderbuffersEXT(1, &m_nRenderScratchBuffer); + glGenRenderbuffersEXT(1, &m_nDepthScratchBuffer); + + bool bRet = InitGL(); + InitGLDebugging(); + return bRet; + } + + virtual void restoreDefaultFramebuffer() override + { + OpenGLContext::restoreDefaultFramebuffer(); + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_nFrameScratchBuffer); + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, + GL_RENDERBUFFER_EXT, m_nRenderScratchBuffer); + } + + virtual void makeCurrent() override + { + if (isCurrent()) + return; + + clearCurrent(); + + if (m_pGLArea) + { + int scale = gtk_widget_get_scale_factor(m_pGLArea); + int width = m_aGLWin.Width * scale; + int height = m_aGLWin.Height * scale; + + gdk_gl_context_make_current(m_pContext); + + glBindRenderbuffer(GL_RENDERBUFFER, m_nRenderScratchBuffer); + glBindRenderbuffer(GL_RENDERBUFFER, m_nDepthScratchBuffer); + glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_nFrameScratchBuffer); + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, + GL_RENDERBUFFER_EXT, m_nRenderScratchBuffer); + glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, + GL_RENDERBUFFER_EXT, m_nDepthScratchBuffer); + glViewport(0, 0, width, height); + } + + registerAsCurrent(); + } + + virtual void destroyCurrentContext() override + { + gdk_gl_context_clear_current(); + } + + virtual bool isCurrent() override + { + return m_pGLArea && gdk_gl_context_get_current() == m_pContext; + } + + virtual void sync() override + { + } + + virtual void resetCurrent() override + { + clearCurrent(); + gdk_gl_context_clear_current(); + } + + virtual void swapBuffers() override + { + int scale = gtk_widget_get_scale_factor(m_pGLArea); + int width = m_aGLWin.Width * scale; + int height = m_aGLWin.Height * scale; + + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_nFrameBuffer); + glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, m_nFrameScratchBuffer); + glReadBuffer(GL_COLOR_ATTACHMENT0_EXT); + + glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, + GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT, GL_NEAREST); + + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_nFrameScratchBuffer); + glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT); + + gtk_gl_area_queue_render(GTK_GL_AREA(m_pGLArea)); + BuffersSwapped(); + } + + virtual ~GtkOpenGLContext() override + { + if (m_nDestroySignalId) + g_signal_handler_disconnect(m_pGLArea, m_nDestroySignalId); + if (m_nRenderSignalId) + g_signal_handler_disconnect(m_pGLArea, m_nRenderSignalId); + if (m_pContext) + g_clear_object(&m_pContext); + } +}; + +} + +OpenGLContext* GtkInstance::CreateOpenGLContext() +{ + return new GtkOpenGLContext; +} + +// tdf#123800 avoid requiring wayland at runtime just because it existed at buildtime +bool DLSYM_GDK_IS_WAYLAND_DISPLAY(GdkDisplay* pDisplay) +{ + static auto get_type = reinterpret_cast(dlsym(nullptr, "gdk_wayland_display_get_type")); + if (!get_type) + return false; + static bool bResult = G_TYPE_CHECK_INSTANCE_TYPE(pDisplay, get_type()); + return bResult; +} + +bool DLSYM_GDK_IS_X11_DISPLAY(GdkDisplay* pDisplay) +{ + static auto get_type = reinterpret_cast(dlsym(nullptr, "gdk_x11_display_get_type")); + if (!get_type) + return false; + static bool bResult = G_TYPE_CHECK_INSTANCE_TYPE(pDisplay, get_type()); + return bResult; +} + +namespace +{ + +class GtkInstanceBuilder; + + void set_help_id(const GtkWidget *pWidget, std::u16string_view rHelpId) + { + gchar *helpid = g_strdup(OUStringToOString(rHelpId, RTL_TEXTENCODING_UTF8).getStr()); + g_object_set_data_full(G_OBJECT(pWidget), "g-lo-helpid", helpid, g_free); + } + + OUString get_help_id(const GtkWidget *pWidget) + { + void* pData = g_object_get_data(G_OBJECT(pWidget), "g-lo-helpid"); + const gchar* pStr = static_cast(pData); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + KeyEvent CreateKeyEvent(guint keyval, guint16 hardware_keycode, guint state, guint8 group) + { + sal_uInt16 nKeyCode = GtkSalFrame::GetKeyCode(keyval); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (nKeyCode == 0) + { + guint updated_keyval = GtkSalFrame::GetKeyValFor(gdk_keymap_get_default(), hardware_keycode, group); + nKeyCode = GtkSalFrame::GetKeyCode(updated_keyval); + } +#else + (void)hardware_keycode; + (void)group; +#endif + nKeyCode |= GtkSalFrame::GetKeyModCode(state); + return KeyEvent(gdk_keyval_to_unicode(keyval), nKeyCode, 0); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + KeyEvent GtkToVcl(const GdkEventKey& rEvent) + { + return CreateKeyEvent(rEvent.keyval, rEvent.hardware_keycode, rEvent.state, rEvent.group); + } +#endif +} + +static MouseEventModifiers ImplGetMouseButtonMode(sal_uInt16 nButton, sal_uInt16 nCode) +{ + MouseEventModifiers nMode = MouseEventModifiers::NONE; + if ( nButton == MOUSE_LEFT ) + nMode |= MouseEventModifiers::SIMPLECLICK; + if ( (nButton == MOUSE_LEFT) && !(nCode & (MOUSE_MIDDLE | MOUSE_RIGHT)) ) + nMode |= MouseEventModifiers::SELECT; + if ( (nButton == MOUSE_LEFT) && (nCode & KEY_MOD1) && + !(nCode & (MOUSE_MIDDLE | MOUSE_RIGHT | KEY_SHIFT)) ) + nMode |= MouseEventModifiers::MULTISELECT; + if ( (nButton == MOUSE_LEFT) && (nCode & KEY_SHIFT) && + !(nCode & (MOUSE_MIDDLE | MOUSE_RIGHT | KEY_MOD1)) ) + nMode |= MouseEventModifiers::RANGESELECT; + return nMode; +} + +static MouseEventModifiers ImplGetMouseMoveMode(sal_uInt16 nCode) +{ + MouseEventModifiers nMode = MouseEventModifiers::NONE; + if ( !nCode ) + nMode |= MouseEventModifiers::SIMPLEMOVE; + if ( (nCode & MOUSE_LEFT) && !(nCode & KEY_MOD1) ) + nMode |= MouseEventModifiers::DRAGMOVE; + if ( (nCode & MOUSE_LEFT) && (nCode & KEY_MOD1) ) + nMode |= MouseEventModifiers::DRAGCOPY; + return nMode; +} + +namespace +{ + bool SwapForRTL(GtkWidget* pWidget) + { + GtkTextDirection eDir = gtk_widget_get_direction(pWidget); + if (eDir == GTK_TEXT_DIR_RTL) + return true; + if (eDir == GTK_TEXT_DIR_LTR) + return false; + return AllSettings::GetLayoutRTL(); + } + + GtkWidget* getPopupRect(GtkWidget* pWidget, const tools::Rectangle& rInRect, GdkRectangle& rOutRect) + { + if (GtkSalFrame* pFrame = GtkSalFrame::getFromWindow(pWidget)) + { + // this is the relatively unusual case where pParent is the toplevel GtkSalFrame and not a stock GtkWidget + // so use the same style of logic as GtkSalMenu::ShowNativePopupMenu to get the right position + AbsoluteScreenPixelRectangle aFloatRect = FloatingWindow::ImplConvertToAbsPos(pFrame->GetWindow(), rInRect); + aFloatRect.Move(-pFrame->maGeometry.x(), -pFrame->maGeometry.y()); + + rOutRect = GdkRectangle{static_cast(aFloatRect.Left()), static_cast(aFloatRect.Top()), + static_cast(aFloatRect.GetWidth()), static_cast(aFloatRect.GetHeight())}; + + pWidget = pFrame->getMouseEventWidget(); + } + else + { + rOutRect = GdkRectangle{static_cast(rInRect.Left()), static_cast(rInRect.Top()), + static_cast(rInRect.GetWidth()), static_cast(rInRect.GetHeight())}; + if (SwapForRTL(pWidget)) + rOutRect.x = gtk_widget_get_allocated_width(pWidget) - rOutRect.width - 1 - rOutRect.x; + } + return pWidget; + } + + void replaceWidget(GtkWidget* pWidget, GtkWidget* pReplacement) + { + // remove the widget and replace it with pReplacement + GtkWidget* pParent = gtk_widget_get_parent(pWidget); + + // if pWidget was un-parented then don't bother + if (!pParent) + return; + + g_object_ref(pWidget); + + gint nTopAttach(0), nLeftAttach(0), nHeight(1), nWidth(1); + if (GTK_IS_GRID(pParent)) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_child_get(GTK_CONTAINER(pParent), pWidget, + "left-attach", &nLeftAttach, + "top-attach", &nTopAttach, + "width", &nWidth, + "height", &nHeight, + nullptr); +#else + gtk_grid_query_child(GTK_GRID(pParent), pWidget, + &nLeftAttach, &nTopAttach, + &nWidth, &nHeight); +#endif + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + gboolean bExpand(false), bFill(false); + GtkPackType ePackType(GTK_PACK_START); + guint nPadding(0); + gint nPosition(0); + if (GTK_IS_BOX(pParent)) + { + gtk_container_child_get(GTK_CONTAINER(pParent), pWidget, + "expand", &bExpand, + "fill", &bFill, + "pack-type", &ePackType, + "padding", &nPadding, + "position", &nPosition, + nullptr); + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + // for gtk3 remove before replacement inserted, or there are warnings + // from GTK_BIN about having two children + container_remove(pParent, pWidget); +#endif + + gtk_widget_set_visible(pReplacement, gtk_widget_get_visible(pWidget)); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_no_show_all(pReplacement, gtk_widget_get_no_show_all(pWidget)); +#endif + + int nReqWidth, nReqHeight; + gtk_widget_get_size_request(pWidget, &nReqWidth, &nReqHeight); + gtk_widget_set_size_request(pReplacement, nReqWidth, nReqHeight); + + static GQuark quark_size_groups = g_quark_from_static_string("gtk-widget-size-groups"); + GSList* pSizeGroups = static_cast(g_object_get_qdata(G_OBJECT(pWidget), quark_size_groups)); + while (pSizeGroups) + { + GtkSizeGroup *pSizeGroup = static_cast(pSizeGroups->data); + pSizeGroups = pSizeGroups->next; + gtk_size_group_remove_widget(pSizeGroup, pWidget); + gtk_size_group_add_widget(pSizeGroup, pReplacement); + } + + // tdf#135368 change the mnemonic to point to our replacement + GList* pLabels = gtk_widget_list_mnemonic_labels(pWidget); + for (GList* pLabel = g_list_first(pLabels); pLabel; pLabel = g_list_next(pLabel)) + { + GtkWidget* pLabelWidget = static_cast(pLabel->data); + if (!GTK_IS_LABEL(pLabelWidget)) + continue; + gtk_label_set_mnemonic_widget(GTK_LABEL(pLabelWidget), pReplacement); + } + g_list_free(pLabels); + + + if (GTK_IS_GRID(pParent)) + { + gtk_grid_attach(GTK_GRID(pParent), pReplacement, nLeftAttach, nTopAttach, nWidth, nHeight); + } + else if (GTK_IS_BOX(pParent)) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_box_pack_start(GTK_BOX(pParent), pReplacement, bExpand, bFill, nPadding); + gtk_container_child_set(GTK_CONTAINER(pParent), pReplacement, + "pack-type", ePackType, + "position", nPosition, + nullptr); +#else + gtk_box_insert_child_after(GTK_BOX(pParent), pReplacement, pWidget); +#endif + } +#if !GTK_CHECK_VERSION(4, 0, 0) + else + gtk_container_add(GTK_CONTAINER(pParent), pReplacement); +#endif + + if (gtk_widget_get_hexpand_set(pWidget)) + gtk_widget_set_hexpand(pReplacement, gtk_widget_get_hexpand(pWidget)); + + if (gtk_widget_get_vexpand_set(pWidget)) + gtk_widget_set_vexpand(pReplacement, gtk_widget_get_vexpand(pWidget)); + + gtk_widget_set_halign(pReplacement, gtk_widget_get_halign(pWidget)); + gtk_widget_set_valign(pReplacement, gtk_widget_get_valign(pWidget)); + +#if GTK_CHECK_VERSION(4, 0, 0) + // for gtk4 remove after replacement inserted so we could use gtk_box_insert_child_after + container_remove(pParent, pWidget); +#endif + + // coverity[freed_arg : FALSE] - this does not free pWidget, it is reffed by pReplacement + g_object_unref(pWidget); + } + + void insertAsParent(GtkWidget* pWidget, GtkWidget* pReplacement) + { + g_object_ref(pWidget); + + replaceWidget(pWidget, pReplacement); + + // coverity[pass_freed_arg : FALSE] - pWidget is not freed here due to initial g_object_ref + container_add(pReplacement, pWidget); + + // coverity[freed_arg : FALSE] - this does not free pWidget, it is reffed by pReplacement + g_object_unref(pWidget); + } + + GtkWidget* ensureEventWidget(GtkWidget* pWidget) + { +#if GTK_CHECK_VERSION(4, 0, 0) + return pWidget; +#else + + if (!pWidget) + return nullptr; + + GtkWidget* pMouseEventBox; + // not every widget has a GdkWindow and can get any event, so if we + // want an event it doesn't have, insert a GtkEventBox so we can get + // those + if (gtk_widget_get_has_window(pWidget)) + pMouseEventBox = pWidget; + else + { + // remove the widget and replace it with an eventbox and put the old + // widget into it + pMouseEventBox = gtk_event_box_new(); + gtk_event_box_set_above_child(GTK_EVENT_BOX(pMouseEventBox), false); + gtk_event_box_set_visible_window(GTK_EVENT_BOX(pMouseEventBox), false); + insertAsParent(pWidget, pMouseEventBox); + } + + return pMouseEventBox; +#endif + } +} + +namespace { + +#if !GTK_CHECK_VERSION(4, 0, 0) +GdkDragAction VclToGdk(sal_Int8 dragOperation) +{ + GdkDragAction eRet(static_cast(0)); + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_COPY) + eRet = static_cast(eRet | GDK_ACTION_COPY); + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_MOVE) + eRet = static_cast(eRet | GDK_ACTION_MOVE); + if (dragOperation & css::datatransfer::dnd::DNDConstants::ACTION_LINK) + eRet = static_cast(eRet | GDK_ACTION_LINK); + return eRet; +} +#endif + +GtkWindow* get_active_window() +{ + GtkWindow* pFocus = nullptr; + + GList* pList = gtk_window_list_toplevels(); + + for (GList* pEntry = pList; pEntry; pEntry = pEntry->next) + { +#if GTK_CHECK_VERSION(4, 0, 0) + if (gtk_window_is_active(GTK_WINDOW(pEntry->data))) +#else + if (gtk_window_has_toplevel_focus(GTK_WINDOW(pEntry->data))) +#endif + { + pFocus = GTK_WINDOW(pEntry->data); + break; + } + } + + g_list_free(pList); + + return pFocus; +} + +void LocalizeDecimalSeparator(guint& keyval) +{ + const bool bDecimalKey = keyval == GDK_KEY_KP_Decimal || keyval == GDK_KEY_KP_Separator; + // #i1820# (and tdf#154623) use locale specific decimal separator + if (bDecimalKey && Application::GetSettings().GetMiscSettings().GetEnableLocalizedDecimalSep()) + { + GtkWindow* pFocusWin = get_active_window(); + GtkWidget* pFocus = pFocusWin ? gtk_window_get_focus(pFocusWin) : nullptr; + // tdf#138932 except if the target is a GtkEntry used for passwords + // GTK4: TODO is it a GtkEntry or a child GtkText that has the focus in this situation? + if (!pFocus || !GTK_IS_ENTRY(pFocus) || gtk_entry_get_visibility(GTK_ENTRY(pFocus))) + { + OUString aSep(Application::GetSettings().GetLocaleDataWrapper().getNumDecimalSep()); + keyval = aSep[0]; + } + } +} + +void set_cursor(GtkWidget* pWidget, const char *pName) +{ + if (!gtk_widget_get_realized(pWidget)) + gtk_widget_realize(pWidget); + GdkDisplay *pDisplay = gtk_widget_get_display(pWidget); +#if GTK_CHECK_VERSION(4, 0, 0) + GdkCursor *pCursor = pName ? gdk_cursor_new_from_name(pName, nullptr) : nullptr; +#else + GdkCursor *pCursor = pName ? gdk_cursor_new_from_name(pDisplay, pName) : nullptr; +#endif + widget_set_cursor(pWidget, pCursor); + gdk_display_flush(pDisplay); + if (pCursor) + g_object_unref(pCursor); +} + +vcl::Font get_font(GtkWidget* pWidget) +{ + PangoContext* pContext = gtk_widget_get_pango_context(pWidget); + return pango_to_vcl(pango_context_get_font_description(pContext), + Application::GetSettings().GetUILanguageTag().getLocale()); +} + +} + +OUString get_buildable_id(GtkBuildable* pWidget) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + const gchar* pStr = gtk_buildable_get_buildable_id(pWidget); +#else + const gchar* pStr = gtk_buildable_get_name(pWidget); +#endif + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); +} + +void set_buildable_id(GtkBuildable* pWidget, const OUString& rId) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + GtkBuildableIface *iface = GTK_BUILDABLE_GET_IFACE(pWidget); + (*iface->set_id)(pWidget, OUStringToOString(rId, RTL_TEXTENCODING_UTF8).getStr()); +#else + gtk_buildable_set_name(pWidget, OUStringToOString(rId, RTL_TEXTENCODING_UTF8).getStr()); +#endif +} + +namespace { + +class GtkInstanceWidget : public virtual weld::Widget +{ +protected: + GtkWidget* m_pWidget; + GtkWidget* m_pMouseEventBox; + GtkInstanceBuilder* m_pBuilder; + +#if !GTK_CHECK_VERSION(4, 0, 0) + DECL_LINK(async_drag_cancel, void*, void); +#endif + + bool IsFirstFreeze() const { return m_nFreezeCount == 0; } + bool IsLastThaw() const { return m_nFreezeCount == 1; } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalFocusIn(GtkEventControllerFocus*, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_focus_in(); + } +#else + static gboolean signalFocusIn(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_focus_in(); + return false; + } +#endif + + void signal_focus_in() + { + GtkWidget* pTopLevel = widget_get_toplevel(m_pWidget); + // see commentary in GtkSalObjectWidgetClip::Show + if (pTopLevel && g_object_get_data(G_OBJECT(pTopLevel), "g-lo-BlockFocusChange")) + return; + + m_aFocusInHdl.Call(*this); + } + + static gboolean signalMnemonicActivate(GtkWidget*, gboolean, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + SolarMutexGuard aGuard; + return pThis->signal_mnemonic_activate(); + } + + bool signal_mnemonic_activate() + { + return m_aMnemonicActivateHdl.Call(*this); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalFocusOut(GtkEventControllerFocus*, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_focus_in(); + } +#else + static gboolean signalFocusOut(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_focus_out(); + return false; + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + void launch_drag_cancel(GdkDragContext* context) + { + // post our drag cancel to happen at the next available event cycle + if (m_pDragCancelEvent) + return; + g_object_ref(context); + m_pDragCancelEvent = Application::PostUserEvent(LINK(this, GtkInstanceWidget, async_drag_cancel), context); + } +#endif + + void signal_focus_out() + { + GtkWidget* pTopLevel = widget_get_toplevel(m_pWidget); + // see commentary in GtkSalObjectWidgetClip::Show + if (pTopLevel && g_object_get_data(G_OBJECT(pTopLevel), "g-lo-BlockFocusChange")) + return; + + m_aFocusOutHdl.Call(*this); + } + + virtual void ensureMouseEventWidget() + { + if (!m_pMouseEventBox) + m_pMouseEventBox = ::ensureEventWidget(m_pWidget); + } + + void ensureButtonPressSignal() + { + if (!m_nButtonPressSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController* pClickController = get_click_controller(); + m_nButtonPressSignalId = g_signal_connect(pClickController, "pressed", G_CALLBACK(signalButtonPress), this); +#else + ensureMouseEventWidget(); + m_nButtonPressSignalId = g_signal_connect(m_pMouseEventBox, "button-press-event", G_CALLBACK(signalButtonPress), this); +#endif + } + } + + void ensureButtonReleaseSignal() + { + if (!m_nButtonReleaseSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController* pClickController = get_click_controller(); + m_nButtonReleaseSignalId = g_signal_connect(pClickController, "released", G_CALLBACK(signalButtonRelease), this); +#else + ensureMouseEventWidget(); + m_nButtonReleaseSignalId = g_signal_connect(m_pMouseEventBox, "button-release-event", G_CALLBACK(signalButtonRelease), this); +#endif + } + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static gboolean signalPopupMenu(GtkWidget* pWidget, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + SolarMutexGuard aGuard; + //center it when we don't know where else to use + Point aPos(gtk_widget_get_allocated_width(pWidget) / 2, + gtk_widget_get_allocated_height(pWidget) / 2); + CommandEvent aCEvt(aPos, CommandEventId::ContextMenu, false); + return pThis->signal_popup_menu(aCEvt); + } +#endif + + virtual void connect_style_updated(const Link& rLink) override + { + if (m_aStyleUpdatedHdl.IsSet()) + ImplGetDefaultWindow()->RemoveEventListener(LINK(this, GtkInstanceWidget, SettingsChangedHdl)); + weld::Widget::connect_style_updated(rLink); + if (m_aStyleUpdatedHdl.IsSet()) + ImplGetDefaultWindow()->AddEventListener(LINK(this, GtkInstanceWidget, SettingsChangedHdl)); + } + + bool SwapForRTL() const + { + return ::SwapForRTL(m_pWidget); + } + + void do_enable_drag_source(const rtl::Reference& rHelper, sal_uInt8 eDNDConstants) + { + ensure_drag_source(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + auto aFormats = rHelper->getTransferDataFlavors(); + std::vector aGtkTargets(m_xDragSource->FormatsToGtk(aFormats)); + + m_eDragAction = VclToGdk(eDNDConstants); + drag_source_set(aGtkTargets, m_eDragAction); + + for (auto &a : aGtkTargets) + g_free(a.target); + + m_xDragSource->set_datatransfer(rHelper, rHelper); +#else + (void)rHelper; + (void)eDNDConstants; +#endif + } + + void localizeDecimalSeparator() + { + // tdf#128867 if localize decimal separator is active we will always + // need to be able to change the output of the decimal key press + if (!m_nKeyPressSignalId && Application::GetSettings().GetMiscSettings().GetEnableLocalizedDecimalSep()) + { +#if GTK_CHECK_VERSION(4, 0, 0) + m_nKeyPressSignalId = g_signal_connect(get_key_controller(), "key-pressed", G_CALLBACK(signalKeyPressed), this); +#else + m_nKeyPressSignalId = g_signal_connect(m_pWidget, "key-press-event", G_CALLBACK(signalKey), this); +#endif + } + } + + void ensure_drag_begin_end() + { + if (!m_nDragBeginSignalId) + { + // using "after" due to https://gitlab.gnome.org/GNOME/pygobject/issues/251 +#if GTK_CHECK_VERSION(4, 0, 0) + m_nDragBeginSignalId = g_signal_connect_after(get_drag_controller(), "drag-begin", G_CALLBACK(signalDragBegin), this); +#else + m_nDragBeginSignalId = g_signal_connect_after(m_pWidget, "drag-begin", G_CALLBACK(signalDragBegin), this); +#endif + } + if (!m_nDragEndSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + m_nDragEndSignalId = g_signal_connect(get_drag_controller(), "drag-end", G_CALLBACK(signalDragEnd), this); +#else + m_nDragEndSignalId = g_signal_connect(m_pWidget, "drag-end", G_CALLBACK(signalDragEnd), this); +#endif + } + } + + void DisconnectMouseEvents() + { + if (m_nButtonPressSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_click_controller(), m_nButtonPressSignalId); +#else + g_signal_handler_disconnect(m_pMouseEventBox, m_nButtonPressSignalId); +#endif + m_nButtonPressSignalId = 0; + } + if (m_nMotionSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_motion_controller(), m_nMotionSignalId); +#else + g_signal_handler_disconnect(m_pMouseEventBox, m_nMotionSignalId); +#endif + m_nMotionSignalId = 0; + } + if (m_nLeaveSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_motion_controller(), m_nLeaveSignalId); +#else + g_signal_handler_disconnect(m_pMouseEventBox, m_nLeaveSignalId); +#endif + m_nLeaveSignalId = 0; + } + if (m_nEnterSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_motion_controller(), m_nEnterSignalId); +#else + g_signal_handler_disconnect(m_pMouseEventBox, m_nEnterSignalId); +#endif + m_nEnterSignalId = 0; + } + if (m_nButtonReleaseSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_click_controller(), m_nButtonReleaseSignalId); +#else + g_signal_handler_disconnect(m_pMouseEventBox, m_nButtonReleaseSignalId); +#endif + m_nButtonReleaseSignalId = 0; + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!m_pMouseEventBox || m_pMouseEventBox == m_pWidget) + return; + + // GtkWindow replacement for GtkPopover case + if (!GTK_IS_EVENT_BOX(m_pMouseEventBox)) + { + m_pMouseEventBox = nullptr; + return; + } + + // put things back they way we found them + GtkWidget* pParent = gtk_widget_get_parent(m_pMouseEventBox); + + g_object_ref(m_pWidget); + gtk_container_remove(GTK_CONTAINER(m_pMouseEventBox), m_pWidget); + + gtk_widget_destroy(m_pMouseEventBox); + + gtk_container_add(GTK_CONTAINER(pParent), m_pWidget); + // coverity[freed_arg : FALSE] - this does not free m_pWidget, it is reffed by pParent + g_object_unref(m_pWidget); + + m_pMouseEventBox = m_pWidget; +#endif + } + +private: + bool m_bTakeOwnership; +#if !GTK_CHECK_VERSION(4, 0, 0) + bool m_bDraggedOver; +#endif + int m_nWaitCount; + int m_nFreezeCount; + sal_uInt16 m_nLastMouseButton; +#if !GTK_CHECK_VERSION(4, 0, 0) + sal_uInt16 m_nLastMouseClicks; +#endif + int m_nPressedButton; +#if !GTK_CHECK_VERSION(4, 0, 0) +protected: + int m_nPressStartX; + int m_nPressStartY; +private: +#endif + ImplSVEvent* m_pDragCancelEvent; + GtkCssProvider* m_pBgCssProvider; +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkDragAction m_eDragAction; +#endif + gulong m_nFocusInSignalId; + gulong m_nMnemonicActivateSignalId; + gulong m_nFocusOutSignalId; + gulong m_nKeyPressSignalId; + gulong m_nKeyReleaseSignalId; +protected: + gulong m_nSizeAllocateSignalId; +private: + gulong m_nButtonPressSignalId; + gulong m_nMotionSignalId; + gulong m_nLeaveSignalId; + gulong m_nEnterSignalId; + gulong m_nButtonReleaseSignalId; + gulong m_nDragMotionSignalId; + gulong m_nDragDropSignalId; + gulong m_nDragDropReceivedSignalId; + gulong m_nDragLeaveSignalId; + gulong m_nDragBeginSignalId; + gulong m_nDragEndSignalId; + gulong m_nDragFailedSignalId; + gulong m_nDragDataDeleteignalId; + gulong m_nDragGetSignalId; + +#if GTK_CHECK_VERSION(4, 0, 0) + int m_nGrabCount; + GtkEventController* m_pFocusController; + GtkEventController* m_pClickController; + GtkEventController* m_pMotionController; + GtkEventController* m_pDragController; + GtkEventController* m_pKeyController; +#endif + + rtl::Reference m_xDropTarget; + rtl::Reference m_xDragSource; + + static void signalSizeAllocate(GtkWidget*, GdkRectangle* allocation, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_size_allocate(allocation->width, allocation->height); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static gboolean signalKeyPressed(GtkEventControllerKey*, guint keyval, guint keycode, GdkModifierType state, gpointer widget) + { + LocalizeDecimalSeparator(keyval); + GtkInstanceWidget* pThis = static_cast(widget); + return pThis->signal_key_press(keyval, keycode, state); + } + + static gboolean signalKeyReleased(GtkEventControllerKey*, guint keyval, guint keycode, GdkModifierType state, gpointer widget) + { + LocalizeDecimalSeparator(keyval); + GtkInstanceWidget* pThis = static_cast(widget); + return pThis->signal_key_release(keyval, keycode, state); + } +#else + static gboolean signalKey(GtkWidget*, GdkEventKey* pEvent, gpointer widget) + { + LocalizeDecimalSeparator(pEvent->keyval); + GtkInstanceWidget* pThis = static_cast(widget); + if (pEvent->type == GDK_KEY_PRESS) + return pThis->signal_key_press(pEvent); + return pThis->signal_key_release(pEvent); + } +#endif + + virtual bool signal_popup_menu(const CommandEvent&) + { + return false; + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalButtonPress(GtkGestureClick* pGesture, int n_press, gdouble x, gdouble y, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_button(pGesture, SalEvent::MouseButtonDown, n_press, x, y); + } + + static void signalButtonRelease(GtkGestureClick* pGesture, int n_press, gdouble x, gdouble y, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_button(pGesture, SalEvent::MouseButtonUp, n_press, x, y); + } + + void signal_button(GtkGestureClick* pGesture, SalEvent nEventType, int n_press, gdouble x, gdouble y) + { + m_nPressedButton = -1; + + Point aPos(x, y); + if (SwapForRTL()) + aPos.setX(gtk_widget_get_allocated_width(m_pWidget) - 1 - aPos.X()); + + if (n_press == 1) + { + GdkEventSequence* pSequence = gtk_gesture_single_get_current_sequence(GTK_GESTURE_SINGLE(pGesture)); + GdkEvent* pEvent = gtk_gesture_get_last_event(GTK_GESTURE(pGesture), pSequence); + if (gdk_event_triggers_context_menu(pEvent)) + { + //if handled for context menu, stop processing + CommandEvent aCEvt(aPos, CommandEventId::ContextMenu, true); + if (signal_popup_menu(aCEvt)) + { + gtk_gesture_set_state(GTK_GESTURE(pGesture), GTK_EVENT_SEQUENCE_CLAIMED); + return; + } + } + } + + GdkModifierType eType = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(pGesture)); + int nButton = gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(pGesture)); + + switch (nButton) + { + case 1: + m_nLastMouseButton = MOUSE_LEFT; + break; + case 2: + m_nLastMouseButton = MOUSE_MIDDLE; + break; + case 3: + m_nLastMouseButton = MOUSE_RIGHT; + break; + default: + return; + } + + sal_uInt32 nModCode = GtkSalFrame::GetMouseModCode(eType); + // strip out which buttons are involved from the nModCode and replace with m_nLastMouseButton + sal_uInt16 nCode = m_nLastMouseButton | (nModCode & (KEY_SHIFT | KEY_MOD1 | KEY_MOD2)); + MouseEvent aMEvt(aPos, n_press, ImplGetMouseButtonMode(m_nLastMouseButton, nModCode), nCode, nCode); + + if (nEventType == SalEvent::MouseButtonDown && m_aMousePressHdl.Call(aMEvt)) + gtk_gesture_set_state(GTK_GESTURE(pGesture), GTK_EVENT_SEQUENCE_CLAIMED); + + if (nEventType == SalEvent::MouseButtonUp && m_aMouseReleaseHdl.Call(aMEvt)) + gtk_gesture_set_state(GTK_GESTURE(pGesture), GTK_EVENT_SEQUENCE_CLAIMED); + } + +#else + + static gboolean signalButtonPress(GtkWidget*, GdkEventButton* pEvent, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + SolarMutexGuard aGuard; + return pThis->signal_button(pEvent); + } + + static gboolean signalButtonRelease(GtkWidget*, GdkEventButton* pEvent, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + SolarMutexGuard aGuard; + return pThis->signal_button(pEvent); + } + + bool signal_button(GdkEventButton* pEvent) + { + m_nPressedButton = -1; + + Point aPos(pEvent->x, pEvent->y); + if (SwapForRTL()) + aPos.setX(gtk_widget_get_allocated_width(m_pWidget) - 1 - aPos.X()); + + if (gdk_event_triggers_context_menu(reinterpret_cast(pEvent)) && pEvent->type == GDK_BUTTON_PRESS) + { + //if handled for context menu, stop processing + CommandEvent aCEvt(aPos, CommandEventId::ContextMenu, true); + if (signal_popup_menu(aCEvt)) + return true; + } + + /* Save press to possibly begin a drag */ + if (pEvent->type != GDK_BUTTON_RELEASE) + { + m_nPressedButton = pEvent->button; + m_nPressStartX = pEvent->x; + m_nPressStartY = pEvent->y; + } + + if (!m_aMousePressHdl.IsSet() && !m_aMouseReleaseHdl.IsSet()) + return false; + + SalEvent nEventType = SalEvent::NONE; + switch (pEvent->type) + { + case GDK_BUTTON_PRESS: + if (GdkEvent* pPeekEvent = gdk_event_peek()) + { + bool bSkip = pPeekEvent->type == GDK_2BUTTON_PRESS || + pPeekEvent->type == GDK_3BUTTON_PRESS; + gdk_event_free(pPeekEvent); + if (bSkip) + { + return false; + } + } + nEventType = SalEvent::MouseButtonDown; + m_nLastMouseClicks = 1; + break; + case GDK_2BUTTON_PRESS: + m_nLastMouseClicks = 2; + nEventType = SalEvent::MouseButtonDown; + break; + case GDK_3BUTTON_PRESS: + m_nLastMouseClicks = 3; + nEventType = SalEvent::MouseButtonDown; + break; + case GDK_BUTTON_RELEASE: + nEventType = SalEvent::MouseButtonUp; + break; + default: + return false; + } + + switch (pEvent->button) + { + case 1: + m_nLastMouseButton = MOUSE_LEFT; + break; + case 2: + m_nLastMouseButton = MOUSE_MIDDLE; + break; + case 3: + m_nLastMouseButton = MOUSE_RIGHT; + break; + default: + return false; + } + + sal_uInt32 nModCode = GtkSalFrame::GetMouseModCode(pEvent->state); + // strip out which buttons are involved from the nModCode and replace with m_nLastMouseButton + sal_uInt16 nCode = m_nLastMouseButton | (nModCode & (KEY_SHIFT | KEY_MOD1 | KEY_MOD2)); + MouseEvent aMEvt(aPos, m_nLastMouseClicks, ImplGetMouseButtonMode(m_nLastMouseButton, nModCode), nCode, nCode); + + if (nEventType == SalEvent::MouseButtonDown) + { + if (!m_aMousePressHdl.IsSet()) + return false; + return m_aMousePressHdl.Call(aMEvt); + } + + if (!m_aMouseReleaseHdl.IsSet()) + return false; + return m_aMouseReleaseHdl.Call(aMEvt); + } +#endif + + bool simple_signal_motion(double x, double y, guint nState) + { + if (!m_aMouseMotionHdl.IsSet()) + return false; + + Point aPos(x, y); + if (SwapForRTL()) + aPos.setX(gtk_widget_get_allocated_width(m_pWidget) - 1 - aPos.X()); + sal_uInt32 nModCode = GtkSalFrame::GetMouseModCode(nState); + MouseEvent aMEvt(aPos, 0, ImplGetMouseMoveMode(nModCode), nModCode, nModCode); + + return m_aMouseMotionHdl.Call(aMEvt); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalMotion(GtkEventControllerMotion *pController, double x, double y, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + GdkModifierType eType = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(pController)); + + SolarMutexGuard aGuard; + pThis->simple_signal_motion(x, y, eType); + } + +#else + static gboolean signalMotion(GtkWidget*, GdkEventMotion* pEvent, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + SolarMutexGuard aGuard; + return pThis->signal_motion(pEvent); + } + + bool signal_motion(const GdkEventMotion* pEvent) + { + const bool bDragData = m_eDragAction != 0 && m_nPressedButton != -1 && m_xDragSource.is() && gtk_drag_source_get_target_list(m_pWidget); + bool bUnsetDragIcon(false); + if (bDragData && gtk_drag_check_threshold(m_pWidget, m_nPressStartX, m_nPressStartY, pEvent->x, pEvent->y) && !do_signal_drag_begin(bUnsetDragIcon)) + { + GdkDragContext* pContext = gtk_drag_begin_with_coordinates(m_pWidget, + gtk_drag_source_get_target_list(m_pWidget), + m_eDragAction, + m_nPressedButton, + const_cast(reinterpret_cast(pEvent)), + m_nPressStartX, m_nPressStartY); + + if (pContext && bUnsetDragIcon) + { + cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 0, 0); + gtk_drag_set_icon_surface(pContext, surface); + cairo_surface_destroy(surface); + } + + m_nPressedButton = -1; + return false; + } + + return simple_signal_motion(pEvent->x, pEvent->y, pEvent->state); + } +#endif + + bool signal_crossing(double x, double y, guint nState, MouseEventModifiers eMouseEventModifiers) + { + if (!m_aMouseMotionHdl.IsSet()) + return false; + + Point aPos(x, y); + if (SwapForRTL()) + aPos.setX(gtk_widget_get_allocated_width(m_pWidget) - 1 - aPos.X()); + sal_uInt32 nModCode = GtkSalFrame::GetMouseModCode(nState); + MouseEventModifiers eModifiers = ImplGetMouseMoveMode(nModCode); + eModifiers = eModifiers | eMouseEventModifiers; + MouseEvent aMEvt(aPos, 0, eModifiers, nModCode, nModCode); + + m_aMouseMotionHdl.Call(aMEvt); + return false; + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalEnter(GtkEventControllerMotion *pController, double x, double y, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + GdkModifierType eType = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(pController)); + SolarMutexGuard aGuard; + pThis->signal_crossing(x, y, eType, MouseEventModifiers::ENTERWINDOW); + } + + static void signalLeave(GtkEventControllerMotion *pController, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + GdkModifierType eType = gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(pController)); + SolarMutexGuard aGuard; + pThis->signal_crossing(-1, -1, eType, MouseEventModifiers::LEAVEWINDOW); + } +#else + static gboolean signalCrossing(GtkWidget*, GdkEventCrossing* pEvent, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + MouseEventModifiers eMouseEventModifiers = pEvent->type == GDK_ENTER_NOTIFY ? MouseEventModifiers::ENTERWINDOW : MouseEventModifiers::LEAVEWINDOW; + SolarMutexGuard aGuard; + return pThis->signal_crossing(pEvent->x, pEvent->y, pEvent->state, eMouseEventModifiers); + } +#endif + + virtual void drag_started() + { + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static gboolean signalDragMotion(GtkWidget *pWidget, GdkDragContext *context, gint x, gint y, guint time, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + if (!pThis->m_bDraggedOver) + { + pThis->m_bDraggedOver = true; + pThis->drag_started(); + } + return pThis->m_xDropTarget->signalDragMotion(pWidget, context, x, y, time); + } + + static gboolean signalDragDrop(GtkWidget* pWidget, GdkDragContext* context, gint x, gint y, guint time, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + return pThis->m_xDropTarget->signalDragDrop(pWidget, context, x, y, time); + } + + static void signalDragDropReceived(GtkWidget* pWidget, GdkDragContext* context, gint x, gint y, GtkSelectionData* data, guint ttype, guint time, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + pThis->m_xDropTarget->signalDragDropReceived(pWidget, context, x, y, data, ttype, time); + } +#endif + + virtual void drag_ended() + { + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static void signalDragLeave(GtkWidget* pWidget, GdkDragContext*, guint /*time*/, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + pThis->m_xDropTarget->signalDragLeave(pWidget); + if (pThis->m_bDraggedOver) + { + pThis->m_bDraggedOver = false; + pThis->drag_ended(); + } + } +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalDragBegin(GtkDragSource* context, GdkDrag*, gpointer widget) +#else + static void signalDragBegin(GtkWidget*, GdkDragContext* context, gpointer widget) +#endif + { + GtkInstanceWidget* pThis = static_cast(widget); + pThis->signal_drag_begin(context); + } + + void ensure_drag_source() + { + if (!m_xDragSource) + { + m_xDragSource.set(new GtkInstDragSource); + +#if !GTK_CHECK_VERSION(4, 0, 0) + m_nDragFailedSignalId = g_signal_connect(m_pWidget, "drag-failed", G_CALLBACK(signalDragFailed), this); + m_nDragDataDeleteignalId = g_signal_connect(m_pWidget, "drag-data-delete", G_CALLBACK(signalDragDelete), this); + m_nDragGetSignalId = g_signal_connect(m_pWidget, "drag-data-get", G_CALLBACK(signalDragDataGet), this); +#endif + + ensure_drag_begin_end(); + } + } + + virtual bool do_signal_drag_begin(bool& rUnsetDragIcon) + { + rUnsetDragIcon = false; + return false; + } + +#if GTK_CHECK_VERSION(4, 0, 0) + virtual void drag_set_icon(GtkDragSource*) +#else + virtual void drag_set_icon(GdkDragContext*) +#endif + { + } + +#if GTK_CHECK_VERSION(4, 0, 0) + void signal_drag_begin(GtkDragSource* context) +#else + void signal_drag_begin(GdkDragContext* context) +#endif + { + bool bUnsetDragIcon(false); + if (do_signal_drag_begin(bUnsetDragIcon)) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + launch_drag_cancel(context); +#endif + return; + } + if (bUnsetDragIcon) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 0, 0); + gtk_drag_set_icon_surface(context, surface); + cairo_surface_destroy(surface); +#endif + } + else + { + drag_set_icon(context); + } + + if (!m_xDragSource) + return; + m_xDragSource->setActiveDragSource(); + } + + virtual void do_signal_drag_end() + { + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalDragEnd(GtkGestureDrag* /*gesture*/, double /*offset_x*/, double /*offset_y*/, gpointer widget) +#else + static void signalDragEnd(GtkWidget* /*widget*/, GdkDragContext* context, gpointer widget) +#endif + { + GtkInstanceWidget* pThis = static_cast(widget); + pThis->do_signal_drag_end(); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (pThis->m_xDragSource.is()) + pThis->m_xDragSource->dragEnd(context); +#endif + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static gboolean signalDragFailed(GtkWidget* /*widget*/, GdkDragContext* /*context*/, GtkDragResult /*result*/, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + pThis->m_xDragSource->dragFailed(); + return false; + } + + static void signalDragDelete(GtkWidget* /*widget*/, GdkDragContext* /*context*/, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + pThis->m_xDragSource->dragDelete(); + } + + static void signalDragDataGet(GtkWidget* /*widget*/, GdkDragContext* /*context*/, GtkSelectionData *data, guint info, + guint /*time*/, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + pThis->m_xDragSource->dragDataGet(data, info); + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + virtual void drag_source_set(const std::vector& rGtkTargets, GdkDragAction eDragAction) + { + if (rGtkTargets.empty() && !eDragAction) + gtk_drag_source_unset(m_pWidget); + else + gtk_drag_source_set(m_pWidget, GDK_BUTTON1_MASK, rGtkTargets.data(), rGtkTargets.size(), eDragAction); + } +#endif + + void do_set_background(const Color& rColor) + { + const bool bRemoveColor = rColor == COL_AUTO; + if (bRemoveColor && !m_pBgCssProvider) + return; + GtkStyleContext *pWidgetContext = gtk_widget_get_style_context(GTK_WIDGET(m_pWidget)); + if (m_pBgCssProvider) + { + gtk_style_context_remove_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pBgCssProvider)); + m_pBgCssProvider = nullptr; + } + if (bRemoveColor) + return; + OUString sColor = rColor.AsRGBHexString(); + m_pBgCssProvider = gtk_css_provider_new(); + OUString aBuffer = "* { background-color: #" + sColor + "; }"; + OString aResult = OUStringToOString(aBuffer, RTL_TEXTENCODING_UTF8); + css_provider_load_from_data(m_pBgCssProvider, aResult.getStr(), aResult.getLength()); + gtk_style_context_add_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pBgCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + + DECL_LINK(SettingsChangedHdl, VclWindowEvent&, void); + +#if !GTK_CHECK_VERSION(4, 0, 0) + static void update_style(GtkWidget* pWidget, gpointer pData) + { + if (GTK_IS_CONTAINER(pWidget)) + gtk_container_foreach(GTK_CONTAINER(pWidget), update_style, pData); + GtkWidgetClass* pWidgetClass = GTK_WIDGET_GET_CLASS(pWidget); + pWidgetClass->style_updated(pWidget); + } +#endif + +public: + GtkInstanceWidget(GtkWidget* pWidget, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : m_pWidget(pWidget) + , m_pMouseEventBox(nullptr) + , m_pBuilder(pBuilder) + , m_bTakeOwnership(bTakeOwnership) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_bDraggedOver(false) +#endif + , m_nWaitCount(0) + , m_nFreezeCount(0) + , m_nLastMouseButton(0) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_nLastMouseClicks(0) +#endif + , m_nPressedButton(-1) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_nPressStartX(-1) + , m_nPressStartY(-1) +#endif + , m_pDragCancelEvent(nullptr) + , m_pBgCssProvider(nullptr) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_eDragAction(GdkDragAction(0)) +#endif + , m_nFocusInSignalId(0) + , m_nMnemonicActivateSignalId(0) + , m_nFocusOutSignalId(0) + , m_nKeyPressSignalId(0) + , m_nKeyReleaseSignalId(0) + , m_nSizeAllocateSignalId(0) + , m_nButtonPressSignalId(0) + , m_nMotionSignalId(0) + , m_nLeaveSignalId(0) + , m_nEnterSignalId(0) + , m_nButtonReleaseSignalId(0) + , m_nDragMotionSignalId(0) + , m_nDragDropSignalId(0) + , m_nDragDropReceivedSignalId(0) + , m_nDragLeaveSignalId(0) + , m_nDragBeginSignalId(0) + , m_nDragEndSignalId(0) + , m_nDragFailedSignalId(0) + , m_nDragDataDeleteignalId(0) + , m_nDragGetSignalId(0) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_nGrabCount(0) + , m_pFocusController(nullptr) + , m_pClickController(nullptr) + , m_pMotionController(nullptr) + , m_pDragController(nullptr) + , m_pKeyController(nullptr) +#endif + { + if (!bTakeOwnership) + g_object_ref(m_pWidget); + + localizeDecimalSeparator(); + } + + virtual void connect_key_press(const Link& rLink) override + { + if (!m_nKeyPressSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + m_nKeyPressSignalId = g_signal_connect(get_key_controller(), "key-pressed", G_CALLBACK(signalKeyPressed), this); +#else + m_nKeyPressSignalId = g_signal_connect(m_pWidget, "key-press-event", G_CALLBACK(signalKey), this); +#endif + } + weld::Widget::connect_key_press(rLink); + } + + virtual void connect_key_release(const Link& rLink) override + { + if (!m_nKeyReleaseSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + m_nKeyReleaseSignalId = g_signal_connect(get_key_controller(), "key-released", G_CALLBACK(signalKeyReleased), this); +#else + m_nKeyReleaseSignalId = g_signal_connect(m_pWidget, "key-release-event", G_CALLBACK(signalKey), this); +#endif + } + weld::Widget::connect_key_release(rLink); + } + + virtual void connect_mouse_press(const Link& rLink) override + { + ensureButtonPressSignal(); + weld::Widget::connect_mouse_press(rLink); + } + + virtual void connect_mouse_move(const Link& rLink) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController* pMotionController = get_motion_controller(); + if (!m_nMotionSignalId) + m_nMotionSignalId = g_signal_connect(pMotionController, "motion", G_CALLBACK(signalMotion), this); + if (!m_nLeaveSignalId) + m_nLeaveSignalId = g_signal_connect(pMotionController, "leave", G_CALLBACK(signalEnter), this); + if (!m_nEnterSignalId) + m_nEnterSignalId = g_signal_connect(pMotionController, "enter", G_CALLBACK(signalLeave), this); +#else + ensureMouseEventWidget(); + if (!m_nMotionSignalId) + m_nMotionSignalId = g_signal_connect(m_pMouseEventBox, "motion-notify-event", G_CALLBACK(signalMotion), this); + if (!m_nLeaveSignalId) + m_nLeaveSignalId = g_signal_connect(m_pMouseEventBox, "leave-notify-event", G_CALLBACK(signalCrossing), this); + if (!m_nEnterSignalId) + m_nEnterSignalId = g_signal_connect(m_pMouseEventBox, "enter-notify-event", G_CALLBACK(signalCrossing), this); +#endif + weld::Widget::connect_mouse_move(rLink); + } + + virtual void connect_mouse_release(const Link& rLink) override + { + ensureButtonReleaseSignal(); + weld::Widget::connect_mouse_release(rLink); + } + + virtual void set_sensitive(bool sensitive) override + { + gtk_widget_set_sensitive(m_pWidget, sensitive); + } + + virtual bool get_sensitive() const override + { + return gtk_widget_get_sensitive(m_pWidget); + } + + virtual bool get_visible() const override + { + return gtk_widget_get_visible(m_pWidget); + } + + virtual bool is_visible() const override + { + return gtk_widget_is_visible(m_pWidget); + } + + virtual void set_can_focus(bool bCanFocus) override + { + gtk_widget_set_can_focus(m_pWidget, bCanFocus); + } + + virtual void grab_focus() override + { + if (has_focus()) + return; + gtk_widget_grab_focus(m_pWidget); + } + + virtual bool has_focus() const override + { + return gtk_widget_has_focus(m_pWidget); + } + + virtual bool is_active() const override + { + GtkWindow* pTopLevel = GTK_WINDOW(widget_get_toplevel(m_pWidget)); + return pTopLevel && gtk_window_is_active(pTopLevel) && has_focus(); + } + + // is the focus in a child of this widget, where a transient popup attached + // to a widget is considered a child of that widget + virtual bool has_child_focus() const override + { + GtkWindow* pFocusWin = get_active_window(); + if (!pFocusWin) + return false; + GtkWidget* pFocus = gtk_window_get_focus(pFocusWin); + if (pFocus && gtk_widget_is_ancestor(pFocus, m_pWidget)) + return true; +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pAttachedTo = gtk_window_get_attached_to(pFocusWin); + if (!pAttachedTo) + return false; + if (pAttachedTo == m_pWidget || gtk_widget_is_ancestor(pAttachedTo, m_pWidget)) + return true; +#endif + return false; + } + + virtual void show() override + { + gtk_widget_show(m_pWidget); + } + + virtual void hide() override + { + gtk_widget_hide(m_pWidget); + } + + virtual void set_size_request(int nWidth, int nHeight) override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_VIEWPORT(pParent)) + pParent = gtk_widget_get_parent(pParent); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + { + gtk_scrolled_window_set_min_content_width(GTK_SCROLLED_WINDOW(pParent), nWidth); + gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(pParent), nHeight); + } + gtk_widget_set_size_request(m_pWidget, nWidth, nHeight); + } + + virtual Size get_size_request() const override + { + int nWidth, nHeight; + gtk_widget_get_size_request(m_pWidget, &nWidth, &nHeight); + return Size(nWidth, nHeight); + } + + virtual Size get_preferred_size() const override + { + GtkRequisition size; + gtk_widget_get_preferred_size(m_pWidget, nullptr, &size); + return Size(size.width, size.height); + } + + virtual float get_approximate_digit_width() const override + { + PangoContext* pContext = gtk_widget_get_pango_context(m_pWidget); + PangoFontMetrics* pMetrics = pango_context_get_metrics(pContext, + pango_context_get_font_description(pContext), + pango_context_get_language(pContext)); + float nDigitWidth = pango_font_metrics_get_approximate_digit_width(pMetrics); + pango_font_metrics_unref(pMetrics); + + return nDigitWidth / PANGO_SCALE; + } + + virtual int get_text_height() const override + { + PangoContext* pContext = gtk_widget_get_pango_context(m_pWidget); + PangoFontMetrics* pMetrics = pango_context_get_metrics(pContext, + pango_context_get_font_description(pContext), + pango_context_get_language(pContext)); + int nLineHeight = pango_font_metrics_get_ascent(pMetrics) + pango_font_metrics_get_descent(pMetrics); + pango_font_metrics_unref(pMetrics); + return nLineHeight / PANGO_SCALE; + } + + virtual Size get_pixel_size(const OUString& rText) const override + { + OString aStr(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)); + PangoLayout* pLayout = gtk_widget_create_pango_layout(m_pWidget, aStr.getStr()); + gint nWidth, nHeight; + pango_layout_get_pixel_size(pLayout, &nWidth, &nHeight); + g_object_unref(pLayout); + return Size(nWidth, nHeight); + } + + virtual vcl::Font get_font() override + { + return ::get_font(m_pWidget); + } + + virtual void set_grid_left_attach(int nAttach) override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); +#if GTK_CHECK_VERSION(4, 0, 0) + int row, width, height; + gtk_grid_query_child(GTK_GRID(pParent), m_pWidget, nullptr, &row, &width, &height); + g_object_ref(m_pWidget); + gtk_grid_remove(GTK_GRID(pParent), m_pWidget); + gtk_grid_attach(GTK_GRID(pParent), m_pWidget, nAttach, row, width, height); + g_object_unref(m_pWidget); +#else + gtk_container_child_set(GTK_CONTAINER(pParent), m_pWidget, "left-attach", nAttach, nullptr); +#endif + } + + virtual int get_grid_left_attach() const override + { + gint nAttach(0); + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_grid_query_child(GTK_GRID(pParent), m_pWidget, &nAttach, nullptr, nullptr, nullptr); +#else + gtk_container_child_get(GTK_CONTAINER(pParent), m_pWidget, "left-attach", &nAttach, nullptr); +#endif + return nAttach; + } + + virtual void set_grid_width(int nCols) override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); +#if GTK_CHECK_VERSION(4, 0, 0) + int col, row, height; + gtk_grid_query_child(GTK_GRID(pParent), m_pWidget, &col, &row, nullptr, &height); + g_object_ref(m_pWidget); + gtk_grid_remove(GTK_GRID(pParent), m_pWidget); + gtk_grid_attach(GTK_GRID(pParent), m_pWidget, col, row, nCols, height); + g_object_unref(m_pWidget); +#else + gtk_container_child_set(GTK_CONTAINER(pParent), m_pWidget, "width", nCols, nullptr); +#endif + } + + virtual void set_grid_top_attach(int nAttach) override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); +#if GTK_CHECK_VERSION(4, 0, 0) + int col, width, height; + gtk_grid_query_child(GTK_GRID(pParent), m_pWidget, &col, nullptr, &width, &height); + g_object_ref(m_pWidget); + gtk_grid_remove(GTK_GRID(pParent), m_pWidget); + gtk_grid_attach(GTK_GRID(pParent), m_pWidget, col, nAttach, width, height); + g_object_unref(m_pWidget); +#else + gtk_container_child_set(GTK_CONTAINER(pParent), m_pWidget, "top-attach", nAttach, nullptr); +#endif + } + + virtual int get_grid_top_attach() const override + { + gint nAttach(0); + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_grid_query_child(GTK_GRID(pParent), m_pWidget, nullptr, &nAttach, nullptr, nullptr); +#else + gtk_container_child_get(GTK_CONTAINER(pParent), m_pWidget, "top-attach", &nAttach, nullptr); +#endif + return nAttach; + } + + virtual void set_hexpand(bool bExpand) override + { + gtk_widget_set_hexpand(m_pWidget, bExpand); + } + + virtual bool get_hexpand() const override + { + return gtk_widget_get_hexpand(m_pWidget); + } + + virtual void set_vexpand(bool bExpand) override + { + gtk_widget_set_vexpand(m_pWidget, bExpand); + } + + virtual bool get_vexpand() const override + { + return gtk_widget_get_vexpand(m_pWidget); + } + + virtual void set_margin_top(int nMargin) override + { + gtk_widget_set_margin_top(m_pWidget, nMargin); + } + + virtual void set_margin_bottom(int nMargin) override + { + gtk_widget_set_margin_bottom(m_pWidget, nMargin); + } + + virtual void set_margin_start(int nMargin) override + { + gtk_widget_set_margin_start(m_pWidget, nMargin); + } + + virtual void set_margin_end(int nMargin) override + { + gtk_widget_set_margin_end(m_pWidget, nMargin); + } + + virtual int get_margin_top() const override + { + return gtk_widget_get_margin_top(m_pWidget); + } + + virtual int get_margin_bottom() const override + { + return gtk_widget_get_margin_bottom(m_pWidget); + } + + virtual int get_margin_start() const override + { + return gtk_widget_get_margin_start(m_pWidget); + } + + virtual int get_margin_end() const override + { + return gtk_widget_get_margin_end(m_pWidget); + } + + virtual void set_accessible_name(const OUString& rName) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_accessible_update_property(GTK_ACCESSIBLE(m_pWidget), GTK_ACCESSIBLE_PROPERTY_LABEL, + OUStringToOString(rName, RTL_TEXTENCODING_UTF8).getStr(), -1); +#else + AtkObject* pAtkObject = gtk_widget_get_accessible(m_pWidget); + if (!pAtkObject) + return; + atk_object_set_name(pAtkObject, OUStringToOString(rName, RTL_TEXTENCODING_UTF8).getStr()); +#endif + } + + virtual void set_accessible_description(const OUString& rDescription) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_accessible_update_property(GTK_ACCESSIBLE(m_pWidget), GTK_ACCESSIBLE_PROPERTY_DESCRIPTION, + OUStringToOString(rDescription, RTL_TEXTENCODING_UTF8).getStr(), -1); +#else + AtkObject* pAtkObject = gtk_widget_get_accessible(m_pWidget); + if (!pAtkObject) + return; + atk_object_set_description(pAtkObject, OUStringToOString(rDescription, RTL_TEXTENCODING_UTF8).getStr()); +#endif + } + + virtual OUString get_accessible_name() const override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + AtkObject* pAtkObject = gtk_widget_get_accessible(m_pWidget); + const char* pStr = pAtkObject ? atk_object_get_name(pAtkObject) : nullptr; + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); +#else + char* pStr = gtk_test_accessible_check_property(GTK_ACCESSIBLE(m_pWidget), GTK_ACCESSIBLE_PROPERTY_LABEL, nullptr); + OUString sRet(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + g_free(pStr); + return sRet; +#endif + } + + virtual OUString get_accessible_description() const override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + AtkObject* pAtkObject = gtk_widget_get_accessible(m_pWidget); + const char* pStr = pAtkObject ? atk_object_get_description(pAtkObject) : nullptr; + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); +#else + char* pStr = gtk_test_accessible_check_property(GTK_ACCESSIBLE(m_pWidget), GTK_ACCESSIBLE_PROPERTY_DESCRIPTION, nullptr); + OUString sRet(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + g_free(pStr); + return sRet; +#endif + } + + virtual void set_accessible_relation_labeled_by(weld::Widget* pLabel) override + { + GtkWidget* pGtkLabel = pLabel ? dynamic_cast(*pLabel).getWidget() : nullptr; +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_accessible_update_relation(GTK_ACCESSIBLE(m_pWidget), + GTK_ACCESSIBLE_RELATION_LABELLED_BY, + pGtkLabel, nullptr, + -1); +#else + AtkObject* pAtkObject = gtk_widget_get_accessible(m_pWidget); + if (!pAtkObject) + return; + AtkObject *pAtkLabel = pGtkLabel ? gtk_widget_get_accessible(pGtkLabel) : nullptr; + AtkRelationSet *pRelationSet = atk_object_ref_relation_set(pAtkObject); + AtkRelation *pRelation = atk_relation_set_get_relation_by_type(pRelationSet, ATK_RELATION_LABELLED_BY); + if (pRelation) + { + // clear ATK_RELATION_LABEL_FOR from old label + GPtrArray* pOldLabelTarget = atk_relation_get_target(pRelation); + guint nElements = pOldLabelTarget ? pOldLabelTarget->len : 0; + for (guint i = 0; i < nElements; ++i) + { + gpointer pOldLabelObject = g_ptr_array_index(pOldLabelTarget, i); + AtkRelationSet *pOldLabelRelationSet = atk_object_ref_relation_set(ATK_OBJECT(pOldLabelObject)); + if (AtkRelation *pOldLabelRelation = atk_relation_set_get_relation_by_type(pRelationSet, ATK_RELATION_LABEL_FOR)) + atk_relation_set_remove(pOldLabelRelationSet, pOldLabelRelation); + g_object_unref(pOldLabelRelationSet); + } + atk_relation_set_remove(pRelationSet, pRelation); + } + + if (pAtkLabel) + { + AtkObject *obj_array_labelled_by[1]; + obj_array_labelled_by[0] = pAtkLabel; + pRelation = atk_relation_new(obj_array_labelled_by, 1, ATK_RELATION_LABELLED_BY); + atk_relation_set_add(pRelationSet, pRelation); + + // add ATK_RELATION_LABEL_FOR to new label to match + AtkRelationSet *pNewLabelRelationSet = atk_object_ref_relation_set(pAtkLabel); + AtkRelation *pNewLabelRelation = atk_relation_set_get_relation_by_type(pNewLabelRelationSet, ATK_RELATION_LABEL_FOR); + if (pNewLabelRelation) + atk_relation_set_remove(pNewLabelRelationSet, pRelation); + AtkObject *obj_array_label_for[1]; + obj_array_label_for[0] = pAtkObject; + pNewLabelRelation = atk_relation_new(obj_array_label_for, 1, ATK_RELATION_LABEL_FOR); + atk_relation_set_add(pNewLabelRelationSet, pNewLabelRelation); + g_object_unref(pNewLabelRelationSet); + } + + g_object_unref(pRelationSet); +#endif + } + + virtual bool get_extents_relative_to(const weld::Widget& rRelative, int& x, int &y, int& width, int &height) const override + { + //for toplevel windows this is sadly futile under wayland, so we can't tell where a dialog is in order to allow + //the document underneath to auto-scroll to place content in a visible location + gtk_coord fX(0.0), fY(0.0); + bool ret = gtk_widget_translate_coordinates(m_pWidget, + dynamic_cast(rRelative).getWidget(), + 0, 0, &fX, &fY); + x = fX; + y = fY; + width = gtk_widget_get_allocated_width(m_pWidget); + height = gtk_widget_get_allocated_height(m_pWidget); + return ret; + } + + virtual void set_tooltip_text(const OUString& rTip) override + { + gtk_widget_set_tooltip_text(m_pWidget, OUStringToOString(rTip, RTL_TEXTENCODING_UTF8).getStr()); + } + + virtual OUString get_tooltip_text() const override + { + const gchar* pStr = gtk_widget_get_tooltip_text(m_pWidget); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual void set_cursor_data(void * /*pData*/) override {}; + + virtual std::unique_ptr weld_parent() const override; + + virtual OUString get_buildable_name() const override + { + return ::get_buildable_id(GTK_BUILDABLE(m_pWidget)); + } + + virtual void set_buildable_name(const OUString& rId) override + { + ::set_buildable_id(GTK_BUILDABLE(m_pWidget), rId); + } + + virtual void set_help_id(const OUString& rHelpId) override + { + ::set_help_id(m_pWidget, rHelpId); + } + + virtual OUString get_help_id() const override + { + OUString sRet = ::get_help_id(m_pWidget); + if (sRet.isEmpty()) + sRet = "null"; + return sRet; + } + + GtkWidget* getWidget() const + { + return m_pWidget; + } + + GtkWindow* getWindow() const + { + return GTK_WINDOW(widget_get_toplevel(m_pWidget)); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController* get_focus_controller() + { + if (!m_pFocusController) + { + gtk_widget_set_focusable(m_pWidget, true); + m_pFocusController = gtk_event_controller_focus_new(); + gtk_widget_add_controller(m_pWidget, m_pFocusController); + } + return m_pFocusController; + } + +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController* get_click_controller() + { + if (!m_pClickController) + { + GtkGesture *pClick = gtk_gesture_click_new(); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(pClick), 0); + m_pClickController = GTK_EVENT_CONTROLLER(pClick); + gtk_widget_add_controller(m_pWidget, m_pClickController); + } + return m_pClickController; + } + + GtkEventController* get_motion_controller() + { + if (!m_pMotionController) + { + m_pMotionController = gtk_event_controller_motion_new(); + gtk_widget_add_controller(m_pWidget, m_pMotionController); + } + return m_pMotionController; + } + + GtkEventController* get_drag_controller() + { + if (!m_pDragController) + { + GtkDragSource* pDrag = gtk_drag_source_new(); + m_pDragController = GTK_EVENT_CONTROLLER(pDrag); + gtk_widget_add_controller(m_pWidget, m_pDragController); + } + return m_pDragController; + } + + GtkEventController* get_key_controller() + { + if (!m_pKeyController) + { + m_pKeyController = gtk_event_controller_key_new(); + gtk_widget_add_controller(m_pWidget, m_pKeyController); + } + return m_pKeyController; + } + +#endif + + +#endif + + virtual void connect_focus_in(const Link& rLink) override + { + if (!m_nFocusInSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + m_nFocusInSignalId = g_signal_connect(get_focus_controller(), "enter", G_CALLBACK(signalFocusIn), this); +#else + m_nFocusInSignalId = g_signal_connect(m_pWidget, "focus-in-event", G_CALLBACK(signalFocusIn), this); +#endif + } + + weld::Widget::connect_focus_in(rLink); + } + + virtual void connect_mnemonic_activate(const Link& rLink) override + { + if (!m_nMnemonicActivateSignalId) + m_nMnemonicActivateSignalId = g_signal_connect(m_pWidget, "mnemonic-activate", G_CALLBACK(signalMnemonicActivate), this); + weld::Widget::connect_mnemonic_activate(rLink); + } + + virtual void connect_focus_out(const Link& rLink) override + { + if (!m_nFocusOutSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + m_nFocusOutSignalId = g_signal_connect(get_focus_controller(), "leave", G_CALLBACK(signalFocusOut), this); +#else + m_nFocusOutSignalId = g_signal_connect(m_pWidget, "focus-out-event", G_CALLBACK(signalFocusOut), this); +#endif + } + weld::Widget::connect_focus_out(rLink); + } + + virtual void connect_size_allocate(const Link& rLink) override + { + m_nSizeAllocateSignalId = g_signal_connect(m_pWidget, "size-allocate", G_CALLBACK(signalSizeAllocate), this); + weld::Widget::connect_size_allocate(rLink); + } + + virtual void signal_size_allocate(guint nWidth, guint nHeight) + { + m_aSizeAllocateHdl.Call(Size(nWidth, nHeight)); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + bool signal_key_press(guint keyval, guint keycode, GdkModifierType state) + { + if (m_aKeyPressHdl.IsSet()) + { + SolarMutexGuard aGuard; + return m_aKeyPressHdl.Call(CreateKeyEvent(keyval, keycode, state, 0)); + } + return false; + } + + bool signal_key_release(guint keyval, guint keycode, GdkModifierType state) + { + if (m_aKeyReleaseHdl.IsSet()) + { + SolarMutexGuard aGuard; + return m_aKeyReleaseHdl.Call(CreateKeyEvent(keyval, keycode, state, 0)); + } + return false; + } +#else + + virtual bool do_signal_key_press(const GdkEventKey* pEvent) + { + if (m_aKeyPressHdl.IsSet()) + { + SolarMutexGuard aGuard; + return m_aKeyPressHdl.Call(GtkToVcl(*pEvent)); + } + return false; + } + + virtual bool do_signal_key_release(const GdkEventKey* pEvent) + { + if (m_aKeyReleaseHdl.IsSet()) + { + SolarMutexGuard aGuard; + return m_aKeyReleaseHdl.Call(GtkToVcl(*pEvent)); + } + return false; + } + + bool signal_key_press(const GdkEventKey* pEvent) + { + return do_signal_key_press(pEvent); + } + + bool signal_key_release(const GdkEventKey* pEvent) + { + return do_signal_key_release(pEvent); + } +#endif + + virtual void grab_add() override + { +#if GTK_CHECK_VERSION(4, 0, 0) + ++m_nGrabCount; +#else + gtk_grab_add(m_pWidget); +#endif + } + + virtual bool has_grab() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + return m_nGrabCount != 0; +#else + return gtk_widget_has_grab(m_pWidget); +#endif + } + + virtual void grab_remove() override + { +#if GTK_CHECK_VERSION(4, 0, 0) + --m_nGrabCount; +#else + gtk_grab_remove(m_pWidget); +#endif + } + + virtual bool get_direction() const override + { + return gtk_widget_get_direction(m_pWidget) == GTK_TEXT_DIR_RTL; + } + + virtual void set_direction(bool bRTL) override + { + gtk_widget_set_direction(m_pWidget, bRTL ? GTK_TEXT_DIR_RTL : GTK_TEXT_DIR_LTR); + } + + virtual void freeze() override + { + ++m_nFreezeCount; +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_freeze_child_notify(m_pWidget); +#endif + g_object_freeze_notify(G_OBJECT(m_pWidget)); + } + + virtual void thaw() override + { + --m_nFreezeCount; + g_object_thaw_notify(G_OBJECT(m_pWidget)); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_thaw_child_notify(m_pWidget); +#endif + } + + virtual void set_busy_cursor(bool bBusy) override + { + if (bBusy) + ++m_nWaitCount; + else + --m_nWaitCount; + if (m_nWaitCount == 1) + set_cursor(m_pWidget, "progress"); + else if (m_nWaitCount == 0) + set_cursor(m_pWidget, nullptr); + assert (m_nWaitCount >= 0); + } + + virtual void queue_resize() override + { + gtk_widget_queue_resize(m_pWidget); + } + + virtual css::uno::Reference get_drop_target() override + { + if (!m_xDropTarget) + { + m_xDropTarget.set(new GtkInstDropTarget); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!gtk_drag_dest_get_track_motion(m_pWidget)) + { + gtk_drag_dest_set(m_pWidget, GtkDestDefaults(0), nullptr, 0, GdkDragAction(0)); + gtk_drag_dest_set_track_motion(m_pWidget, true); + } + m_nDragMotionSignalId = g_signal_connect(m_pWidget, "drag-motion", G_CALLBACK(signalDragMotion), this); + m_nDragDropSignalId = g_signal_connect(m_pWidget, "drag-drop", G_CALLBACK(signalDragDrop), this); + m_nDragDropReceivedSignalId = g_signal_connect(m_pWidget, "drag-data-received", G_CALLBACK(signalDragDropReceived), this); + m_nDragLeaveSignalId = g_signal_connect(m_pWidget, "drag-leave", G_CALLBACK(signalDragLeave), this); +#endif + } + return m_xDropTarget; + } + + virtual css::uno::Reference get_clipboard() const override + { + // the gen backend can have per-frame clipboards which is (presumably) useful for LibreOffice Online + // but normal usage is the shared system clipboard + return GetSystemClipboard(); + } + + virtual void connect_get_property_tree(const Link& /*rLink*/) override + { + //not implemented for the gtk variant + } + + virtual void get_property_tree(tools::JsonWriter& /*rJsonWriter*/) override + { + //not implemented for the gtk variant + } + + virtual void call_attention_to() override + { + // Change the class name to restart the animation under + // its other name: https://css-tricks.com/restart-css-animation/ +#if GTK_CHECK_VERSION(4, 0, 0) + if (gtk_widget_has_css_class(m_pWidget, "call_attention_1")) + { + gtk_widget_remove_css_class(m_pWidget, "call_attention_1"); + gtk_widget_add_css_class(m_pWidget, "call_attention_2"); + } + else + { + gtk_widget_remove_css_class(m_pWidget, "call_attention_2"); + gtk_widget_add_css_class(m_pWidget, "call_attention_1"); + } +#else + GtkStyleContext *pWidgetContext = gtk_widget_get_style_context(m_pWidget); + if (gtk_style_context_has_class(pWidgetContext, "call_attention_1")) + { + gtk_style_context_remove_class(pWidgetContext, "call_attention_1"); + gtk_style_context_add_class(pWidgetContext, "call_attention_2"); + } + else + { + gtk_style_context_remove_class(pWidgetContext, "call_attention_2"); + gtk_style_context_add_class(pWidgetContext, "call_attention_1"); + } +#endif + } + + virtual void set_stack_background() override + { + do_set_background(Application::GetSettings().GetStyleSettings().GetWindowColor()); + } + + virtual void set_title_background() override + { + do_set_background(Application::GetSettings().GetStyleSettings().GetShadowColor()); + } + + virtual void set_highlight_background() override + { + do_set_background(Application::GetSettings().GetStyleSettings().GetHighlightColor()); + } + + virtual void set_background(const Color& rColor) override + { + do_set_background(rColor); + } + + virtual void set_toolbar_background() override + { + // no-op + } + + virtual ~GtkInstanceWidget() override + { + if (m_aStyleUpdatedHdl.IsSet()) + ImplGetDefaultWindow()->RemoveEventListener(LINK(this, GtkInstanceWidget, SettingsChangedHdl)); + + if (m_pDragCancelEvent) + Application::RemoveUserEvent(m_pDragCancelEvent); + if (m_nDragMotionSignalId) + g_signal_handler_disconnect(m_pWidget, m_nDragMotionSignalId); + if (m_nDragDropSignalId) + g_signal_handler_disconnect(m_pWidget, m_nDragDropSignalId); + if (m_nDragDropReceivedSignalId) + g_signal_handler_disconnect(m_pWidget, m_nDragDropReceivedSignalId); + if (m_nDragLeaveSignalId) + g_signal_handler_disconnect(m_pWidget, m_nDragLeaveSignalId); + if (m_nDragEndSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_drag_controller(), m_nDragEndSignalId); +#else + g_signal_handler_disconnect(m_pWidget, m_nDragEndSignalId); +#endif + } + if (m_nDragBeginSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_drag_controller(), m_nDragBeginSignalId); +#else + g_signal_handler_disconnect(m_pWidget, m_nDragBeginSignalId); +#endif + } + if (m_nDragFailedSignalId) + g_signal_handler_disconnect(m_pWidget, m_nDragFailedSignalId); + if (m_nDragDataDeleteignalId) + g_signal_handler_disconnect(m_pWidget, m_nDragDataDeleteignalId); + if (m_nDragGetSignalId) + g_signal_handler_disconnect(m_pWidget, m_nDragGetSignalId); + if (m_nKeyPressSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_key_controller(), m_nKeyPressSignalId); +#else + g_signal_handler_disconnect(m_pWidget, m_nKeyPressSignalId); +#endif + } + if (m_nKeyReleaseSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_key_controller(), m_nKeyReleaseSignalId); +#else + g_signal_handler_disconnect(m_pWidget, m_nKeyReleaseSignalId); +#endif + } + + if (m_nFocusInSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_focus_controller(), m_nFocusInSignalId); +#else + g_signal_handler_disconnect(m_pWidget, m_nFocusInSignalId); +#endif + } + if (m_nMnemonicActivateSignalId) + g_signal_handler_disconnect(m_pWidget, m_nMnemonicActivateSignalId); + if (m_nFocusOutSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(get_focus_controller(), m_nFocusOutSignalId); +#else + g_signal_handler_disconnect(m_pWidget, m_nFocusOutSignalId); +#endif + } + if (m_nSizeAllocateSignalId) + g_signal_handler_disconnect(m_pWidget, m_nSizeAllocateSignalId); + + do_set_background(COL_AUTO); + + DisconnectMouseEvents(); + + if (m_bTakeOwnership) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_destroy(m_pWidget); +#else + gtk_window_destroy(GTK_WINDOW(m_pWidget)); +#endif + } + else + g_object_unref(m_pWidget); + } + + virtual void disable_notify_events() + { + if (m_nFocusInSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_block(get_focus_controller(), m_nFocusInSignalId); +#else + g_signal_handler_block(m_pWidget, m_nFocusInSignalId); +#endif + } + if (m_nMnemonicActivateSignalId) + g_signal_handler_block(m_pWidget, m_nMnemonicActivateSignalId); + if (m_nFocusOutSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_block(get_focus_controller(), m_nFocusOutSignalId); +#else + g_signal_handler_block(m_pWidget, m_nFocusOutSignalId); +#endif + } + if (m_nSizeAllocateSignalId) + g_signal_handler_block(m_pWidget, m_nSizeAllocateSignalId); + } + + virtual void enable_notify_events() + { + if (m_nSizeAllocateSignalId) + g_signal_handler_unblock(m_pWidget, m_nSizeAllocateSignalId); + if (m_nFocusOutSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_unblock(get_focus_controller(), m_nFocusOutSignalId); +#else + g_signal_handler_unblock(m_pWidget, m_nFocusOutSignalId); +#endif + } + if (m_nMnemonicActivateSignalId) + g_signal_handler_unblock(m_pWidget, m_nMnemonicActivateSignalId); + + if (m_nFocusInSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_unblock(get_focus_controller(), m_nFocusInSignalId); +#else + g_signal_handler_unblock(m_pWidget, m_nFocusInSignalId); +#endif + } + } + + virtual void help_hierarchy_foreach(const std::function& func) override; + + virtual OUString strip_mnemonic(const OUString &rLabel) const override + { + return rLabel.replaceFirst("_", ""); + } + + virtual VclPtr create_virtual_device() const override + { + // create with no separate alpha layer like everything sane does + auto xRet = VclPtr::Create(); + xRet->SetBackground(COL_TRANSPARENT); + return xRet; + } + + virtual void draw(OutputDevice& rOutput, const Point& rPos, const Size& rPixelSize) override + { + // detect if we have to manually setup its size + bool bAlreadyRealized = gtk_widget_get_realized(m_pWidget); + // has to be visible for draw to work + bool bAlreadyVisible = gtk_widget_get_visible(m_pWidget); + // has to be mapped for draw to work + bool bAlreadyMapped = gtk_widget_get_mapped(m_pWidget); + + if (!bAlreadyRealized) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + /* + tdf#141633 The "sample db" example (Mockup.odb) has multiline + entries used in its "Journal Entry" column. Those are painted by + taking snapshots of a never-really-shown textview widget. + Without this style_updated then the textview is always drawn + using its original default font size and changing the page zoom + has no effect on the size of text in the "Journal Entry" column. + */ + update_style(m_pWidget, nullptr); +#endif + gtk_widget_realize(m_pWidget); + } + if (!bAlreadyVisible) + gtk_widget_show(m_pWidget); + if (!bAlreadyMapped) + gtk_widget_map(m_pWidget); + + assert(gtk_widget_is_drawable(m_pWidget)); // all that should result in this holding + + // turn off animations, otherwise we get a frame of an animation sequence + gboolean bAnimations; + GtkSettings* pSettings = gtk_widget_get_settings(m_pWidget); + g_object_get(pSettings, "gtk-enable-animations", &bAnimations, nullptr); + if (bAnimations) + g_object_set(pSettings, "gtk-enable-animations", false, nullptr); + + Size aSize(rPixelSize); + + GtkAllocation aOrigAllocation; + gtk_widget_get_allocation(m_pWidget, &aOrigAllocation); + + GtkAllocation aNewAllocation {aOrigAllocation.x, + aOrigAllocation.y, + static_cast(aSize.Width()), + static_cast(aSize.Height()) }; +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_size_allocate(m_pWidget, &aNewAllocation); +#else + gtk_widget_size_allocate(m_pWidget, &aNewAllocation, 0); +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + if (GTK_IS_CONTAINER(m_pWidget)) + gtk_container_resize_children(GTK_CONTAINER(m_pWidget)); +#endif + + VclPtr xOutput(VclPtr::Create(DeviceFormat::WITHOUT_ALPHA)); + xOutput->SetOutputSizePixel(aSize); + + switch (rOutput.GetOutDevType()) + { + case OUTDEV_WINDOW: + case OUTDEV_VIRDEV: + xOutput->DrawOutDev(Point(), aSize, rPos, aSize, rOutput); + break; + case OUTDEV_PRINTER: + case OUTDEV_PDF: + xOutput->SetBackground(rOutput.GetBackground()); + xOutput->Erase(); + break; + } + + cairo_surface_t* pSurface = get_underlying_cairo_surface(*xOutput); + cairo_t* cr = cairo_create(pSurface); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_draw(m_pWidget, cr); +#else + GtkSnapshot* pSnapshot = gtk_snapshot_new(); + GtkWidgetClass* pWidgetClass = GTK_WIDGET_GET_CLASS(m_pWidget); + pWidgetClass->snapshot(m_pWidget, pSnapshot); + GskRenderNode* pNode = gtk_snapshot_free_to_node(pSnapshot); + gsk_render_node_draw(pNode, cr); + gsk_render_node_unref(pNode); +#endif + + cairo_destroy(cr); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_allocation(m_pWidget, &aOrigAllocation); + gtk_widget_size_allocate(m_pWidget, &aOrigAllocation); +#else + gtk_widget_size_allocate(m_pWidget, &aOrigAllocation, 0); +#endif + + switch (rOutput.GetOutDevType()) + { + case OUTDEV_WINDOW: + case OUTDEV_VIRDEV: + rOutput.DrawOutDev(rPos, aSize, Point(), aSize, *xOutput); + break; + case OUTDEV_PRINTER: + case OUTDEV_PDF: + rOutput.DrawBitmapEx(rPos, xOutput->GetBitmapEx(Point(), aSize)); + break; + } + + if (bAnimations) + g_object_set(pSettings, "gtk-enable-animations", true, nullptr); + + if (!bAlreadyMapped) + gtk_widget_unmap(m_pWidget); + if (!bAlreadyVisible) + gtk_widget_hide(m_pWidget); + if (!bAlreadyRealized) + gtk_widget_unrealize(m_pWidget); + } +}; + +} + +IMPL_LINK(GtkInstanceWidget, SettingsChangedHdl, VclWindowEvent&, rEvent, void) +{ + if (rEvent.GetId() != VclEventId::WindowDataChanged) + return; + + DataChangedEvent* pData = static_cast(rEvent.GetData()); + if (pData->GetType() == DataChangedEventType::SETTINGS) + m_aStyleUpdatedHdl.Call(*this); +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +IMPL_LINK(GtkInstanceWidget, async_drag_cancel, void*, arg, void) +{ + m_pDragCancelEvent = nullptr; + GdkDragContext* context = static_cast(arg); + + // tdf#132477 simply calling gtk_drag_cancel on the treeview dnd under X + // doesn't seem to work as hoped for (though under wayland all is well). + // Under X the next (allowed) drag effort doesn't work to drop anything, + // but a then repeated attempt does. + // emitting cancel to get gtk to cancel the drag for us does work as hoped for. + g_signal_emit_by_name(context, "cancel", 0, GDK_DRAG_CANCEL_USER_CANCELLED); + + g_object_unref(context); +} +#endif + +namespace +{ + OString MapToGtkAccelerator(const OUString &rStr) + { + return OUStringToOString(rStr.replaceFirst("~", "_"), RTL_TEXTENCODING_UTF8); + } + + OUString get_label(GtkLabel* pLabel) + { + const gchar* pStr = gtk_label_get_label(pLabel); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + void set_label(GtkLabel* pLabel, const OUString& rText) + { + gtk_label_set_label(pLabel, MapToGtkAccelerator(rText).getStr()); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* find_label_widget(GtkWidget* pContainer) + { + GtkWidget* pLabel = nullptr; + for (GtkWidget* pChild = gtk_widget_get_first_child(pContainer); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + if (GTK_IS_LABEL(pChild)) + { + pLabel = pChild; + break; + } + else + { + pLabel = find_label_widget(pChild); + if (pLabel) + break; + } + } + return pLabel; + } + + GtkWidget* find_image_widget(GtkWidget* pContainer) + { + GtkWidget* pImage = nullptr; + for (GtkWidget* pChild = gtk_widget_get_first_child(pContainer); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + if (GTK_IS_IMAGE(pChild)) + { + pImage = pChild; + break; + } + else + { + pImage = find_image_widget(pChild); + if (pImage) + break; + } + } + return pImage; + } +#else + GtkWidget* find_label_widget(GtkContainer* pContainer) + { + GList* pChildren = gtk_container_get_children(pContainer); + + GtkWidget* pChild = nullptr; + for (GList* pCandidate = pChildren; pCandidate; pCandidate = pCandidate->next) + { + if (GTK_IS_LABEL(pCandidate->data)) + { + pChild = GTK_WIDGET(pCandidate->data); + break; + } + else if (GTK_IS_CONTAINER(pCandidate->data)) + { + pChild = find_label_widget(GTK_CONTAINER(pCandidate->data)); + if (pChild) + break; + } + } + g_list_free(pChildren); + + return pChild; + } + + GtkWidget* find_image_widget(GtkContainer* pContainer) + { + GList* pChildren = gtk_container_get_children(pContainer); + + GtkWidget* pChild = nullptr; + for (GList* pCandidate = pChildren; pCandidate; pCandidate = pCandidate->next) + { + if (GTK_IS_IMAGE(pCandidate->data)) + { + pChild = GTK_WIDGET(pCandidate->data); + break; + } + else if (GTK_IS_CONTAINER(pCandidate->data)) + { + pChild = find_image_widget(GTK_CONTAINER(pCandidate->data)); + if (pChild) + break; + } + } + g_list_free(pChildren); + + return pChild; + } +#endif + + GtkLabel* get_label_widget(GtkWidget* pButton) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pChild = gtk_bin_get_child(GTK_BIN(pButton)); + + if (GTK_IS_CONTAINER(pChild)) + pChild = find_label_widget(GTK_CONTAINER(pChild)); + else if (!GTK_IS_LABEL(pChild)) + pChild = nullptr; + + return GTK_LABEL(pChild); +#else + return GTK_LABEL(find_label_widget(pButton)); +#endif + } + + GtkImage* get_image_widget(GtkWidget *pButton) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pChild = gtk_bin_get_child(GTK_BIN(pButton)); + + if (GTK_IS_CONTAINER(pChild)) + pChild = find_image_widget(GTK_CONTAINER(pChild)); + else if (!GTK_IS_IMAGE(pChild)) + pChild = nullptr; + + return GTK_IMAGE(pChild); +#else + return GTK_IMAGE(find_image_widget(pButton)); +#endif + } + + OUString button_get_label(GtkButton* pButton) + { + if (GtkLabel* pLabel = get_label_widget(GTK_WIDGET(pButton))) + return ::get_label(pLabel); + const gchar* pStr = gtk_button_get_label(pButton); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + void button_set_label(GtkButton* pButton, const OUString& rText) + { + if (GtkLabel* pLabel = get_label_widget(GTK_WIDGET(pButton))) + { + ::set_label(pLabel, rText); + gtk_widget_set_visible(GTK_WIDGET(pLabel), true); + return; + } + gtk_button_set_label(pButton, MapToGtkAccelerator(rText).getStr()); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + OUString get_label(GtkCheckButton* pButton) + { + const gchar* pStr = gtk_check_button_get_label(pButton); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + void set_label(GtkCheckButton* pButton, const OUString& rText) + { + gtk_check_button_set_label(pButton, MapToGtkAccelerator(rText).getStr()); + } +#endif + + OUString get_title(GtkWindow* pWindow) + { + const gchar* pStr = gtk_window_get_title(pWindow); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + void set_title(GtkWindow* pWindow, std::u16string_view rTitle) + { + gtk_window_set_title(pWindow, OUStringToOString(rTitle, RTL_TEXTENCODING_UTF8).getStr()); + } + + OUString get_primary_text(GtkMessageDialog* pMessageDialog) + { + gchar* pText = nullptr; + g_object_get(G_OBJECT(pMessageDialog), "text", &pText, nullptr); + return OUString(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + } + + void set_primary_text(GtkMessageDialog* pMessageDialog, std::u16string_view rText) + { + g_object_set(G_OBJECT(pMessageDialog), "text", + OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr(), + nullptr); + } + + void set_secondary_text(GtkMessageDialog* pMessageDialog, std::u16string_view rText) + { + g_object_set(G_OBJECT(pMessageDialog), "secondary-text", + OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr(), + nullptr); + } + + OUString get_secondary_text(GtkMessageDialog* pMessageDialog) + { + gchar* pText = nullptr; + g_object_get(G_OBJECT(pMessageDialog), "secondary-text", &pText, nullptr); + return OUString(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + } +} + +namespace +{ + GdkPixbuf* load_icon_from_stream(SvMemoryStream& rStream) + { + auto nLength = rStream.TellEnd(); + if (!nLength) + return nullptr; + const guchar* pData = static_cast(rStream.GetData()); + assert((*pData == 137 || *pData == '<') && "if we want to support more than png or svg this function must change"); + // if we know the image type, it's a little faster to hand the type over and skip the type detection. + GdkPixbufLoader *pixbuf_loader = gdk_pixbuf_loader_new_with_type(*pData == 137 ? "png" : "svg", nullptr); + gdk_pixbuf_loader_write(pixbuf_loader, pData, nLength, nullptr); + gdk_pixbuf_loader_close(pixbuf_loader, nullptr); + GdkPixbuf* pixbuf = gdk_pixbuf_loader_get_pixbuf(pixbuf_loader); + if (pixbuf) + g_object_ref(pixbuf); + g_object_unref(pixbuf_loader); + return pixbuf; + } + + std::shared_ptr get_icon_stream_by_name_theme_lang(const OUString& rIconName, const OUString& rIconTheme, const OUString& rUILang) + { + return ImageTree::get().getImageStream(rIconName, rIconTheme, rUILang); + } + + GdkPixbuf* load_icon_by_name_theme_lang(const OUString& rIconName, const OUString& rIconTheme, const OUString& rUILang) + { + auto xMemStm = get_icon_stream_by_name_theme_lang(rIconName, rIconTheme, rUILang); + if (!xMemStm) + return nullptr; + return load_icon_from_stream(*xMemStm); + } + + std::unique_ptr get_icon_stream_as_file_by_name_theme_lang(const OUString& rIconName, const OUString& rIconTheme, const OUString& rUILang) + { + uno::Reference xInputStream = ImageTree::get().getImageXInputStream(rIconName, rIconTheme, rUILang); + if (!xInputStream) + return nullptr; + + std::unique_ptr xRet(new utl::TempFileNamed); + xRet->EnableKillingFile(true); + SvStream* pStream = xRet->GetStream(StreamMode::WRITE); + + for (;;) + { + const sal_Int32 nSize(2048); + uno::Sequence aData(nSize); + sal_Int32 nRead = xInputStream->readBytes(aData, nSize); + pStream->WriteBytes(aData.getConstArray(), nRead); + if (nRead < nSize) + break; + } + xRet->CloseStream(); + + return xRet; + } + + std::unique_ptr get_icon_stream_as_file(const OUString& rIconName) + { + OUString sIconTheme = Application::GetSettings().GetStyleSettings().DetermineIconTheme(); + OUString sUILang = Application::GetSettings().GetUILanguageTag().getBcp47(); + return get_icon_stream_as_file_by_name_theme_lang(rIconName, sIconTheme, sUILang); + } +} + +GdkPixbuf* load_icon_by_name(const OUString& rIconName) +{ + OUString sIconTheme = Application::GetSettings().GetStyleSettings().DetermineIconTheme(); + OUString sUILang = Application::GetSettings().GetUILanguageTag().getBcp47(); + return load_icon_by_name_theme_lang(rIconName, sIconTheme, sUILang); +} + +namespace +{ + Image mirrorImage(const Image& rImage) + { + BitmapEx aMirrBitmapEx(rImage.GetBitmapEx()); + aMirrBitmapEx.Mirror(BmpMirrorFlags::Horizontal); + return Image(aMirrBitmapEx); + } + + GdkPixbuf* getPixbuf(const css::uno::Reference& rImage, bool bMirror = false) + { + Image aImage(rImage); + if (bMirror) + aImage = mirrorImage(aImage); + + OUString sStock(aImage.GetStock()); + if (!sStock.isEmpty()) + return load_icon_by_name(sStock); + + SvMemoryStream aMemStm; + + // We "know" that this gets passed to zlib's deflateInit2_(). 1 means best speed. + css::uno::Sequence aFilterData{ comphelper::makePropertyValue( + "Compression", sal_Int32(1)) }; + auto aBitmapEx = aImage.GetBitmapEx(); + vcl::PngImageWriter aWriter(aMemStm); + aWriter.setParameters(aFilterData); + aWriter.write(aBitmapEx); + + return load_icon_from_stream(aMemStm); + } + + // tdf#151898 as far as I can see only gtk_image_new_from_file (or gtk_image_new_from_resource) can support the use of a + // scalable input format to create a hidpi GtkImage, rather than an upscaled lodpi one so forced to go via a file here + std::unique_ptr getImageFile(const css::uno::Reference& rImage, bool bMirror = false) + { + Image aImage(rImage); + if (bMirror) + aImage = mirrorImage(aImage); + + OUString sStock(aImage.GetStock()); + if (!sStock.isEmpty()) + return get_icon_stream_as_file(sStock); + + std::unique_ptr xRet(new utl::TempFileNamed); + xRet->EnableKillingFile(true); + SvStream* pStream = xRet->GetStream(StreamMode::WRITE); + + // We "know" that this gets passed to zlib's deflateInit2_(). 1 means best speed. + css::uno::Sequence aFilterData{ comphelper::makePropertyValue( + "Compression", sal_Int32(1)) }; + auto aBitmapEx = aImage.GetBitmapEx(); + vcl::PngImageWriter aWriter(*pStream); + aWriter.setParameters(aFilterData); + aWriter.write(aBitmapEx); + + xRet->CloseStream(); + return xRet; + } + + GdkPixbuf* getPixbuf(const VirtualDevice& rDevice) + { + Size aSize(rDevice.GetOutputSizePixel()); + cairo_surface_t* orig_surface = get_underlying_cairo_surface(rDevice); + double fXScale, fYScale; + dl_cairo_surface_get_device_scale(orig_surface, &fXScale, &fYScale); + + cairo_surface_t* surface; + if (fXScale != 1.0 || fYScale != -1) + { + surface = cairo_surface_create_similar_image(orig_surface, + CAIRO_FORMAT_ARGB32, + aSize.Width(), + aSize.Height()); + cairo_t* cr = cairo_create(surface); + cairo_set_source_surface(cr, orig_surface, 0, 0); + cairo_paint(cr); + cairo_destroy(cr); + } + else + surface = orig_surface; + + GdkPixbuf* pRet = gdk_pixbuf_get_from_surface(surface, 0, 0, aSize.Width(), aSize.Height()); + + if (surface != orig_surface) + cairo_surface_destroy(surface); + + return pRet; + } + +#if GTK_CHECK_VERSION(4, 0, 0) + cairo_surface_t* render_paintable_to_surface(GdkPaintable *paintable, int nWidth, int nHeight) + { + cairo_surface_t* surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, nWidth, nHeight); + + GtkSnapshot* snapshot = gtk_snapshot_new(); + gdk_paintable_snapshot(paintable, snapshot, nWidth, nHeight); + GskRenderNode* node = gtk_snapshot_free_to_node(snapshot); + + cairo_t* cr = cairo_create(surface); + gsk_render_node_draw(node, cr); + cairo_destroy(cr); + + gsk_render_node_unref(node); + + return surface; + } +#endif + + GdkPixbuf* getPixbuf(const OUString& rIconName) + { + if (rIconName.isEmpty()) + return nullptr; + + GdkPixbuf* pixbuf = nullptr; + if (rIconName.lastIndexOf('.') != rIconName.getLength() - 4) + { + assert((rIconName== "dialog-warning" || rIconName== "dialog-error" || rIconName== "dialog-information") && + "unknown stock image"); + +#if GTK_CHECK_VERSION(4, 0, 0) + GtkIconTheme *icon_theme = gtk_icon_theme_get_for_display(gdk_display_get_default()); + GtkIconPaintable *icon = gtk_icon_theme_lookup_icon(icon_theme, + OUStringToOString(rIconName, RTL_TEXTENCODING_UTF8).getStr(), + nullptr, + 16, + 1, + AllSettings::GetLayoutRTL() ? GTK_TEXT_DIR_RTL : GTK_TEXT_DIR_LTR, + static_cast(0)); + GdkPaintable* paintable = GDK_PAINTABLE(icon); + int nWidth = gdk_paintable_get_intrinsic_width(paintable); + int nHeight = gdk_paintable_get_intrinsic_height(paintable); + cairo_surface_t* surface = render_paintable_to_surface(paintable, nWidth, nHeight); + pixbuf = gdk_pixbuf_get_from_surface(surface, 0, 0, nWidth, nHeight); + cairo_surface_destroy(surface); +#else + GtkIconTheme *icon_theme = gtk_icon_theme_get_default(); + GError *error = nullptr; + pixbuf = gtk_icon_theme_load_icon(icon_theme, OUStringToOString(rIconName, RTL_TEXTENCODING_UTF8).getStr(), + 16, GTK_ICON_LOOKUP_USE_BUILTIN, &error); +#endif + } + else + { + const AllSettings& rSettings = Application::GetSettings(); + pixbuf = load_icon_by_name_theme_lang(rIconName, + rSettings.GetStyleSettings().DetermineIconTheme(), + rSettings.GetUILanguageTag().getBcp47()); + } + return pixbuf; + } +} + +namespace +{ +#if GTK_CHECK_VERSION(4, 0, 0) + SurfacePaintable* paintable_new_from_virtual_device(const VirtualDevice& rImageSurface) + { + cairo_surface_t* surface = get_underlying_cairo_surface(rImageSurface); + + Size aSize(rImageSurface.GetOutputSizePixel()); + cairo_surface_t* target = cairo_surface_create_similar(surface, + cairo_surface_get_content(surface), + aSize.Width(), + aSize.Height()); + cairo_t* cr = cairo_create(target); + cairo_set_source_surface(cr, surface, 0, 0); + cairo_paint(cr); + cairo_destroy(cr); + + SurfacePaintable* pPaintable = SURFACE_PAINTABLE(g_object_new(surface_paintable_get_type(), nullptr)); + surface_paintable_set_source(pPaintable, target, aSize.Width(), aSize.Height()); + return pPaintable; + } + + GtkWidget* image_new_from_virtual_device(const VirtualDevice& rImageSurface) + { + SurfacePaintable* paintable = paintable_new_from_virtual_device(rImageSurface); + return gtk_image_new_from_paintable(GDK_PAINTABLE(paintable)); + } + + GtkWidget* picture_new_from_virtual_device(const VirtualDevice& rImageSurface) + { + SurfacePaintable* paintable = paintable_new_from_virtual_device(rImageSurface); + return gtk_picture_new_for_paintable(GDK_PAINTABLE(paintable)); + } + +#else + GtkWidget* image_new_from_virtual_device(const VirtualDevice& rImageSurface) + { + GtkWidget* pImage = nullptr; + cairo_surface_t* surface = get_underlying_cairo_surface(rImageSurface); + + Size aSize(rImageSurface.GetOutputSizePixel()); + cairo_surface_t* target = cairo_surface_create_similar(surface, + cairo_surface_get_content(surface), + aSize.Width(), + aSize.Height()); + cairo_t* cr = cairo_create(target); + cairo_set_source_surface(cr, surface, 0, 0); + cairo_paint(cr); + cairo_destroy(cr); + + pImage = gtk_image_new_from_surface(target); + cairo_surface_destroy(target); + return pImage; + } +#endif + + GtkWidget* image_new_from_xgraphic(const css::uno::Reference& rIcon, bool bMirror) + { + GtkWidget* pImage = nullptr; + if (auto xTempFile = getImageFile(rIcon, bMirror)) + pImage = gtk_image_new_from_file(OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + return pImage; + } + + GtkWidget* image_new_from_icon_name(const OUString& rIconName) + { + GtkWidget* pImage = nullptr; + if (auto xTempFile = get_icon_stream_as_file(rIconName)) + pImage = gtk_image_new_from_file(OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + return pImage; + } + + GtkWidget* image_new_from_icon_name_theme_lang(const OUString& rIconName, const OUString& rIconTheme, const OUString& rUILang) + { + GtkWidget* pImage = nullptr; + if (auto xTempFile = get_icon_stream_as_file_by_name_theme_lang(rIconName, rIconTheme, rUILang)) + pImage = gtk_image_new_from_file(OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + return pImage; + } + + void image_set_from_icon_name(GtkImage* pImage, const OUString& rIconName) + { + if (auto xTempFile = get_icon_stream_as_file(rIconName)) + gtk_image_set_from_file(pImage, OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + else + gtk_image_set_from_pixbuf(pImage, nullptr); + } + + void image_set_from_icon_name_theme_lang(GtkImage* pImage, const OUString& rIconName, const OUString& rIconTheme, const OUString& rUILang) + { + if (auto xTempFile = get_icon_stream_as_file_by_name_theme_lang(rIconName, rIconTheme, rUILang)) + gtk_image_set_from_file(pImage, OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + else + gtk_image_set_from_pixbuf(pImage, nullptr); + } + + void image_set_from_virtual_device(GtkImage* pImage, const VirtualDevice* pDevice) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_image_set_from_paintable(pImage, pDevice ? GDK_PAINTABLE(paintable_new_from_virtual_device(*pDevice)) : nullptr); +#else + gtk_image_set_from_surface(pImage, pDevice ? get_underlying_cairo_surface(*pDevice) : nullptr); +#endif + } + + void image_set_from_xgraphic(GtkImage* pImage, const css::uno::Reference& rImage) + { + if (auto xTempFile = getImageFile(rImage, false)) + gtk_image_set_from_file(pImage, OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + else + gtk_image_set_from_pixbuf(pImage, nullptr); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + void picture_set_from_icon_name(GtkPicture* pPicture, const OUString& rIconName) + { + if (auto xTempFile = get_icon_stream_as_file(rIconName)) + gtk_picture_set_filename(pPicture, OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + else + gtk_picture_set_pixbuf(pPicture, nullptr); + } + + void picture_set_from_icon_name_theme_lang(GtkPicture* pPicture, const OUString& rIconName, const OUString& rIconTheme, const OUString& rUILang) + { + if (auto xTempFile = get_icon_stream_as_file_by_name_theme_lang(rIconName, rIconTheme, rUILang)) + gtk_picture_set_filename(pPicture, OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + else + gtk_picture_set_pixbuf(pPicture, nullptr); + } + + void picture_set_from_virtual_device(GtkPicture* pPicture, const VirtualDevice* pDevice) + { + if (!pDevice) + gtk_picture_set_paintable(pPicture, nullptr); + else + gtk_picture_set_paintable(pPicture, GDK_PAINTABLE(paintable_new_from_virtual_device(*pDevice))); + } + + void picture_set_from_xgraphic(GtkPicture* pPicture, const css::uno::Reference& rPicture) + { + if (auto xTempFile = getImageFile(rPicture, false)) + gtk_picture_set_filename(pPicture, OUStringToOString(xTempFile->GetFileName(), osl_getThreadTextEncoding()).getStr()); + else + gtk_picture_set_pixbuf(pPicture, nullptr); + } +#endif + + void button_set_from_icon_name(GtkButton* pButton, const OUString& rIconName) + { + if (GtkImage* pImage = get_image_widget(GTK_WIDGET(pButton))) + { + ::image_set_from_icon_name(pImage, rIconName); + gtk_widget_set_visible(GTK_WIDGET(pImage), true); + return; + } + + GtkWidget* pImage = image_new_from_icon_name(rIconName); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_button_set_child(pButton, pImage); +#else + gtk_button_set_image(pButton, pImage); +#endif + } + + void button_set_image(GtkButton* pButton, const VirtualDevice* pDevice) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_button_set_always_show_image(pButton, true); + gtk_button_set_image_position(pButton, GTK_POS_LEFT); +#endif + GtkWidget* pImage = pDevice ? image_new_from_virtual_device(*pDevice) : nullptr; +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_button_set_child(pButton, pImage); +#else + gtk_button_set_image(pButton, pImage); +#endif + } + + void button_set_image(GtkButton* pButton, const css::uno::Reference& rImage) + { + if (GtkImage* pImage = get_image_widget(GTK_WIDGET(pButton))) + { + ::image_set_from_xgraphic(pImage, rImage); + gtk_widget_set_visible(GTK_WIDGET(pImage), true); + return; + } + + GtkWidget* pImage = image_new_from_xgraphic(rImage, false); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_button_set_child(pButton, pImage); +#else + gtk_button_set_image(pButton, pImage); +#endif + } + + +class MenuHelper +{ +protected: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkMenu* m_pMenu; + + std::map m_aMap; +#else + GtkPopoverMenu* m_pMenu; + + o3tl::sorted_vector m_aInsertedActions; // must outlive m_aActionEntries + std::map m_aIdToAction; + std::set m_aHiddenIds; + std::vector m_aActionEntries; + GActionGroup* m_pActionGroup; + // move 'invisible' entries to m_pHiddenActionGroup + GActionGroup* m_pHiddenActionGroup; +#endif + bool m_bTakeOwnership; +private: + + virtual void signal_item_activate(const OUString& rIdent) = 0; + +#if !GTK_CHECK_VERSION(4, 0, 0) + static void collect(GtkWidget* pItem, gpointer widget) + { + GtkMenuItem* pMenuItem = GTK_MENU_ITEM(pItem); + if (GtkWidget* pSubMenu = gtk_menu_item_get_submenu(pMenuItem)) + gtk_container_foreach(GTK_CONTAINER(pSubMenu), collect, widget); + MenuHelper* pThis = static_cast(widget); + pThis->add_to_map(pMenuItem); + } + + static void signalActivate(GtkMenuItem* pItem, gpointer widget) + { + MenuHelper* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_item_activate(::get_buildable_id(GTK_BUILDABLE(pItem))); + } +#else + static std::pair find_id(GMenuModel* pMenuModel, const OUString& rId) + { + for (int i = 0, nCount = g_menu_model_get_n_items(pMenuModel); i < nCount; ++i) + { + OUString sTarget; + char *id; + if (g_menu_model_get_item_attribute(pMenuModel, i, "target", "s", &id)) + { + sTarget = OStringToOUString(id, RTL_TEXTENCODING_UTF8); + g_free(id); + } + + if (sTarget == rId) + return std::make_pair(pMenuModel, i); + + if (GMenuModel* pSectionModel = g_menu_model_get_item_link(pMenuModel, i, G_MENU_LINK_SECTION)) + { + std::pair aRet = find_id(pSectionModel, rId); + if (aRet.first) + return aRet; + } + if (GMenuModel* pSubMenuModel = g_menu_model_get_item_link(pMenuModel, i, G_MENU_LINK_SUBMENU)) + { + std::pair aRet = find_id(pSubMenuModel, rId); + if (aRet.first) + return aRet; + } + } + + return std::make_pair(nullptr, -1); + } + + void clear_actions() + { + for (const auto& rAction : m_aActionEntries) + { + g_action_map_remove_action(G_ACTION_MAP(m_pActionGroup), rAction.name); + g_action_map_remove_action(G_ACTION_MAP(m_pHiddenActionGroup), rAction.name); + } + m_aActionEntries.clear(); + m_aInsertedActions.clear(); + m_aIdToAction.clear(); + } + + static void action_activated(GSimpleAction*, GVariant* pParameter, gpointer widget) + { + gsize nLength(0); + const gchar* pStr = g_variant_get_string(pParameter, &nLength); + OUString aStr(pStr, nLength, RTL_TEXTENCODING_UTF8); + MenuHelper* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_item_activate(aStr); + } +#endif + +public: +#if !GTK_CHECK_VERSION(4, 0, 0) + MenuHelper(GtkMenu* pMenu, bool bTakeOwnership) +#else + MenuHelper(GtkPopoverMenu* pMenu, bool bTakeOwnership) +#endif + : m_pMenu(pMenu) + , m_bTakeOwnership(bTakeOwnership) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!m_pMenu) + return; + gtk_container_foreach(GTK_CONTAINER(m_pMenu), collect, this); +#else + m_pActionGroup = G_ACTION_GROUP(g_simple_action_group_new()); + m_pHiddenActionGroup = G_ACTION_GROUP(g_simple_action_group_new()); +#endif + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + void add_to_map(GtkMenuItem* pMenuItem) + { + OUString id = ::get_buildable_id(GTK_BUILDABLE(pMenuItem)); + m_aMap[id] = pMenuItem; + g_signal_connect(pMenuItem, "activate", G_CALLBACK(signalActivate), this); + } + + void remove_from_map(GtkMenuItem* pMenuItem) + { + OUString id = ::get_buildable_id(GTK_BUILDABLE(pMenuItem)); + auto iter = m_aMap.find(id); + g_signal_handlers_disconnect_by_data(pMenuItem, this); + m_aMap.erase(iter); + } + + void disable_item_notify_events() + { + for (auto& a : m_aMap) + g_signal_handlers_block_by_func(a.second, reinterpret_cast(signalActivate), this); + } + + void enable_item_notify_events() + { + for (auto& a : m_aMap) + g_signal_handlers_unblock_by_func(a.second, reinterpret_cast(signalActivate), this); + } +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + /* LibreOffice likes to think of separators between menu entries, while gtk likes + to think of sections of menus with separators drawn between sections. We always + arrange to have a section in a menu so toplevel menumodels comprise of + sections and we move entries between sections on pretending to insert separators */ + static std::pair get_section_and_pos_for(GMenuModel* pMenuModel, int pos) + { + int nSectionCount = g_menu_model_get_n_items(pMenuModel); + assert(nSectionCount); + + GMenuModel* pSectionModel = nullptr; + int nIndexWithinSection = 0; + + int nExternalPos = 0; + for (int nSection = 0; nSection < nSectionCount; ++nSection) + { + pSectionModel = g_menu_model_get_item_link(pMenuModel, nSection, G_MENU_LINK_SECTION); + assert(pSectionModel); + int nCount = g_menu_model_get_n_items(pSectionModel); + for (nIndexWithinSection = 0; nIndexWithinSection < nCount; ++nIndexWithinSection) + { + if (pos == nExternalPos) + break; + ++nExternalPos; + } + ++nExternalPos; + } + + return std::make_pair(pSectionModel, nIndexWithinSection); + } + + static int count_immediate_children(GMenuModel* pMenuModel) + { + int nSectionCount = g_menu_model_get_n_items(pMenuModel); + assert(nSectionCount); + + int nExternalPos = 0; + for (int nSection = 0; nSection < nSectionCount; ++nSection) + { + GMenuModel* pSectionModel = g_menu_model_get_item_link(pMenuModel, nSection, G_MENU_LINK_SECTION); + assert(pSectionModel); + int nCount = g_menu_model_get_n_items(pSectionModel); + for (int nIndexWithinSection = 0; nIndexWithinSection < nCount; ++nIndexWithinSection) + { + ++nExternalPos; + } + ++nExternalPos; + } + + return nExternalPos - 1; + } +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + void process_menu_model(GMenuModel* pMenuModel) + { + for (int i = 0, nCount = g_menu_model_get_n_items(pMenuModel); i < nCount; ++i) + { + OString sAction; + OUString sTarget; + char *id; + if (g_menu_model_get_item_attribute(pMenuModel, i, "action", "s", &id)) + { + assert(o3tl::starts_with(id, "menu.")); + + sAction = OString(id + 5); + + auto res = m_aInsertedActions.insert(sAction); + if (res.second) + { + // the const char* arg isn't copied by anything so it must continue to exist for the life time of + // the action group + if (sAction.startsWith("radio.")) + m_aActionEntries.push_back({res.first->getStr(), action_activated, "s", "'none'", nullptr, {}}); + else + m_aActionEntries.push_back({res.first->getStr(), action_activated, "s", nullptr, nullptr, {}}); + } + + g_free(id); + } + + if (g_menu_model_get_item_attribute(pMenuModel, i, "target", "s", &id)) + { + sTarget = OStringToOUString(id, RTL_TEXTENCODING_UTF8); + g_free(id); + } + + m_aIdToAction[sTarget] = sAction; + + if (GMenuModel* pSectionModel = g_menu_model_get_item_link(pMenuModel, i, G_MENU_LINK_SECTION)) + process_menu_model(pSectionModel); + if (GMenuModel* pSubMenuModel = g_menu_model_get_item_link(pMenuModel, i, G_MENU_LINK_SUBMENU)) + process_menu_model(pSubMenuModel); + } + } + + // build an action group for the menu, "action" is the normal menu entry case + // the others are radiogroups + void update_action_group_from_popover_model() + { + clear_actions(); + + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + process_menu_model(pMenuModel); + } + + // move hidden entries to m_pHiddenActionGroup + g_action_map_add_action_entries(G_ACTION_MAP(m_pActionGroup), m_aActionEntries.data(), m_aActionEntries.size(), this); + for (const auto& id : m_aHiddenIds) + { + GAction* pAction = g_action_map_lookup_action(G_ACTION_MAP(m_pActionGroup), m_aIdToAction[id].getStr()); + g_action_map_add_action(G_ACTION_MAP(m_pHiddenActionGroup), pAction); + g_action_map_remove_action(G_ACTION_MAP(m_pActionGroup), m_aIdToAction[id].getStr()); + } + } +#endif + + void insert_item(int pos, const OUString& rId, const OUString& rStr, + const OUString* pIconName, const VirtualDevice* pImageSurface, + TriState eCheckRadioFalse) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pImage = nullptr; + if (pIconName && !pIconName->isEmpty()) + pImage = image_new_from_icon_name(*pIconName); + else if (pImageSurface) + pImage = image_new_from_virtual_device(*pImageSurface); + + GtkWidget *pItem; + if (pImage) + { + GtkBox *pBox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6)); + GtkWidget *pLabel = gtk_label_new_with_mnemonic(MapToGtkAccelerator(rStr).getStr()); + pItem = eCheckRadioFalse != TRISTATE_INDET ? gtk_check_menu_item_new() : gtk_menu_item_new(); + gtk_box_pack_start(pBox, pImage, true, true, 0); + gtk_box_pack_start(pBox, pLabel, true, true, 0); + gtk_container_add(GTK_CONTAINER(pItem), GTK_WIDGET(pBox)); + gtk_widget_show_all(pItem); + } + else + { + pItem = eCheckRadioFalse != TRISTATE_INDET ? gtk_check_menu_item_new_with_mnemonic(MapToGtkAccelerator(rStr).getStr()) + : gtk_menu_item_new_with_mnemonic(MapToGtkAccelerator(rStr).getStr()); + } + + if (eCheckRadioFalse == TRISTATE_FALSE) + gtk_check_menu_item_set_draw_as_radio(GTK_CHECK_MENU_ITEM(pItem), true); + + ::set_buildable_id(GTK_BUILDABLE(pItem), rId); + gtk_menu_shell_append(GTK_MENU_SHELL(m_pMenu), pItem); + gtk_widget_show(pItem); + add_to_map(GTK_MENU_ITEM(pItem)); + if (pos != -1) + gtk_menu_reorder_child(m_pMenu, pItem, pos); +#else + (void)pIconName; (void)pImageSurface; + + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + auto aSectionAndPos = get_section_and_pos_for(pMenuModel, pos); + GMenu* pMenu = G_MENU(aSectionAndPos.first); + // action with a target value ... the action name and target value are separated by a double + // colon ... For example: "app.action::target" + OUString sActionAndTarget; + if (eCheckRadioFalse == TRISTATE_INDET) + sActionAndTarget = "menu.normal." + rId + "::" + rId; + else + sActionAndTarget = "menu.radio." + rId + "::" + rId; + g_menu_insert(pMenu, aSectionAndPos.second, MapToGtkAccelerator(rStr).getStr(), sActionAndTarget.toUtf8().getStr()); + + assert(eCheckRadioFalse == TRISTATE_INDET); // come back to this later + + // TODO not redo entire group + update_action_group_from_popover_model(); + } +#endif + } + + void insert_separator(int pos, const OUString& rId) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pItem = gtk_separator_menu_item_new(); + ::set_buildable_id(GTK_BUILDABLE(pItem), rId); + gtk_menu_shell_append(GTK_MENU_SHELL(m_pMenu), pItem); + gtk_widget_show(pItem); + add_to_map(GTK_MENU_ITEM(pItem)); + if (pos != -1) + gtk_menu_reorder_child(m_pMenu, pItem, pos); +#else + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + auto aSectionAndPos = get_section_and_pos_for(pMenuModel, pos); + + for (int nSection = 0, nSectionCount = g_menu_model_get_n_items(pMenuModel); nSection < nSectionCount; ++nSection) + { + GMenuModel* pSectionModel = g_menu_model_get_item_link(pMenuModel, nSection, G_MENU_LINK_SECTION); + assert(pSectionModel); + if (aSectionAndPos.first == pSectionModel) + { + GMenu* pNewSection = g_menu_new(); + GMenuItem* pSectionItem = g_menu_item_new_section(nullptr, G_MENU_MODEL(pNewSection)); + OUString sActionAndTarget = "menu.separator." + rId + "::" + rId; + g_menu_item_set_detailed_action(pSectionItem, sActionAndTarget.toUtf8().getStr()); + g_menu_insert_item(G_MENU(pMenuModel), nSection + 1, pSectionItem); + int nOldSectionCount = g_menu_model_get_n_items(pSectionModel); + for (int i = nOldSectionCount - 1; i >= aSectionAndPos.second; --i) + { + GMenuItem* pMenuItem = g_menu_item_new_from_model(pSectionModel, i); + g_menu_prepend_item(pNewSection, pMenuItem); + g_menu_remove(G_MENU(pSectionModel), i); + g_object_unref(pMenuItem); + } + g_object_unref(pSectionItem); + g_object_unref(pNewSection); + } + } + } + +#endif + } + + void remove_item(const OUString& rIdent) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkMenuItem* pMenuItem = m_aMap[rIdent]; + remove_from_map(pMenuItem); + gtk_widget_destroy(GTK_WIDGET(pMenuItem)); +#else + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + std::pair aRes = find_id(pMenuModel, rIdent); + if (!aRes.first) + return; + g_menu_remove(G_MENU(aRes.first), aRes.second); + } +#endif + } + + void set_item_sensitive(const OUString& rIdent, bool bSensitive) + { +#if GTK_CHECK_VERSION(4, 0, 0) + GActionGroup* pActionGroup = m_aHiddenIds.find(rIdent) == m_aHiddenIds.end() ? m_pActionGroup : m_pHiddenActionGroup; + GAction* pAction = g_action_map_lookup_action(G_ACTION_MAP(pActionGroup), m_aIdToAction[rIdent].getStr()); + g_simple_action_set_enabled(G_SIMPLE_ACTION(pAction), bSensitive); +#else + gtk_widget_set_sensitive(GTK_WIDGET(m_aMap[rIdent]), bSensitive); +#endif + } + + bool get_item_sensitive(const OUString& rIdent) const + { +#if GTK_CHECK_VERSION(4, 0, 0) + GActionGroup* pActionGroup = m_aHiddenIds.find(rIdent) == m_aHiddenIds.end() ? m_pActionGroup : m_pHiddenActionGroup; + GAction* pAction = g_action_map_lookup_action(G_ACTION_MAP(pActionGroup), m_aIdToAction.find(rIdent)->second.getStr()); + return g_action_get_enabled(pAction); +#else + return gtk_widget_get_sensitive(GTK_WIDGET(m_aMap.find(rIdent)->second)); +#endif + } + + void set_item_active(const OUString& rIdent, bool bActive) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + disable_item_notify_events(); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(m_aMap[rIdent]), bActive); + enable_item_notify_events(); +#else + GActionGroup* pActionGroup = m_aHiddenIds.find(rIdent) == m_aHiddenIds.end() ? m_pActionGroup : m_pHiddenActionGroup; + g_action_group_change_action_state(pActionGroup, m_aIdToAction[rIdent].getStr(), + g_variant_new_string(bActive ? OUStringToOString(rIdent, RTL_TEXTENCODING_UTF8).getStr() : "'none'")); +#endif + } + + bool get_item_active(const OUString& rIdent) const + { +#if !GTK_CHECK_VERSION(4, 0, 0) + return gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(m_aMap.find(rIdent)->second)); +#else + GActionGroup* pActionGroup = m_aHiddenIds.find(rIdent) == m_aHiddenIds.end() ? m_pActionGroup : m_pHiddenActionGroup; + GVariant* pState = g_action_group_get_action_state(pActionGroup, m_aIdToAction.find(rIdent)->second.getStr()); + if (!pState) + return false; + const char *pStateString = g_variant_get_string(pState, nullptr); + bool bInactive = g_strcmp0(pStateString, "'none'") == 0; + g_variant_unref(pState); + return bInactive; +#endif + } + + void set_item_label(const OUString& rIdent, const OUString& rText) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_menu_item_set_label(m_aMap[rIdent], MapToGtkAccelerator(rText).getStr()); +#else + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + std::pair aRes = find_id(pMenuModel, rIdent); + if (!aRes.first) + return; + // clone the original item, remove the original, insert the replacement at + // the original location + GMenuItem* pMenuItem = g_menu_item_new_from_model(aRes.first, aRes.second); + g_menu_remove(G_MENU(aRes.first), aRes.second); + g_menu_item_set_label(pMenuItem, MapToGtkAccelerator(rText).getStr()); + g_menu_insert_item(G_MENU(aRes.first), aRes.second, pMenuItem); + g_object_unref(pMenuItem); + } +#endif + } + + OUString get_item_label(const OUString& rIdent) const + { +#if !GTK_CHECK_VERSION(4, 0, 0) + const gchar* pText = gtk_menu_item_get_label(m_aMap.find(rIdent)->second); + return OUString(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); +#else + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + std::pair aRes = find_id(pMenuModel, rIdent); + if (!aRes.first) + return OUString(); + + // clone the original item to query its label + GMenuItem* pMenuItem = g_menu_item_new_from_model(aRes.first, aRes.second); + char *pLabel = nullptr; + g_menu_item_get_attribute(pMenuItem, G_MENU_ATTRIBUTE_LABEL, "&s", &pLabel); + OUString aRet(pLabel, pLabel ? strlen(pLabel) : 0, RTL_TEXTENCODING_UTF8); + g_free(pLabel); + g_object_unref(pMenuItem); + return aRet; + } + return OUString(); +#endif + } + + void set_item_visible(const OUString& rIdent, bool bShow) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pWidget = GTK_WIDGET(m_aMap[rIdent]); + if (bShow) + gtk_widget_show(pWidget); + else + gtk_widget_hide(pWidget); +#else + bool bOldVisible = m_aHiddenIds.find(rIdent) == m_aHiddenIds.end(); + if (bShow == bOldVisible) + return; + + if (!bShow) + { + GAction* pAction = g_action_map_lookup_action(G_ACTION_MAP(m_pActionGroup), m_aIdToAction[rIdent].getStr()); + g_action_map_add_action(G_ACTION_MAP(m_pHiddenActionGroup), pAction); + g_action_map_remove_action(G_ACTION_MAP(m_pActionGroup), m_aIdToAction[rIdent].getStr()); + m_aHiddenIds.insert(rIdent); + } + else + { + GAction* pAction = g_action_map_lookup_action(G_ACTION_MAP(m_pHiddenActionGroup), m_aIdToAction[rIdent].getStr()); + g_action_map_add_action(G_ACTION_MAP(m_pActionGroup), pAction); + g_action_map_remove_action(G_ACTION_MAP(m_pHiddenActionGroup), m_aIdToAction[rIdent].getStr()); + m_aHiddenIds.erase(rIdent); + } +#endif + } + + OUString get_item_id(int pos) const + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(m_pMenu)); + gpointer pMenuItem = g_list_nth_data(pChildren, pos); + OUString id = ::get_buildable_id(GTK_BUILDABLE(pMenuItem)); + g_list_free(pChildren); + return id; +#else + OUString sTarget; + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + auto aSectionAndPos = get_section_and_pos_for(pMenuModel, pos); + char *id; + if (g_menu_model_get_item_attribute(aSectionAndPos.first, aSectionAndPos.second, "target", "s", &id)) + { + sTarget = OStringToOUString(id, RTL_TEXTENCODING_UTF8); + g_free(id); + } + } + return sTarget; +#endif + } + + int get_n_children() const + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(m_pMenu)); + int nLen = g_list_length(pChildren); + g_list_free(pChildren); + return nLen; +#else + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + return count_immediate_children(pMenuModel); + return 0; +#endif + } + + void clear_items() + { +#if !GTK_CHECK_VERSION(4, 0, 0) + for (const auto& a : m_aMap) + { + GtkMenuItem* pMenuItem = a.second; + g_signal_handlers_disconnect_by_data(pMenuItem, this); + gtk_widget_destroy(GTK_WIDGET(pMenuItem)); + } + m_aMap.clear(); +#else + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + GMenu* pMenu = G_MENU(pMenuModel); + g_menu_remove_all(pMenu); + g_menu_insert_section(pMenu, 0, nullptr, G_MENU_MODEL(g_menu_new())); + m_aHiddenIds.clear(); + update_action_group_from_popover_model(); + } +#endif + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkMenu* getMenu() const +#else + GtkPopoverMenu* getMenu() const +#endif + { + return m_pMenu; + } + + virtual ~MenuHelper() + { +#if !GTK_CHECK_VERSION(4, 0, 0) + for (auto& a : m_aMap) + g_signal_handlers_disconnect_by_data(a.second, this); + if (m_bTakeOwnership) + gtk_widget_destroy(GTK_WIDGET(m_pMenu)); +#else + g_object_unref(m_pActionGroup); + g_object_unref(m_pHiddenActionGroup); +#endif + } +}; + +class GtkInstanceSizeGroup : public weld::SizeGroup +{ +private: + GtkSizeGroup* m_pGroup; +public: + GtkInstanceSizeGroup() + : m_pGroup(gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL)) + { + } + virtual void add_widget(weld::Widget* pWidget) override + { + GtkInstanceWidget* pVclWidget = dynamic_cast(pWidget); + assert(pVclWidget); + gtk_size_group_add_widget(m_pGroup, pVclWidget->getWidget()); + } + virtual void set_mode(VclSizeGroupMode eVclMode) override + { + GtkSizeGroupMode eGtkMode(GTK_SIZE_GROUP_NONE); + switch (eVclMode) + { + case VclSizeGroupMode::NONE: + eGtkMode = GTK_SIZE_GROUP_NONE; + break; + case VclSizeGroupMode::Horizontal: + eGtkMode = GTK_SIZE_GROUP_HORIZONTAL; + break; + case VclSizeGroupMode::Vertical: + eGtkMode = GTK_SIZE_GROUP_VERTICAL; + break; + case VclSizeGroupMode::Both: + eGtkMode = GTK_SIZE_GROUP_BOTH; + break; + } + gtk_size_group_set_mode(m_pGroup, eGtkMode); + } + virtual ~GtkInstanceSizeGroup() override + { + g_object_unref(m_pGroup); + } +}; + +class ChildFrame : public WorkWindow +{ +private: + Idle maLayoutIdle; + + DECL_LINK(ImplHandleLayoutTimerHdl, Timer*, void); +public: + ChildFrame(vcl::Window* pParent, WinBits nStyle) + : WorkWindow(pParent, nStyle) + , maLayoutIdle( "ChildFrame maLayoutIdle" ) + { + maLayoutIdle.SetPriority(TaskPriority::RESIZE); + maLayoutIdle.SetInvokeHandler( LINK( this, ChildFrame, ImplHandleLayoutTimerHdl ) ); + } + + virtual void dispose() override + { + maLayoutIdle.Stop(); + WorkWindow::dispose(); + } + + virtual void queue_resize(StateChangedType eReason = StateChangedType::Layout) override + { + WorkWindow::queue_resize(eReason); + if (maLayoutIdle.IsActive()) + return; + maLayoutIdle.Start(); + } + + void Layout() + { + if (vcl::Window *pChild = GetWindow(GetWindowType::FirstChild)) + pChild->SetPosSizePixel(Point(0, 0), GetSizePixel()); + } + + virtual void Resize() override + { + maLayoutIdle.Stop(); + Layout(); + WorkWindow::Resize(); + } +}; + +IMPL_LINK_NOARG(ChildFrame, ImplHandleLayoutTimerHdl, Timer*, void) +{ + Layout(); +} + +class GtkInstanceContainer : public GtkInstanceWidget, public virtual weld::Container +{ +private: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkContainer* m_pContainer; +#else + GtkWidget* m_pContainer; +#endif + gulong m_nSetFocusChildSignalId; + bool m_bChildHasFocus; + + void signal_set_focus_child(bool bChildHasFocus) + { + if (m_bChildHasFocus != bChildHasFocus) + { + m_bChildHasFocus = bChildHasFocus; + signal_container_focus_changed(); + } + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static void signalSetFocusChild(GtkContainer*, GtkWidget* pChild, gpointer widget) + { + GtkInstanceContainer* pThis = static_cast(widget); + pThis->signal_set_focus_child(pChild != nullptr); + } +#endif + +public: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkInstanceContainer(GtkContainer* pContainer, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pContainer), pBuilder, bTakeOwnership) +#else + GtkInstanceContainer(GtkWidget* pContainer, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(pContainer, pBuilder, bTakeOwnership) +#endif + , m_pContainer(pContainer) + , m_nSetFocusChildSignalId(0) + , m_bChildHasFocus(false) + { + } + + virtual void connect_container_focus_changed(const Link& rLink) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!m_nSetFocusChildSignalId) + m_nSetFocusChildSignalId = g_signal_connect(G_OBJECT(m_pContainer), "set-focus-child", G_CALLBACK(signalSetFocusChild), this); +#endif + weld::Container::connect_container_focus_changed(rLink); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* getContainer() { return m_pContainer; } +#else + GtkContainer* getContainer() { return m_pContainer; } +#endif + + virtual void child_grab_focus() override + { + gtk_widget_grab_focus(m_pWidget); +#if GTK_CHECK_VERSION(4, 0, 0) + bool bHasFocusChild = gtk_widget_get_focus_child(GTK_WIDGET(m_pContainer)); +#else + bool bHasFocusChild = gtk_container_get_focus_child(m_pContainer); +#endif + if (!bHasFocusChild) + { +#if GTK_CHECK_VERSION(4, 0, 0) + if (GtkWidget* pChild = gtk_widget_get_first_child(m_pContainer)) + { + gtk_widget_set_focus_child(m_pContainer, pChild); + bHasFocusChild = true; + } +#else + GList* pChildren = gtk_container_get_children(m_pContainer); + if (GList* pChild = g_list_first(pChildren)) + { + gtk_container_set_focus_child(m_pContainer, static_cast(pChild->data)); + bHasFocusChild = true; + } + g_list_free(pChildren); +#endif + } + + if (bHasFocusChild) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_child_focus(gtk_widget_get_focus_child(m_pWidget), GTK_DIR_TAB_FORWARD); +#else + gtk_widget_child_focus(gtk_container_get_focus_child(GTK_CONTAINER(m_pWidget)), GTK_DIR_TAB_FORWARD); +#endif + } + + } + + virtual void move(weld::Widget* pWidget, weld::Container* pNewParent) override + { + GtkInstanceWidget* pGtkWidget = dynamic_cast(pWidget); + assert(pGtkWidget); + GtkWidget* pChild = pGtkWidget->getWidget(); + g_object_ref(pChild); + auto pOldContainer = getContainer(); + container_remove(GTK_WIDGET(pOldContainer), pChild); + + GtkInstanceContainer* pNewGtkParent = dynamic_cast(pNewParent); + assert(!pNewParent || pNewGtkParent); + if (pNewGtkParent) + { + auto pNewContainer = pNewGtkParent->getContainer(); + container_add(GTK_WIDGET(pNewContainer), pChild); + } + g_object_unref(pChild); + } + + virtual css::uno::Reference CreateChildFrame() override + { + // This will cause a GtkSalFrame to be created. With WB_SYSTEMCHILDWINDOW set it + // will create a toplevel GtkEventBox window + auto xEmbedWindow = VclPtr::Create(ImplGetDefaultWindow(), WB_SYSTEMCHILDWINDOW | WB_DIALOGCONTROL | WB_CHILDDLGCTRL); + SalFrame* pFrame = xEmbedWindow->ImplGetFrame(); + GtkSalFrame* pGtkFrame = dynamic_cast(pFrame); + assert(pGtkFrame); + + // relocate that toplevel GtkEventBox into this widget + GtkWidget* pWindow = pGtkFrame->getWindow(); + + GtkWidget* pParent = gtk_widget_get_parent(pWindow); + + g_object_ref(pWindow); + container_remove(pParent, pWindow); + container_add(GTK_WIDGET(m_pContainer), pWindow); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_child_set(m_pContainer, pWindow, "expand", true, "fill", true, nullptr); +#endif + gtk_widget_set_hexpand(pWindow, true); + gtk_widget_set_vexpand(pWindow, true); + gtk_widget_realize(pWindow); + gtk_widget_set_can_focus(pWindow, true); + g_object_unref(pWindow); + + // NoActivate otherwise Show grab focus to this widget + xEmbedWindow->Show(true, ShowFlags::NoActivate); + css::uno::Reference xWindow(xEmbedWindow->GetComponentInterface(), css::uno::UNO_QUERY); + return xWindow; + } + + virtual ~GtkInstanceContainer() override + { + if (m_nSetFocusChildSignalId) + g_signal_handler_disconnect(m_pContainer, m_nSetFocusChildSignalId); + } +}; + +} + +std::unique_ptr GtkInstanceWidget::weld_parent() const +{ + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (!pParent) + return nullptr; +#if !GTK_CHECK_VERSION(4, 0, 0) + return std::make_unique(GTK_CONTAINER(pParent), m_pBuilder, false); +#else + return std::make_unique(pParent, m_pBuilder, false); +#endif +} + +namespace { + +bool sortButtons(const GtkWidget* pA, const GtkWidget* pB) +{ + //order within groups according to platform rules + return getButtonPriority(get_buildable_id(GTK_BUILDABLE(pA))) < + getButtonPriority(get_buildable_id(GTK_BUILDABLE(pB))); +} + +void sort_native_button_order(GtkBox* pContainer) +{ + std::vector aChildren; +#if GTK_CHECK_VERSION(4, 0, 0) + for (GtkWidget* pChild = gtk_widget_get_first_child(GTK_WIDGET(pContainer)); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + aChildren.push_back(pChild); + } +#else + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(pContainer)); + for (GList* pChild = g_list_first(pChildren); pChild; pChild = g_list_next(pChild)) + aChildren.push_back(static_cast(pChild->data)); + g_list_free(pChildren); +#endif + + //sort child order within parent so that we match the platform button order + std::stable_sort(aChildren.begin(), aChildren.end(), sortButtons); + +#if GTK_CHECK_VERSION(4, 0, 0) + for (size_t pos = 0; pos < aChildren.size(); ++pos) + gtk_box_reorder_child_after(pContainer, aChildren[pos], pos ? aChildren[pos - 1] : nullptr); +#else + for (size_t pos = 0; pos < aChildren.size(); ++pos) + gtk_box_reorder_child(pContainer, aChildren[pos], pos); +#endif +} + +class GtkInstanceBox : public GtkInstanceContainer, public virtual weld::Box +{ +private: + GtkBox* m_pBox; + +public: + GtkInstanceBox(GtkBox* pBox, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) +#if !GTK_CHECK_VERSION(4, 0, 0) + : GtkInstanceContainer(GTK_CONTAINER(pBox), pBuilder, bTakeOwnership) +#else + : GtkInstanceContainer(GTK_WIDGET(pBox), pBuilder, bTakeOwnership) +#endif + , m_pBox(pBox) + { + } + + virtual void reorder_child(weld::Widget* pWidget, int nNewPosition) override + { + GtkInstanceWidget* pGtkWidget = dynamic_cast(pWidget); + assert(pGtkWidget); + GtkWidget* pChild = pGtkWidget->getWidget(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_box_reorder_child(m_pBox, pChild, nNewPosition); +#else + if (nNewPosition == 0) + gtk_box_reorder_child_after(m_pBox, pChild, nullptr); + else + { + int nNewSiblingPos = nNewPosition - 1; + int nChildPosition = 0; + for (GtkWidget* pNewSibling = gtk_widget_get_first_child(GTK_WIDGET(m_pBox)); + pNewSibling; pNewSibling = gtk_widget_get_next_sibling(pNewSibling)) + { + if (nChildPosition == nNewSiblingPos) + { + gtk_box_reorder_child_after(m_pBox, pChild, pNewSibling); + break; + } + ++nChildPosition; + } + } +#endif + } + + virtual void sort_native_button_order() override + { + ::sort_native_button_order(m_pBox); + } +}; + +} + +namespace +{ + Point get_csd_offset(GtkWidget* pTopLevel) + { + // try and omit drawing CSD under wayland + GtkWidget* pChild = widget_get_first_child(pTopLevel); + + gtk_coord x, y; + gtk_widget_translate_coordinates(pChild, pTopLevel, 0, 0, &x, &y); + +#if !GTK_CHECK_VERSION(4, 0, 0) + int innerborder = gtk_container_get_border_width(GTK_CONTAINER(pChild)); + int outerborder = gtk_container_get_border_width(GTK_CONTAINER(pTopLevel)); + int totalborder = outerborder + innerborder; + x -= totalborder; + y -= totalborder; +#endif + + return Point(x, y); + } + + void do_collect_screenshot_data(GtkWidget* pItem, gpointer data) + { + GtkWidget* pTopLevel = widget_get_toplevel(pItem); + + gtk_coord x, y; + gtk_widget_translate_coordinates(pItem, pTopLevel, 0, 0, &x, &y); + + Point aOffset = get_csd_offset(pTopLevel); + + GtkAllocation alloc; + gtk_widget_get_allocation(pItem, &alloc); + + const basegfx::B2IPoint aCurrentTopLeft(x - aOffset.X(), y - aOffset.Y()); + const basegfx::B2IRange aCurrentRange(aCurrentTopLeft, aCurrentTopLeft + basegfx::B2IPoint(alloc.width, alloc.height)); + + if (!aCurrentRange.isEmpty()) + { + weld::ScreenShotCollection* pCollection = static_cast(data); + pCollection->emplace_back(::get_help_id(pItem), aCurrentRange); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + for (GtkWidget* pChild = gtk_widget_get_first_child(pItem); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + do_collect_screenshot_data(pChild, data); + } +#else + if (GTK_IS_CONTAINER(pItem)) + gtk_container_forall(GTK_CONTAINER(pItem), do_collect_screenshot_data, data); +#endif + } + + AbsoluteScreenPixelRectangle get_monitor_workarea(GtkWidget* pWindow) + { + GdkRectangle aRect; +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkScreen* pScreen = gtk_widget_get_screen(pWindow); + gint nMonitor = gdk_screen_get_monitor_at_window(pScreen, widget_get_surface(pWindow)); + gdk_screen_get_monitor_workarea(pScreen, nMonitor, &aRect); +#else + GdkDisplay* pDisplay = gtk_widget_get_display(pWindow); + GdkSurface* gdkWindow = widget_get_surface(pWindow); + GdkMonitor* pMonitor = gdk_display_get_monitor_at_surface(pDisplay, gdkWindow); + gdk_monitor_get_geometry(pMonitor, &aRect); +#endif + return AbsoluteScreenPixelRectangle(aRect.x, aRect.y, aRect.x + aRect.width, aRect.y + aRect.height); + } + + +class GtkInstanceWindow : public GtkInstanceContainer, public virtual weld::Window +{ +private: + GtkWindow* m_pWindow; + rtl::Reference m_xWindow; //uno api + gulong m_nToplevelFocusChangedSignalId; +protected: + std::optional m_aPosWhileInvis; //tdf#146648 store last known position when visible to return as pos if hidden + +#if !GTK_CHECK_VERSION(4, 0, 0) + static void implResetDefault(GtkWidget *pWidget, gpointer user_data) + { + if (GTK_IS_BUTTON(pWidget)) + g_object_set(G_OBJECT(pWidget), "has-default", false, nullptr); + if (GTK_IS_CONTAINER(pWidget)) + gtk_container_forall(GTK_CONTAINER(pWidget), implResetDefault, user_data); + } + + void recursively_unset_default_buttons() + { + implResetDefault(GTK_WIDGET(m_pWindow), nullptr); + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + static gboolean help_pressed(GtkAccelGroup*, GObject*, guint, GdkModifierType, gpointer widget) + { + GtkInstanceWindow* pThis = static_cast(widget); + pThis->help(); + return true; + } +#endif + + static void signalToplevelFocusChanged(GtkWindow*, GParamSpec*, gpointer widget) + { + GtkInstanceWindow* pThis = static_cast(widget); + pThis->signal_container_focus_changed(); + } + + bool isPositioningAllowed() const + { + // no X/Y positioning under Wayland + GdkDisplay *pDisplay = gtk_widget_get_display(m_pWidget); + return !DLSYM_GDK_IS_WAYLAND_DISPLAY(pDisplay); + } + +protected: + void help(); +public: + GtkInstanceWindow(GtkWindow* pWindow, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) +#if !GTK_CHECK_VERSION(4, 0, 0) + : GtkInstanceContainer(GTK_CONTAINER(pWindow), pBuilder, bTakeOwnership) +#else + : GtkInstanceContainer(GTK_WIDGET(pWindow), pBuilder, bTakeOwnership) +#endif + , m_pWindow(pWindow) + , m_nToplevelFocusChangedSignalId(0) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + const bool bIsFrameWeld = pBuilder == nullptr; + if (!bIsFrameWeld) + { + //hook up F1 to show help + GtkAccelGroup *pGroup = gtk_accel_group_new(); + GClosure* closure = g_cclosure_new(G_CALLBACK(help_pressed), this, nullptr); + gtk_accel_group_connect(pGroup, GDK_KEY_F1, static_cast(0), GTK_ACCEL_LOCKED, closure); + gtk_window_add_accel_group(pWindow, pGroup); + } +#endif + } + + virtual void set_title(const OUString& rTitle) override + { + ::set_title(m_pWindow, rTitle); + } + + virtual OUString get_title() const override + { + return ::get_title(m_pWindow); + } + + virtual css::uno::Reference GetXWindow() override + { + if (!m_xWindow.is()) + m_xWindow.set(new SalGtkXWindow(this, m_pWidget)); + return m_xWindow; + } + + virtual void set_modal(bool bModal) override + { + gtk_window_set_modal(m_pWindow, bModal); + } + + virtual bool get_modal() const override + { + return gtk_window_get_modal(m_pWindow); + } + + virtual void resize_to_request() override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_window_set_default_size(m_pWindow, 1, 1); +#else + gtk_window_resize(m_pWindow, 1, 1); +#endif + } + + virtual void window_move(int x, int y) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_window_move(m_pWindow, x, y); +#else + (void)x; + (void)y; +#endif + } + + virtual SystemEnvData get_system_data() const override + { + GtkSalFrame* pFrame = GtkSalFrame::getFromWindow(GTK_WIDGET(m_pWindow)); + assert(pFrame && "nothing should call this impl, yet anyway, if ever, except on result of GetFrameWeld()"); + const SystemEnvData* pEnvData = pFrame->GetSystemData(); + assert(pEnvData); + return *pEnvData; + } + + virtual Size get_size() const override + { + int current_width, current_height; +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_window_get_size(m_pWindow, ¤t_width, ¤t_height); +#else + gtk_window_get_default_size(m_pWindow, ¤t_width, ¤t_height); +#endif + return Size(current_width, current_height); + } + + virtual Point get_position() const override + { + if (m_aPosWhileInvis) + { + assert(!get_visible()); + return *m_aPosWhileInvis; + } + + int current_x(0), current_y(0); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_window_get_position(m_pWindow, ¤t_x, ¤t_y); +#endif + return Point(current_x, current_y); + } + + virtual void show() override + { + m_aPosWhileInvis.reset(); + GtkInstanceContainer::show(); + } + + virtual void hide() override + { + if (is_visible()) + m_aPosWhileInvis = get_position(); + GtkInstanceContainer::hide(); + } + + virtual AbsoluteScreenPixelRectangle get_monitor_workarea() const override + { + return ::get_monitor_workarea(GTK_WIDGET(m_pWindow)); + } + + virtual void set_centered_on_parent(bool bTrackGeometryRequests) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + if (bTrackGeometryRequests) + gtk_window_set_position(m_pWindow, GTK_WIN_POS_CENTER_ALWAYS); + else + gtk_window_set_position(m_pWindow, GTK_WIN_POS_CENTER_ON_PARENT); +#else + (void)bTrackGeometryRequests; +#endif + } + + virtual bool get_resizable() const override + { + return gtk_window_get_resizable(m_pWindow); + } + + virtual bool has_toplevel_focus() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_window_is_active(m_pWindow); +#else + return gtk_window_has_toplevel_focus(m_pWindow); +#endif + } + + virtual void present() override + { + gtk_window_present(m_pWindow); + } + + virtual void change_default_widget(weld::Widget* pOld, weld::Widget* pNew) override + { + GtkInstanceWidget* pGtkNew = dynamic_cast(pNew); + GtkWidget* pWidgetNew = pGtkNew ? pGtkNew->getWidget() : nullptr; +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_window_set_default_widget(m_pWindow, pWidgetNew); + (void)pOld; +#else + GtkInstanceWidget* pGtkOld = dynamic_cast(pOld); + GtkWidget* pWidgetOld = pGtkOld ? pGtkOld->getWidget() : nullptr; + if (pWidgetOld) + g_object_set(G_OBJECT(pWidgetOld), "has-default", false, nullptr); + else + recursively_unset_default_buttons(); + if (pWidgetNew) + g_object_set(G_OBJECT(pWidgetNew), "has-default", true, nullptr); +#endif + } + + virtual bool is_default_widget(const weld::Widget* pCandidate) const override + { + const GtkInstanceWidget* pGtkCandidate = dynamic_cast(pCandidate); + GtkWidget* pWidget = pGtkCandidate ? pGtkCandidate->getWidget() : nullptr; +#if GTK_CHECK_VERSION(4, 0, 0) + return pWidget && gtk_window_get_default_widget(m_pWindow) == pWidget; +#else + gboolean has_default(false); + if (pWidget) + g_object_get(G_OBJECT(pWidget), "has-default", &has_default, nullptr); + return has_default; +#endif + } + + virtual void set_window_state(const OUString& rStr) override + { + const vcl::WindowData aData(rStr); + const auto nMask = aData.mask(); + const auto nState = aData.state() & vcl::WindowState::SystemMask; + + if ((nMask & vcl::WindowDataMask::Size) == vcl::WindowDataMask::Size) + { + gtk_window_set_default_size(m_pWindow, aData.width(), aData.height()); + } + if (nMask & vcl::WindowDataMask::State) + { + if (nState & vcl::WindowState::Maximized) + gtk_window_maximize(m_pWindow); + else + gtk_window_unmaximize(m_pWindow); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + if (isPositioningAllowed() && ((nMask & vcl::WindowDataMask::Pos) == vcl::WindowDataMask::Pos)) + { + gtk_window_move(m_pWindow, aData.x(), aData.y()); + } +#endif + } + + virtual OUString get_window_state(vcl::WindowDataMask nMask) const override + { + bool bPositioningAllowed = isPositioningAllowed(); + + vcl::WindowData aData; + vcl::WindowDataMask nAvailable = vcl::WindowDataMask::State | vcl::WindowDataMask::Size; + if (bPositioningAllowed) + nAvailable |= vcl::WindowDataMask::Pos; + aData.setMask(nMask & nAvailable); + + if (nMask & vcl::WindowDataMask::State) + { + vcl::WindowState nState = vcl::WindowState::Normal; + if (gtk_window_is_maximized(m_pWindow)) + nState |= vcl::WindowState::Maximized; + aData.setState(nState); + } + + if (bPositioningAllowed && (nMask & vcl::WindowDataMask::Pos)) + aData.setPos(get_position()); + + if (nMask & vcl::WindowDataMask::Size) + aData.setSize(get_size()); + + return aData.toStr(); + } + + virtual void connect_container_focus_changed(const Link& rLink) override + { + if (!m_nToplevelFocusChangedSignalId) + m_nToplevelFocusChangedSignalId = g_signal_connect(m_pWindow, "notify::has-toplevel-focus", G_CALLBACK(signalToplevelFocusChanged), this); + weld::Container::connect_container_focus_changed(rLink); + } + + virtual void disable_notify_events() override + { + if (m_nToplevelFocusChangedSignalId) + g_signal_handler_block(m_pWidget, m_nToplevelFocusChangedSignalId); + GtkInstanceContainer::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceContainer::enable_notify_events(); + if (m_nToplevelFocusChangedSignalId) + g_signal_handler_unblock(m_pWidget, m_nToplevelFocusChangedSignalId); + } + + virtual VclPtr screenshot() override + { + // detect if we have to manually setup its size + bool bAlreadyRealized = gtk_widget_get_realized(GTK_WIDGET(m_pWindow)); + // has to be visible for draw to work + bool bAlreadyVisible = gtk_widget_get_visible(GTK_WIDGET(m_pWindow)); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!bAlreadyVisible) + { + if (GTK_IS_DIALOG(m_pWindow)) + sort_native_button_order(GTK_BOX(gtk_dialog_get_action_area(GTK_DIALOG(m_pWindow)))); + gtk_widget_show(GTK_WIDGET(m_pWindow)); + } +#endif + + if (!bAlreadyRealized) + { + GtkAllocation allocation; + gtk_widget_realize(GTK_WIDGET(m_pWindow)); + gtk_widget_get_allocation(GTK_WIDGET(m_pWindow), &allocation); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_size_allocate(GTK_WIDGET(m_pWindow), &allocation); +#else + gtk_widget_size_allocate(GTK_WIDGET(m_pWindow), &allocation, 0); +#endif + } + + VclPtr xOutput(VclPtr::Create(DeviceFormat::WITHOUT_ALPHA)); + xOutput->SetOutputSizePixel(get_size()); + cairo_surface_t* pSurface = get_underlying_cairo_surface(*xOutput); + cairo_t* cr = cairo_create(pSurface); + + Point aOffset = get_csd_offset(GTK_WIDGET(m_pWindow)); + + cairo_translate(cr, -aOffset.X(), -aOffset.Y()); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_draw(GTK_WIDGET(m_pWindow), cr); +#else + GtkSnapshot* pSnapshot = gtk_snapshot_new(); + GtkWidgetClass* pWidgetClass = GTK_WIDGET_GET_CLASS(GTK_WIDGET(m_pWindow)); + pWidgetClass->snapshot(GTK_WIDGET(m_pWindow), pSnapshot); + GskRenderNode* pNode = gtk_snapshot_free_to_node(pSnapshot); + gsk_render_node_draw(pNode, cr); + gsk_render_node_unref(pNode); +#endif + + cairo_destroy(cr); + + if (!bAlreadyVisible) + gtk_widget_hide(GTK_WIDGET(m_pWindow)); + if (!bAlreadyRealized) + gtk_widget_unrealize(GTK_WIDGET(m_pWindow)); + + return xOutput; + } + + virtual weld::ScreenShotCollection collect_screenshot_data() override + { + weld::ScreenShotCollection aRet; + +#if GTK_CHECK_VERSION(4, 0, 0) + for (GtkWidget* pChild = gtk_widget_get_first_child(GTK_WIDGET(m_pWindow)); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + do_collect_screenshot_data(pChild, &aRet); + } +#else + gtk_container_foreach(GTK_CONTAINER(m_pWindow), do_collect_screenshot_data, &aRet); +#endif + + return aRet; + } + + virtual const vcl::ILibreOfficeKitNotifier* GetLOKNotifier() override + { + // dummy implementation + return nullptr; + } + + virtual ~GtkInstanceWindow() override + { + if (m_nToplevelFocusChangedSignalId) + g_signal_handler_disconnect(m_pWindow, m_nToplevelFocusChangedSignalId); + if (m_xWindow.is()) + m_xWindow->clear(); + } +}; + +class GtkInstanceDialog; + +struct DialogRunner +{ + GtkWindow* m_pDialog; + GtkInstanceDialog *m_pInstance; + gint m_nResponseId; + GMainLoop *m_pLoop; + VclPtr m_xFrameWindow; + int m_nModalDepth; + + DialogRunner(GtkWindow* pDialog, GtkInstanceDialog* pInstance) + : m_pDialog(pDialog) + , m_pInstance(pInstance) + , m_nResponseId(GTK_RESPONSE_NONE) + , m_pLoop(nullptr) + , m_nModalDepth(0) + { + GtkWindow* pParent = gtk_window_get_transient_for(m_pDialog); + GtkSalFrame* pFrame = pParent ? GtkSalFrame::getFromWindow(GTK_WIDGET(pParent)) : nullptr; + m_xFrameWindow = pFrame ? pFrame->GetWindow() : nullptr; + } + + bool loop_is_running() const + { + return m_pLoop && g_main_loop_is_running(m_pLoop); + } + + void loop_quit() + { + if (g_main_loop_is_running(m_pLoop)) + g_main_loop_quit(m_pLoop); + } + + static void signal_response(GtkDialog*, gint nResponseId, gpointer data); + static void signal_cancel(GtkAssistant*, gpointer data); + +#if !GTK_CHECK_VERSION(4, 0, 0) + static gboolean signal_delete(GtkDialog* pDialog, GdkEventAny*, gpointer data) + { + DialogRunner* pThis = static_cast(data); + if (GTK_IS_ASSISTANT(pThis->m_pDialog)) + { + // An assistant isn't a dialog, but we want to treat it like one + signal_response(pDialog, GTK_RESPONSE_DELETE_EVENT, data); + } + else + pThis->loop_quit(); + return true; /* Do not destroy */ + } +#endif + + static void signal_destroy(GtkDialog*, gpointer data) + { + DialogRunner* pThis = static_cast(data); + pThis->loop_quit(); + } + + void inc_modal_count() + { + if (m_xFrameWindow) + { + m_xFrameWindow->IncModalCount(); + if (m_nModalDepth == 0) + m_xFrameWindow->ImplGetFrame()->NotifyModalHierarchy(true); + ++m_nModalDepth; + } + } + + void dec_modal_count() + { + if (m_xFrameWindow) + { + m_xFrameWindow->DecModalCount(); + --m_nModalDepth; + if (m_nModalDepth == 0) + m_xFrameWindow->ImplGetFrame()->NotifyModalHierarchy(false); + } + } + + // same as gtk_dialog_run except that unmap doesn't auto-respond + // so we can hide the dialog and restore it without a response getting + // triggered + gint run() + { + g_object_ref(m_pDialog); + + inc_modal_count(); + + bool bWasModal = gtk_window_get_modal(m_pDialog); + if (!bWasModal) + gtk_window_set_modal(m_pDialog, true); + + if (!gtk_widget_get_visible(GTK_WIDGET(m_pDialog))) + gtk_widget_show(GTK_WIDGET(m_pDialog)); + + gulong nSignalResponseId = GTK_IS_DIALOG(m_pDialog) ? g_signal_connect(m_pDialog, "response", G_CALLBACK(signal_response), this) : 0; + gulong nSignalCancelId = GTK_IS_ASSISTANT(m_pDialog) ? g_signal_connect(m_pDialog, "cancel", G_CALLBACK(signal_cancel), this) : 0; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong nSignalDeleteId = g_signal_connect(m_pDialog, "delete-event", G_CALLBACK(signal_delete), this); +#endif + gulong nSignalDestroyId = g_signal_connect(m_pDialog, "destroy", G_CALLBACK(signal_destroy), this); + + m_pLoop = g_main_loop_new(nullptr, false); + m_nResponseId = GTK_RESPONSE_NONE; + + main_loop_run(m_pLoop); + + g_main_loop_unref(m_pLoop); + + m_pLoop = nullptr; + + if (!bWasModal) + gtk_window_set_modal(m_pDialog, false); + + if (nSignalResponseId) + g_signal_handler_disconnect(m_pDialog, nSignalResponseId); + if (nSignalCancelId) + g_signal_handler_disconnect(m_pDialog, nSignalCancelId); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pDialog, nSignalDeleteId); +#endif + g_signal_handler_disconnect(m_pDialog, nSignalDestroyId); + + dec_modal_count(); + + g_object_unref(m_pDialog); + + return m_nResponseId; + } + + ~DialogRunner() + { + if (m_xFrameWindow && m_nModalDepth) + { + // if, like the calc validation dialog does, the modality was + // toggled off during execution ensure that on cleanup the parent + // is left in the state it was found + while (m_nModalDepth++ < 0) + m_xFrameWindow->IncModalCount(); + } + } +}; + +} + +typedef std::set winset; + +namespace +{ +#if GTK_CHECK_VERSION(4, 0, 0) + void collectVisibleChildren(GtkWidget* pTop, winset& rVisibleWidgets) + { + for (GtkWidget* pChild = gtk_widget_get_first_child(pTop); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + if (!gtk_widget_get_visible(pChild)) + continue; + rVisibleWidgets.insert(pChild); + collectVisibleChildren(pChild, rVisibleWidgets); + } + } +#endif + + void hideUnless(GtkWidget* pTop, const winset& rVisibleWidgets, + std::vector &rWasVisibleWidgets) + { +#if GTK_CHECK_VERSION(4, 0, 0) + for (GtkWidget* pChild = gtk_widget_get_first_child(pTop); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + if (!gtk_widget_get_visible(pChild)) + continue; + if (rVisibleWidgets.find(pChild) == rVisibleWidgets.end()) + { + g_object_ref(pChild); + rWasVisibleWidgets.emplace_back(pChild); + gtk_widget_hide(pChild); + } + else + { + hideUnless(pChild, rVisibleWidgets, rWasVisibleWidgets); + } + } +#else + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(pTop)); + for (GList* pEntry = g_list_first(pChildren); pEntry; pEntry = g_list_next(pEntry)) + { + GtkWidget* pChild = static_cast(pEntry->data); + if (!gtk_widget_get_visible(pChild)) + continue; + if (rVisibleWidgets.find(pChild) == rVisibleWidgets.end()) + { + g_object_ref(pChild); + rWasVisibleWidgets.emplace_back(pChild); + gtk_widget_hide(pChild); + } + else if (GTK_IS_CONTAINER(pChild)) + { + hideUnless(pChild, rVisibleWidgets, rWasVisibleWidgets); + } + } + g_list_free(pChildren); +#endif + } + +class GtkInstanceButton; + +class GtkInstanceDialog : public GtkInstanceWindow, public virtual weld::Dialog +{ +private: + GtkWindow* m_pDialog; + DialogRunner m_aDialogRun; + std::shared_ptr m_xDialogController; + // Used to keep ourself alive during a runAsync(when doing runAsync without a DialogController) + std::shared_ptr m_xRunAsyncSelf; + std::function m_aFunc; + gulong m_nCloseSignalId; + gulong m_nResponseSignalId; + gulong m_nCancelSignalId; + gulong m_nSignalDeleteId; + + // for calc ref dialog that shrink to range selection widgets and resize back + GtkWidget* m_pRefEdit; + std::vector m_aHiddenWidgets; // vector of hidden Controls + int m_nOldEditWidth; // Original width of the input field + int m_nOldEditWidthReq; // Original width request of the input field +#if !GTK_CHECK_VERSION(4, 0, 0) + int m_nOldBorderWidth; // border width for expanded dialog +#endif + + void signal_close() + { + close(true); + } + + static void signalClose(GtkWidget*, gpointer widget) + { + GtkInstanceDialog* pThis = static_cast(widget); + pThis->signal_close(); + } + + static void signalAsyncResponse(GtkWidget*, gint ret, gpointer widget) + { + GtkInstanceDialog* pThis = static_cast(widget); + pThis->asyncresponse(ret); + } + + static void signalAsyncCancel(GtkAssistant*, gpointer widget) + { + GtkInstanceDialog* pThis = static_cast(widget); + // make esc in an assistant act as if cancel button was pressed + pThis->close(false); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static gboolean signalAsyncDelete(GtkWidget* pDialog, GdkEventAny*, gpointer widget) + { + GtkInstanceDialog* pThis = static_cast(widget); + if (GTK_IS_ASSISTANT(pThis->m_pDialog)) + { + // An assistant isn't a dialog, but we want to treat it like one + signalAsyncResponse(pDialog, GTK_RESPONSE_DELETE_EVENT, widget); + } + return true; /* Do not destroy */ + } +#endif + + static int GtkToVcl(int ret) + { + if (ret == GTK_RESPONSE_OK) + ret = RET_OK; + else if (ret == GTK_RESPONSE_CANCEL) + ret = RET_CANCEL; + else if (ret == GTK_RESPONSE_DELETE_EVENT) + ret = RET_CANCEL; + else if (ret == GTK_RESPONSE_CLOSE) + ret = RET_CLOSE; + else if (ret == GTK_RESPONSE_YES) + ret = RET_YES; + else if (ret == GTK_RESPONSE_NO) + ret = RET_NO; + else if (ret == GTK_RESPONSE_HELP) + ret = RET_HELP; + return ret; + } + + static int VclToGtk(int nResponse) + { + if (nResponse == RET_OK) + return GTK_RESPONSE_OK; + else if (nResponse == RET_CANCEL) + return GTK_RESPONSE_CANCEL; + else if (nResponse == RET_CLOSE) + return GTK_RESPONSE_CLOSE; + else if (nResponse == RET_YES) + return GTK_RESPONSE_YES; + else if (nResponse == RET_NO) + return GTK_RESPONSE_NO; + else if (nResponse == RET_HELP) + return GTK_RESPONSE_HELP; + return nResponse; + } + + void asyncresponse(gint ret); + +#if !GTK_CHECK_VERSION(4, 0, 0) + static void signalActivate(GtkMenuItem*, gpointer data) + { + bool* pActivate = static_cast(data); + *pActivate = true; + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + bool signal_screenshot_popup_menu(const GdkEventButton* pEvent) + { + GtkWidget *pMenu = gtk_menu_new(); + + GtkWidget* pMenuItem = gtk_menu_item_new_with_mnemonic(MapToGtkAccelerator(VclResId(SV_BUTTONTEXT_SCREENSHOT)).getStr()); + gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem); + bool bActivate(false); + g_signal_connect(pMenuItem, "activate", G_CALLBACK(signalActivate), &bActivate); + gtk_widget_show(pMenuItem); + + int button, event_time; + if (pEvent) + { + button = pEvent->button; + event_time = pEvent->time; + } + else + { + button = 0; + event_time = gtk_get_current_event_time(); + } + + gtk_menu_attach_to_widget(GTK_MENU(pMenu), GTK_WIDGET(m_pDialog), nullptr); + + GMainLoop* pLoop = g_main_loop_new(nullptr, true); + gulong nSignalId = g_signal_connect_swapped(G_OBJECT(pMenu), "deactivate", G_CALLBACK(g_main_loop_quit), pLoop); + + gtk_menu_popup(GTK_MENU(pMenu), nullptr, nullptr, nullptr, nullptr, button, event_time); + + if (g_main_loop_is_running(pLoop)) + main_loop_run(pLoop); + + g_main_loop_unref(pLoop); + g_signal_handler_disconnect(pMenu, nSignalId); + gtk_menu_detach(GTK_MENU(pMenu)); + + if (bActivate) + { + // open screenshot annotation dialog + VclAbstractDialogFactory* pFact = VclAbstractDialogFactory::Create(); + VclPtr xTmp = pFact->CreateScreenshotAnnotationDlg(*this); + ScopedVclPtr xDialog(xTmp); + xDialog->Execute(); + } + + return false; + } +#endif + + static gboolean signalScreenshotPopupMenu(GtkWidget*, gpointer widget) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkInstanceDialog* pThis = static_cast(widget); + return pThis->signal_screenshot_popup_menu(nullptr); +#else + (void)widget; + return false; +#endif + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static gboolean signalScreenshotButton(GtkWidget*, GdkEventButton* pEvent, gpointer widget) + { + GtkInstanceDialog* pThis = static_cast(widget); + SolarMutexGuard aGuard; + return pThis->signal_screenshot_button(pEvent); + } + + bool signal_screenshot_button(GdkEventButton* pEvent) + { + if (gdk_event_triggers_context_menu(reinterpret_cast(pEvent)) && pEvent->type == GDK_BUTTON_PRESS) + { + //if handled for context menu, stop processing + return signal_screenshot_popup_menu(pEvent); + } + return false; + } +#endif + +public: + GtkInstanceDialog(GtkWindow* pDialog, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWindow(pDialog, pBuilder, bTakeOwnership) + , m_pDialog(pDialog) + , m_aDialogRun(pDialog, this) + , m_nResponseSignalId(0) + , m_nCancelSignalId(0) + , m_nSignalDeleteId(0) + , m_pRefEdit(nullptr) + , m_nOldEditWidth(0) + , m_nOldEditWidthReq(0) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_nOldBorderWidth(0) +#endif + { + if (GTK_IS_DIALOG(m_pDialog) || GTK_IS_ASSISTANT(m_pDialog)) + m_nCloseSignalId = g_signal_connect(m_pDialog, "close", G_CALLBACK(signalClose), this); + else + m_nCloseSignalId = 0; + const bool bScreenshotMode(officecfg::Office::Common::Misc::ScreenshotMode::get()); + if (bScreenshotMode) + { + g_signal_connect(m_pDialog, "popup-menu", G_CALLBACK(signalScreenshotPopupMenu), this); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_connect(m_pDialog, "button-press-event", G_CALLBACK(signalScreenshotButton), this); +#endif + } + } + + virtual bool runAsync(std::shared_ptr rDialogController, const std::function& func) override + { + assert(!m_nResponseSignalId && !m_nCancelSignalId && !m_nSignalDeleteId); + + m_xDialogController = rDialogController; + m_aFunc = func; + + if (get_modal()) + m_aDialogRun.inc_modal_count(); + show(); + + m_nResponseSignalId = GTK_IS_DIALOG(m_pDialog) ? g_signal_connect(m_pDialog, "response", G_CALLBACK(signalAsyncResponse), this) : 0; + m_nCancelSignalId = GTK_IS_ASSISTANT(m_pDialog) ? g_signal_connect(m_pDialog, "cancel", G_CALLBACK(signalAsyncCancel), this) : 0; +#if !GTK_CHECK_VERSION(4, 0, 0) + m_nSignalDeleteId = g_signal_connect(m_pDialog, "delete-event", G_CALLBACK(signalAsyncDelete), this); +#endif + + return true; + } + + virtual bool runAsync(std::shared_ptr const & rxSelf, const std::function& func) override + { + assert( rxSelf.get() == this ); + assert(!m_nResponseSignalId && !m_nCancelSignalId && !m_nSignalDeleteId); + + // In order to store a shared_ptr to ourself, we have to have been constructed by make_shared, + // which is that rxSelf enforces. + m_xRunAsyncSelf = rxSelf; + m_aFunc = func; + + if (get_modal()) + m_aDialogRun.inc_modal_count(); + show(); + + m_nResponseSignalId = GTK_IS_DIALOG(m_pDialog) ? g_signal_connect(m_pDialog, "response", G_CALLBACK(signalAsyncResponse), this) : 0; + m_nCancelSignalId = GTK_IS_ASSISTANT(m_pDialog) ? g_signal_connect(m_pDialog, "cancel", G_CALLBACK(signalAsyncCancel), this) : 0; +#if !GTK_CHECK_VERSION(4, 0, 0) + m_nSignalDeleteId = g_signal_connect(m_pDialog, "delete-event", G_CALLBACK(signalAsyncDelete), this); +#endif + + return true; + } + + GtkInstanceButton* has_click_handler(int nResponse); + + virtual int run() override; + + virtual void show() override + { + if (gtk_widget_get_visible(m_pWidget)) + return; +#if !GTK_CHECK_VERSION(4, 0, 0) + if (GTK_IS_DIALOG(m_pDialog)) + sort_native_button_order(GTK_BOX(gtk_dialog_get_action_area(GTK_DIALOG(m_pDialog)))); +#endif + GtkInstanceWindow::show(); + } + + virtual void set_modal(bool bModal) override + { + if (get_modal() == bModal) + return; + GtkInstanceWindow::set_modal(bModal); + /* if change the dialog modality while its running, then also change the parent LibreOffice window + modal count, we typically expect the dialog modality to be restored to its original state + + This change modality while running case is for... + + a) the calc/chart dialogs which put up an extra range chooser + dialog, hides the original, the user can select a range of cells and + on completion the original dialog is restored + + b) the validity dialog in calc + */ + // tdf#135567 we know we are running in the sync case if loop_is_running is true + // but for the async case we instead check for m_xDialogController which is set in + // runAsync and cleared in asyncresponse + if (m_aDialogRun.loop_is_running() || m_xDialogController) + { + if (bModal) + m_aDialogRun.inc_modal_count(); + else + m_aDialogRun.dec_modal_count(); + } + } + + virtual void response(int nResponse) override; + + virtual void add_button(const OUString& rText, int nResponse, const OUString& rHelpId) override + { + GtkWidget* pWidget = gtk_dialog_add_button(GTK_DIALOG(m_pDialog), MapToGtkAccelerator(rText).getStr(), VclToGtk(nResponse)); + if (!rHelpId.isEmpty()) + ::set_help_id(pWidget, rHelpId); + } + + virtual void set_default_response(int nResponse) override + { + gtk_dialog_set_default_response(GTK_DIALOG(m_pDialog), VclToGtk(nResponse)); + } + + virtual GtkButton* get_widget_for_response(int nGtkResponse) + { + return GTK_BUTTON(gtk_dialog_get_widget_for_response(GTK_DIALOG(m_pDialog), nGtkResponse)); + } + + virtual weld::Button* weld_widget_for_response(int nVclResponse) override; + + virtual Container* weld_content_area() override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + return new GtkInstanceContainer(GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(m_pDialog))), m_pBuilder, false); +#else + return new GtkInstanceContainer(gtk_dialog_get_content_area(GTK_DIALOG(m_pDialog)), m_pBuilder, false); +#endif + } + + virtual void collapse(weld::Widget* pEdit, weld::Widget* pButton) override + { + GtkInstanceWidget* pVclEdit = dynamic_cast(pEdit); + assert(pVclEdit); + GtkInstanceWidget* pVclButton = dynamic_cast(pButton); + + GtkWidget* pRefEdit = pVclEdit->getWidget(); + GtkWidget* pRefBtn = pVclButton ? pVclButton->getWidget() : nullptr; + + m_nOldEditWidth = gtk_widget_get_allocated_width(pRefEdit); + + gtk_widget_get_size_request(pRefEdit, &m_nOldEditWidthReq, nullptr); + + //We want just pRefBtn and pRefEdit to be shown + //mark widgets we want to be visible, starting with pRefEdit + //and all its direct parents. + winset aVisibleWidgets; + GtkWidget *pContentArea = gtk_dialog_get_content_area(GTK_DIALOG(m_pDialog)); + for (GtkWidget *pCandidate = pRefEdit; + pCandidate && pCandidate != pContentArea && gtk_widget_get_visible(pCandidate); + pCandidate = gtk_widget_get_parent(pCandidate)) + { + aVisibleWidgets.insert(pCandidate); + } +#if GTK_CHECK_VERSION(4, 0, 0) + collectVisibleChildren(pRefEdit, aVisibleWidgets); +#endif + if (pRefBtn) + { +#if GTK_CHECK_VERSION(4, 0, 0) + collectVisibleChildren(pRefBtn, aVisibleWidgets); +#endif + //same again with pRefBtn, except stop if there's a + //shared parent in the existing widgets + for (GtkWidget *pCandidate = pRefBtn; + pCandidate && pCandidate != pContentArea && gtk_widget_get_visible(pCandidate); + pCandidate = gtk_widget_get_parent(pCandidate)) + { + if (aVisibleWidgets.insert(pCandidate).second) + break; + } + } + + //hide everything except the aVisibleWidgets + hideUnless(pContentArea, aVisibleWidgets, m_aHiddenWidgets); + gtk_widget_set_size_request(pRefEdit, m_nOldEditWidth, -1); +#if !GTK_CHECK_VERSION(4, 0, 0) + m_nOldBorderWidth = gtk_container_get_border_width(GTK_CONTAINER(m_pDialog)); + gtk_container_set_border_width(GTK_CONTAINER(m_pDialog), 0); + if (GtkWidget* pActionArea = gtk_dialog_get_action_area(GTK_DIALOG(m_pDialog))) + gtk_widget_hide(pActionArea); + gtk_widget_show_all(pRefEdit); + if (pRefBtn) + gtk_widget_show_all(pRefBtn); +#else + if (GtkWidget* pActionArea = gtk_dialog_get_header_bar(GTK_DIALOG(m_pDialog))) + gtk_widget_hide(pActionArea); +#endif + + // calc's insert->function is springing back to its original size if the ref-button + // is used to shrink the dialog down and then the user clicks in the calc area to do + // the selection + bool bWorkaroundSizeSpringingBack = DLSYM_GDK_IS_WAYLAND_DISPLAY(gtk_widget_get_display(m_pWidget)); + if (bWorkaroundSizeSpringingBack) + gtk_widget_unmap(GTK_WIDGET(m_pDialog)); + + resize_to_request(); + + if (bWorkaroundSizeSpringingBack) + gtk_widget_map(GTK_WIDGET(m_pDialog)); + + m_pRefEdit = pRefEdit; + } + + virtual void undo_collapse() override + { + // All others: Show(); + for (GtkWidget* pWindow : m_aHiddenWidgets) + { + gtk_widget_show(pWindow); + g_object_unref(pWindow); + } + m_aHiddenWidgets.clear(); + + gtk_widget_set_size_request(m_pRefEdit, m_nOldEditWidthReq, -1); + m_pRefEdit = nullptr; +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_set_border_width(GTK_CONTAINER(m_pDialog), m_nOldBorderWidth); + if (GtkWidget* pActionArea = gtk_dialog_get_action_area(GTK_DIALOG(m_pDialog))) + gtk_widget_show(pActionArea); +#else + if (GtkWidget* pActionArea = gtk_dialog_get_header_bar(GTK_DIALOG(m_pDialog))) + gtk_widget_show(pActionArea); +#endif + resize_to_request(); + present(); + } + + void close(bool bCloseSignal); + + virtual void SetInstallLOKNotifierHdl(const Link&) override + { + //not implemented for the gtk variant + } + + virtual ~GtkInstanceDialog() override + { + if (!m_aHiddenWidgets.empty()) + { + for (GtkWidget* pWindow : m_aHiddenWidgets) + g_object_unref(pWindow); + m_aHiddenWidgets.clear(); + } + + if (m_nCloseSignalId) + g_signal_handler_disconnect(m_pDialog, m_nCloseSignalId); + assert(!m_nResponseSignalId && !m_nCancelSignalId && !m_nSignalDeleteId); + } +}; + +} + +void DialogRunner::signal_response(GtkDialog*, gint nResponseId, gpointer data) +{ + DialogRunner* pThis = static_cast(data); + + // make GTK_RESPONSE_DELETE_EVENT act as if cancel button was pressed + if (nResponseId == GTK_RESPONSE_DELETE_EVENT) + { + pThis->m_pInstance->close(false); + return; + } + + pThis->m_nResponseId = nResponseId; + pThis->loop_quit(); +} + +void DialogRunner::signal_cancel(GtkAssistant*, gpointer data) +{ + DialogRunner* pThis = static_cast(data); + + // make esc in an assistant act as if cancel button was pressed + pThis->m_pInstance->close(false); +} + +namespace { + +class GtkInstanceMessageDialog : public GtkInstanceDialog, public virtual weld::MessageDialog +{ +private: + GtkMessageDialog* m_pMessageDialog; +public: + GtkInstanceMessageDialog(GtkMessageDialog* pMessageDialog, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceDialog(GTK_WINDOW(pMessageDialog), pBuilder, bTakeOwnership) + , m_pMessageDialog(pMessageDialog) + { + } + + virtual void set_primary_text(const OUString& rText) override + { + ::set_primary_text(m_pMessageDialog, rText); + } + + virtual OUString get_primary_text() const override + { + return ::get_primary_text(m_pMessageDialog); + } + + virtual void set_secondary_text(const OUString& rText) override + { + ::set_secondary_text(m_pMessageDialog, rText); + } + + virtual OUString get_secondary_text() const override + { + return ::get_secondary_text(m_pMessageDialog); + } + + virtual Container* weld_message_area() override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + return new GtkInstanceContainer(GTK_CONTAINER(gtk_message_dialog_get_message_area(m_pMessageDialog)), m_pBuilder, false); +#else + return new GtkInstanceContainer(gtk_message_dialog_get_message_area(m_pMessageDialog), m_pBuilder, false); +#endif + } +}; + +void set_label_wrap(GtkLabel* pLabel, bool bWrap) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_label_set_wrap(pLabel, bWrap); +#else + gtk_label_set_line_wrap(pLabel, bWrap); +#endif +} + +class GtkInstanceAssistant : public GtkInstanceDialog, public virtual weld::Assistant +{ +private: + GtkAssistant* m_pAssistant; + GtkWidget* m_pSidebar; + GtkWidget* m_pSidebarEventBox; +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkButtonBox* m_pButtonBox; +#else + GtkBox* m_pButtonBox; + GtkEventController* m_pSidebarClickController; +#endif + GtkButton* m_pHelp; + GtkButton* m_pBack; + GtkButton* m_pNext; + GtkButton* m_pFinish; + GtkButton* m_pCancel; + gulong m_nButtonPressSignalId; + std::vector> m_aPages; + std::map m_aNotClickable; + + int find_page(std::u16string_view ident) const + { + int nPages = gtk_assistant_get_n_pages(m_pAssistant); + for (int i = 0; i < nPages; ++i) + { + GtkWidget* pPage = gtk_assistant_get_nth_page(m_pAssistant, i); + OUString sBuildableName = ::get_buildable_id(GTK_BUILDABLE(pPage)); + if (sBuildableName == ident) + return i; + } + return -1; + } + + static void wrap_sidebar_label(GtkWidget *pWidget, gpointer /*user_data*/) + { + if (GTK_IS_LABEL(pWidget)) + { + ::set_label_wrap(GTK_LABEL(pWidget), true); + gtk_label_set_width_chars(GTK_LABEL(pWidget), 22); + gtk_label_set_max_width_chars(GTK_LABEL(pWidget), 22); + } + } + + static void find_sidebar(GtkWidget *pWidget, gpointer user_data) + { + OUString sBuildableName = ::get_buildable_id(GTK_BUILDABLE(pWidget)); + if (sBuildableName == "sidebar") + { + GtkWidget **ppSidebar = static_cast(user_data); + *ppSidebar = pWidget; + } +#if !GTK_CHECK_VERSION(4, 0, 0) + if (GTK_IS_CONTAINER(pWidget)) + gtk_container_forall(GTK_CONTAINER(pWidget), find_sidebar, user_data); +#endif + } + + static void signalHelpClicked(GtkButton*, gpointer widget) + { + GtkInstanceAssistant* pThis = static_cast(widget); + pThis->signal_help_clicked(); + } + + void signal_help_clicked() + { + help(); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalButton(GtkGestureClick* /*pGesture*/, int /*n_press*/, gdouble x, gdouble y, gpointer widget) + { + GtkInstanceAssistant* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_button(x, y); + } +#else + static gboolean signalButton(GtkWidget*, GdkEventButton* pEvent, gpointer widget) + { + GtkInstanceAssistant* pThis = static_cast(widget); + SolarMutexGuard aGuard; + return pThis->signal_button(pEvent->x, pEvent->y); + } +#endif + + bool signal_button(gtk_coord event_x, gtk_coord event_y) + { + int nNewCurrentPage = -1; + + GtkAllocation allocation; + + int nPageIndex = 0; + +#if GTK_CHECK_VERSION(4, 0, 0) + for (GtkWidget* pWidget = gtk_widget_get_first_child(m_pSidebar); + pWidget; pWidget = gtk_widget_get_next_sibling(pWidget)) + { +#else + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(m_pSidebar)); + for (GList* pChild = g_list_first(pChildren); pChild; pChild = g_list_next(pChild)) + { + GtkWidget* pWidget = static_cast(pChild->data); +#endif + if (!gtk_widget_get_visible(pWidget)) + continue; + + gtk_widget_get_allocation(pWidget, &allocation); + + gtk_coord dest_x1, dest_y1; + gtk_widget_translate_coordinates(pWidget, + m_pSidebarEventBox, + 0, + 0, + &dest_x1, + &dest_y1); + + gtk_coord dest_x2, dest_y2; + gtk_widget_translate_coordinates(pWidget, + m_pSidebarEventBox, + allocation.width, + allocation.height, + &dest_x2, + &dest_y2); + + + if (event_x >= dest_x1 && event_x <= dest_x2 && event_y >= dest_y1 && event_y <= dest_y2) + { + nNewCurrentPage = nPageIndex; + break; + } + + ++nPageIndex; + } +#if !GTK_CHECK_VERSION(4, 0, 0) + g_list_free(pChildren); +#endif + + if (nNewCurrentPage != -1 && nNewCurrentPage != get_current_page()) + { + OUString sIdent = get_page_ident(nNewCurrentPage); + if (!m_aNotClickable[sIdent] && !signal_jump_page(sIdent)) + set_current_page(nNewCurrentPage); + } + + return false; + } + +public: + GtkInstanceAssistant(GtkAssistant* pAssistant, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceDialog(GTK_WINDOW(pAssistant), pBuilder, bTakeOwnership) + , m_pAssistant(pAssistant) + , m_pSidebar(nullptr) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_pSidebarClickController(nullptr) +#endif + , m_nButtonPressSignalId(0) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + m_pButtonBox = GTK_BUTTON_BOX(gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL)); + gtk_button_box_set_layout(m_pButtonBox, GTK_BUTTONBOX_END); + gtk_box_set_spacing(GTK_BOX(m_pButtonBox), 6); +#else + m_pButtonBox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6)); +#endif + + m_pBack = GTK_BUTTON(gtk_button_new_with_mnemonic(MapToGtkAccelerator(GetStandardText(StandardButtonType::Back)).getStr())); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_can_default(GTK_WIDGET(m_pBack), true); +#endif + ::set_buildable_id(GTK_BUILDABLE(m_pBack), "previous"); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_box_append(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pBack)); +#else + gtk_box_pack_end(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pBack), false, false, 0); +#endif + + m_pNext = GTK_BUTTON(gtk_button_new_with_mnemonic(MapToGtkAccelerator(GetStandardText(StandardButtonType::Next)).getStr())); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_can_default(GTK_WIDGET(m_pNext), true); +#endif + ::set_buildable_id(GTK_BUILDABLE(m_pNext), "next"); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_box_append(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pNext)); +#else + gtk_box_pack_end(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pNext), false, false, 0); +#endif + + m_pCancel = GTK_BUTTON(gtk_button_new_with_mnemonic(MapToGtkAccelerator(GetStandardText(StandardButtonType::Cancel)).getStr())); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_can_default(GTK_WIDGET(m_pCancel), true); +#endif +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_box_append(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pCancel)); +#else + gtk_box_pack_end(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pCancel), false, false, 0); +#endif + + m_pFinish = GTK_BUTTON(gtk_button_new_with_mnemonic(MapToGtkAccelerator(GetStandardText(StandardButtonType::Finish)).getStr())); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_can_default(GTK_WIDGET(m_pFinish), true); +#endif + ::set_buildable_id(GTK_BUILDABLE(m_pFinish), "finish"); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_box_append(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pFinish)); +#else + gtk_box_pack_end(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pFinish), false, false, 0); +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + m_pHelp = GTK_BUTTON(gtk_button_new_from_icon_name("help-browser-symbolic")); +#else + m_pHelp = GTK_BUTTON(gtk_button_new_with_mnemonic(MapToGtkAccelerator(GetStandardText(StandardButtonType::Help)).getStr())); +#endif +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_can_default(GTK_WIDGET(m_pHelp), true); +#endif + g_signal_connect(m_pHelp, "clicked", G_CALLBACK(signalHelpClicked), this); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_box_prepend(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pHelp)); + gtk_widget_set_hexpand(GTK_WIDGET(m_pHelp), true); + gtk_widget_set_halign(GTK_WIDGET(m_pHelp), GTK_ALIGN_START); +#else + gtk_box_pack_end(GTK_BOX(m_pButtonBox), GTK_WIDGET(m_pHelp), false, false, 0); +#endif + + gtk_assistant_add_action_widget(pAssistant, GTK_WIDGET(m_pButtonBox)); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_button_box_set_child_secondary(m_pButtonBox, GTK_WIDGET(m_pHelp), true); +#endif + gtk_widget_set_hexpand(GTK_WIDGET(m_pButtonBox), true); + + GtkWidget* pParent = gtk_widget_get_parent(GTK_WIDGET(m_pButtonBox)); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_child_set(GTK_CONTAINER(pParent), GTK_WIDGET(m_pButtonBox), "expand", true, "fill", true, nullptr); +#endif + gtk_widget_set_halign(pParent, GTK_ALIGN_FILL); + + // Hide the built-in ones early so we get a nice optimal size for the width without + // including the unused contents +#if GTK_CHECK_VERSION(4, 0, 0) + for (GtkWidget* pChild = gtk_widget_get_first_child(pParent); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + gtk_widget_hide(pChild); + } +#else + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(pParent)); + for (GList* pChild = g_list_first(pChildren); pChild; pChild = g_list_next(pChild)) + { + GtkWidget* pWidget = static_cast(pChild->data); + gtk_widget_hide(pWidget); + } + g_list_free(pChildren); +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_show_all(GTK_WIDGET(m_pButtonBox)); +#else + gtk_widget_show(GTK_WIDGET(m_pButtonBox)); +#endif + + find_sidebar(GTK_WIDGET(m_pAssistant), &m_pSidebar); + + m_pSidebarEventBox = ::ensureEventWidget(m_pSidebar); + if (m_pSidebarEventBox) + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkGesture *pClick = gtk_gesture_click_new(); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(pClick), 0); + m_pSidebarClickController = GTK_EVENT_CONTROLLER(pClick); + gtk_widget_add_controller(m_pSidebarEventBox, m_pSidebarClickController); + m_nButtonPressSignalId = g_signal_connect(m_pSidebarClickController, "pressed", G_CALLBACK(signalButton), this); +#else + m_nButtonPressSignalId = g_signal_connect(m_pSidebarEventBox, "button-press-event", G_CALLBACK(signalButton), this); +#endif + } + } + + virtual int get_current_page() const override + { + return gtk_assistant_get_current_page(m_pAssistant); + } + + virtual int get_n_pages() const override + { + return gtk_assistant_get_n_pages(m_pAssistant); + } + + virtual OUString get_page_ident(int nPage) const override + { + const GtkWidget* pWidget = gtk_assistant_get_nth_page(m_pAssistant, nPage); + return ::get_buildable_id(GTK_BUILDABLE(pWidget)); + } + + virtual OUString get_current_page_ident() const override + { + return get_page_ident(get_current_page()); + } + + virtual void set_current_page(int nPage) override + { + OString sDialogTitle(gtk_window_get_title(GTK_WINDOW(m_pAssistant))); + + gtk_assistant_set_current_page(m_pAssistant, nPage); + + // if the page doesn't have a title, then the dialog will now have no + // title, so restore the original title as a fallback + GtkWidget* pPage = gtk_assistant_get_nth_page(m_pAssistant, nPage); + if (!gtk_assistant_get_page_title(m_pAssistant, pPage)) + gtk_window_set_title(GTK_WINDOW(m_pAssistant), sDialogTitle.getStr()); + } + + virtual void set_current_page(const OUString& rIdent) override + { + int nPage = find_page(rIdent); + if (nPage == -1) + return; + set_current_page(nPage); + } + + virtual void set_page_title(const OUString& rIdent, const OUString& rTitle) override + { + int nIndex = find_page(rIdent); + if (nIndex == -1) + return; + GtkWidget* pPage = gtk_assistant_get_nth_page(m_pAssistant, nIndex); + gtk_assistant_set_page_title(m_pAssistant, pPage, + OUStringToOString(rTitle, RTL_TEXTENCODING_UTF8).getStr()); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_forall(GTK_CONTAINER(m_pSidebar), wrap_sidebar_label, nullptr); +#endif + } + + virtual OUString get_page_title(const OUString& rIdent) const override + { + int nIndex = find_page(rIdent); + if (nIndex == -1) + return OUString(); + GtkWidget* pPage = gtk_assistant_get_nth_page(m_pAssistant, nIndex); + const gchar* pStr = gtk_assistant_get_page_title(m_pAssistant, pPage); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual void set_page_sensitive(const OUString& rIdent, bool bSensitive) override + { + m_aNotClickable[rIdent] = !bSensitive; + } + + virtual void set_page_index(const OUString& rIdent, int nNewIndex) override + { + int nOldIndex = find_page(rIdent); + if (nOldIndex == -1) + return; + + if (nOldIndex == nNewIndex) + return; + + GtkWidget* pPage = gtk_assistant_get_nth_page(m_pAssistant, nOldIndex); + + g_object_ref(pPage); + std::optional sTitle; + if (auto const title = gtk_assistant_get_page_title(m_pAssistant, pPage)) { + sTitle = title; + } + gtk_assistant_remove_page(m_pAssistant, nOldIndex); + gtk_assistant_insert_page(m_pAssistant, pPage, nNewIndex); + gtk_assistant_set_page_type(m_pAssistant, pPage, GTK_ASSISTANT_PAGE_CUSTOM); + gtk_assistant_set_page_title(m_pAssistant, pPage, sTitle ? sTitle->getStr() : nullptr); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_forall(GTK_CONTAINER(m_pSidebar), wrap_sidebar_label, nullptr); +#endif + g_object_unref(pPage); + } + + virtual weld::Container* append_page(const OUString& rIdent) override + { + disable_notify_events(); + + GtkWidget *pChild = gtk_grid_new(); + ::set_buildable_id(GTK_BUILDABLE(pChild), rIdent); + gtk_assistant_append_page(m_pAssistant, pChild); + gtk_assistant_set_page_type(m_pAssistant, pChild, GTK_ASSISTANT_PAGE_CUSTOM); + gtk_widget_show(pChild); + + enable_notify_events(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + m_aPages.emplace_back(new GtkInstanceContainer(GTK_CONTAINER(pChild), m_pBuilder, false)); +#else + m_aPages.emplace_back(new GtkInstanceContainer(pChild, m_pBuilder, false)); +#endif + + return m_aPages.back().get(); + } + + virtual void set_page_side_help_id(const OUString& rHelpId) override + { + if (!m_pSidebar) + return; + ::set_help_id(m_pSidebar, rHelpId); + } + + virtual GtkButton* get_widget_for_response(int nGtkResponse) override + { + GtkButton* pButton = nullptr; + if (nGtkResponse == GTK_RESPONSE_YES) + pButton = m_pNext; + else if (nGtkResponse == GTK_RESPONSE_NO) + pButton = m_pBack; + else if (nGtkResponse == GTK_RESPONSE_OK) + pButton = m_pFinish; + else if (nGtkResponse == GTK_RESPONSE_CANCEL) + pButton = m_pCancel; + else if (nGtkResponse == GTK_RESPONSE_HELP) + pButton = m_pHelp; + return pButton; + } + + virtual void set_page_side_image(const OUString& /*rImage*/) override + { + // Since GTK+ 3.2, sidebar images are not shown anymore + } + + virtual ~GtkInstanceAssistant() override + { + if (m_nButtonPressSignalId) + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pSidebarClickController, m_nButtonPressSignalId); +#else + g_signal_handler_disconnect(m_pSidebarEventBox, m_nButtonPressSignalId); +#endif + } + } +}; + +class GtkInstanceFrame : public GtkInstanceContainer, public virtual weld::Frame +{ +private: + GtkFrame* m_pFrame; +public: + GtkInstanceFrame(GtkFrame* pFrame, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) +#if !GTK_CHECK_VERSION(4, 0, 0) + : GtkInstanceContainer(GTK_CONTAINER(pFrame), pBuilder, bTakeOwnership) +#else + : GtkInstanceContainer(GTK_WIDGET(pFrame), pBuilder, bTakeOwnership) +#endif + , m_pFrame(pFrame) + { + } + + virtual void set_label(const OUString& rText) override + { + gtk_label_set_label(GTK_LABEL(gtk_frame_get_label_widget(m_pFrame)), rText.replaceFirst("~", "").toUtf8().getStr()); + } + + virtual OUString get_label() const override + { + const gchar* pStr = gtk_frame_get_label(m_pFrame); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual std::unique_ptr weld_label_widget() const override; +}; + +class GtkInstancePaned : public GtkInstanceContainer, public virtual weld::Paned +{ +private: + GtkPaned* m_pPaned; +public: + GtkInstancePaned(GtkPaned* pPaned, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) +#if !GTK_CHECK_VERSION(4, 0, 0) + : GtkInstanceContainer(GTK_CONTAINER(pPaned), pBuilder, bTakeOwnership) +#else + : GtkInstanceContainer(GTK_WIDGET(pPaned), pBuilder, bTakeOwnership) +#endif + , m_pPaned(pPaned) + { + } + + virtual void set_position(int nPos) override + { + gtk_paned_set_position(m_pPaned, nPos); + } + + virtual int get_position() const override + { + return gtk_paned_get_position(m_pPaned); + } +}; + +} + +static GType immobilized_viewport_get_type(); +static gpointer immobilized_viewport_parent_class; + +#ifndef NDEBUG +# define IMMOBILIZED_TYPE_VIEWPORT (immobilized_viewport_get_type()) +# define IMMOBILIZED_IS_VIEWPORT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), IMMOBILIZED_TYPE_VIEWPORT)) +#endif + +namespace { + +struct ImmobilizedViewportPrivate +{ + GtkAdjustment *hadjustment; + GtkAdjustment *vadjustment; +}; + +} + +#define IMMOBILIZED_VIEWPORT_PRIVATE_DATA "ImmobilizedViewportPrivateData" + +enum +{ + PROP_0, + PROP_HADJUSTMENT, + PROP_VADJUSTMENT, + PROP_HSCROLL_POLICY, + PROP_VSCROLL_POLICY, + PROP_SHADOW_TYPE +}; + +static void viewport_set_adjustment(GtkViewport *viewport, + GtkOrientation orientation, + GtkAdjustment *adjustment) +{ + ImmobilizedViewportPrivate* priv = + static_cast(g_object_get_data(G_OBJECT(viewport), + IMMOBILIZED_VIEWPORT_PRIVATE_DATA)); + + if (!adjustment) + adjustment = gtk_adjustment_new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + { + if (priv->hadjustment) + g_object_unref(priv->hadjustment); + priv->hadjustment = adjustment; + } + else + { + if (priv->vadjustment) + g_object_unref(priv->vadjustment); + priv->vadjustment = adjustment; + } + + g_object_ref_sink(adjustment); +} + +static void +immobilized_viewport_set_property(GObject* object, + guint prop_id, + const GValue* value, + GParamSpec* /*pspec*/) +{ + GtkViewport *viewport = GTK_VIEWPORT(object); + + switch (prop_id) + { + case PROP_HADJUSTMENT: + viewport_set_adjustment(viewport, GTK_ORIENTATION_HORIZONTAL, GTK_ADJUSTMENT(g_value_get_object(value))); + break; + case PROP_VADJUSTMENT: + viewport_set_adjustment(viewport, GTK_ORIENTATION_VERTICAL, GTK_ADJUSTMENT(g_value_get_object(value))); + break; + case PROP_HSCROLL_POLICY: + case PROP_VSCROLL_POLICY: + break; + default: + SAL_WARN( "vcl.gtk", "unknown property\n"); + break; + } +} + +static void +immobilized_viewport_get_property(GObject* object, + guint prop_id, + GValue* value, + GParamSpec* /*pspec*/) +{ + ImmobilizedViewportPrivate* priv = + static_cast(g_object_get_data(object, + IMMOBILIZED_VIEWPORT_PRIVATE_DATA)); + + switch (prop_id) + { + case PROP_HADJUSTMENT: + g_value_set_object(value, priv->hadjustment); + break; + case PROP_VADJUSTMENT: + g_value_set_object(value, priv->vadjustment); + break; + case PROP_HSCROLL_POLICY: + g_value_set_enum(value, GTK_SCROLL_MINIMUM); + break; + case PROP_VSCROLL_POLICY: + g_value_set_enum(value, GTK_SCROLL_MINIMUM); + break; + default: + SAL_WARN( "vcl.gtk", "unknown property\n"); + break; + } +} + +static ImmobilizedViewportPrivate* +immobilized_viewport_new_private_data() +{ + ImmobilizedViewportPrivate* priv = g_slice_new0(ImmobilizedViewportPrivate); + priv->hadjustment = nullptr; + priv->vadjustment = nullptr; + return priv; +} + +static void +immobilized_viewport_instance_init(GTypeInstance *instance, gpointer /*klass*/) +{ + GObject* object = G_OBJECT(instance); + g_object_set_data(object, IMMOBILIZED_VIEWPORT_PRIVATE_DATA, + immobilized_viewport_new_private_data()); +} + +static void +immobilized_viewport_finalize(GObject* object) +{ + void* priv = g_object_get_data(object, IMMOBILIZED_VIEWPORT_PRIVATE_DATA); + if (priv) + { + g_slice_free(ImmobilizedViewportPrivate, priv); + g_object_set_data(object, IMMOBILIZED_VIEWPORT_PRIVATE_DATA, nullptr); + } + G_OBJECT_CLASS(immobilized_viewport_parent_class)->finalize(object); +} + +static void immobilized_viewport_class_init(GtkWidgetClass* klass) +{ + immobilized_viewport_parent_class = g_type_class_peek_parent(klass); + + GObjectClass* o_class = G_OBJECT_CLASS(klass); + + /* GObject signals */ + o_class->finalize = immobilized_viewport_finalize; + o_class->set_property = immobilized_viewport_set_property; + o_class->get_property = immobilized_viewport_get_property; + + /* Properties */ + g_object_class_override_property(o_class, PROP_HADJUSTMENT, "hadjustment"); + g_object_class_override_property(o_class, PROP_VADJUSTMENT, "vadjustment"); + g_object_class_override_property(o_class, PROP_HSCROLL_POLICY, "hscroll-policy"); + g_object_class_override_property(o_class, PROP_VSCROLL_POLICY, "vscroll-policy"); +} + +GType immobilized_viewport_get_type() +{ + static GType type = 0; + + if (!type) + { + GTypeQuery query; + g_type_query(gtk_viewport_get_type(), &query); + + static const GTypeInfo tinfo = + { + static_cast(query.class_size), + nullptr, /* base init */ + nullptr, /* base finalize */ + reinterpret_cast(immobilized_viewport_class_init), /* class init */ + nullptr, /* class finalize */ + nullptr, /* class data */ + static_cast(query.instance_size), /* instance size */ + 0, /* nb preallocs */ + immobilized_viewport_instance_init, /* instance init */ + nullptr /* value table */ + }; + + type = g_type_register_static(GTK_TYPE_VIEWPORT, "ImmobilizedViewport", + &tinfo, GTypeFlags(0)); + } + + return type; +} + +static VclPolicyType GtkToVcl(GtkPolicyType eType) +{ + VclPolicyType eRet(VclPolicyType::NEVER); + switch (eType) + { + case GTK_POLICY_ALWAYS: + eRet = VclPolicyType::ALWAYS; + break; + case GTK_POLICY_AUTOMATIC: + eRet = VclPolicyType::AUTOMATIC; + break; + case GTK_POLICY_EXTERNAL: + case GTK_POLICY_NEVER: + eRet = VclPolicyType::NEVER; + break; + } + return eRet; +} + +static GtkPolicyType VclToGtk(VclPolicyType eType) +{ + GtkPolicyType eRet(GTK_POLICY_ALWAYS); + switch (eType) + { + case VclPolicyType::ALWAYS: + eRet = GTK_POLICY_ALWAYS; + break; + case VclPolicyType::AUTOMATIC: + eRet = GTK_POLICY_AUTOMATIC; + break; + case VclPolicyType::NEVER: + eRet = GTK_POLICY_NEVER; + break; + } + return eRet; +} + +static GtkMessageType VclToGtk(VclMessageType eType) +{ + GtkMessageType eRet(GTK_MESSAGE_INFO); + switch (eType) + { + case VclMessageType::Info: + eRet = GTK_MESSAGE_INFO; + break; + case VclMessageType::Warning: + eRet = GTK_MESSAGE_WARNING; + break; + case VclMessageType::Question: + eRet = GTK_MESSAGE_QUESTION; + break; + case VclMessageType::Error: + eRet = GTK_MESSAGE_ERROR; + break; + case VclMessageType::Other: + eRet = GTK_MESSAGE_OTHER; + break; + } + return eRet; +} + +static GtkButtonsType VclToGtk(VclButtonsType eType) +{ + GtkButtonsType eRet(GTK_BUTTONS_NONE); + switch (eType) + { + case VclButtonsType::NONE: + eRet = GTK_BUTTONS_NONE; + break; + case VclButtonsType::Ok: + eRet = GTK_BUTTONS_OK; + break; + case VclButtonsType::Close: + eRet = GTK_BUTTONS_CLOSE; + break; + case VclButtonsType::Cancel: + eRet = GTK_BUTTONS_CANCEL; + break; + case VclButtonsType::YesNo: + eRet = GTK_BUTTONS_YES_NO; + break; + case VclButtonsType::OkCancel: + eRet = GTK_BUTTONS_OK_CANCEL; + break; + } + return eRet; +} + +static GtkSelectionMode VclToGtk(SelectionMode eType) +{ + GtkSelectionMode eRet(GTK_SELECTION_NONE); + switch (eType) + { + case SelectionMode::NONE: + eRet = GTK_SELECTION_NONE; + break; + case SelectionMode::Single: + eRet = GTK_SELECTION_SINGLE; + break; + case SelectionMode::Range: + eRet = GTK_SELECTION_BROWSE; + break; + case SelectionMode::Multiple: + eRet = GTK_SELECTION_MULTIPLE; + break; + } + return eRet; +} + +namespace { + +class GtkInstanceScrolledWindow final : public GtkInstanceContainer, public virtual weld::ScrolledWindow +{ +private: + GtkScrolledWindow* m_pScrolledWindow; + GtkWidget *m_pOrigViewport; + GtkCssProvider* m_pScrollBarCssProvider; + GtkAdjustment* m_pVAdjustment; + GtkAdjustment* m_pHAdjustment; + gulong m_nVAdjustChangedSignalId; + gulong m_nHAdjustChangedSignalId; + + static void signalVAdjustValueChanged(GtkAdjustment*, gpointer widget) + { + GtkInstanceScrolledWindow* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_vadjustment_changed(); + } + + static void signalHAdjustValueChanged(GtkAdjustment*, gpointer widget) + { + GtkInstanceScrolledWindow* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_hadjustment_changed(); + } + +public: + GtkInstanceScrolledWindow(GtkScrolledWindow* pScrolledWindow, GtkInstanceBuilder* pBuilder, bool bTakeOwnership, bool bUserManagedScrolling) +#if !GTK_CHECK_VERSION(4, 0, 0) + : GtkInstanceContainer(GTK_CONTAINER(pScrolledWindow), pBuilder, bTakeOwnership) +#else + : GtkInstanceContainer(GTK_WIDGET(pScrolledWindow), pBuilder, bTakeOwnership) +#endif + , m_pScrolledWindow(pScrolledWindow) + , m_pOrigViewport(nullptr) + , m_pScrollBarCssProvider(nullptr) + , m_pVAdjustment(gtk_scrolled_window_get_vadjustment(m_pScrolledWindow)) + , m_pHAdjustment(gtk_scrolled_window_get_hadjustment(m_pScrolledWindow)) + , m_nVAdjustChangedSignalId(g_signal_connect(m_pVAdjustment, "value-changed", G_CALLBACK(signalVAdjustValueChanged), this)) + , m_nHAdjustChangedSignalId(g_signal_connect(m_pHAdjustment, "value-changed", G_CALLBACK(signalHAdjustValueChanged), this)) + { + if (bUserManagedScrolling) + set_user_managed_scrolling(); + } + + void set_user_managed_scrolling() + { + disable_notify_events(); + //remove the original viewport and replace it with our bodged one which + //doesn't do any scrolling and expects its child to figure it out somehow + assert(!m_pOrigViewport); +#if GTK_CHECK_VERSION(4, 0, 0) + GtkWidget *pViewport = gtk_scrolled_window_get_child(m_pScrolledWindow); +#else + GtkWidget *pViewport = gtk_bin_get_child(GTK_BIN(m_pScrolledWindow)); +#endif + assert(GTK_IS_VIEWPORT(pViewport)); +#if GTK_CHECK_VERSION(4, 0, 0) + GtkWidget *pChild= gtk_viewport_get_child(GTK_VIEWPORT(pViewport)); +#else + GtkWidget *pChild = gtk_bin_get_child(GTK_BIN(pViewport)); +#endif + g_object_ref(pChild); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_viewport_set_child(GTK_VIEWPORT(pViewport), nullptr); +#else + gtk_container_remove(GTK_CONTAINER(pViewport), pChild); +#endif + g_object_ref(pViewport); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_scrolled_window_set_child(m_pScrolledWindow, nullptr); +#else + gtk_container_remove(GTK_CONTAINER(m_pScrolledWindow), pViewport); +#endif + GtkWidget* pNewViewport = GTK_WIDGET(g_object_new(immobilized_viewport_get_type(), nullptr)); + gtk_widget_show(pNewViewport); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_scrolled_window_set_child(m_pScrolledWindow, pNewViewport); + gtk_viewport_set_child(GTK_VIEWPORT(pNewViewport), pChild); +#else + gtk_container_add(GTK_CONTAINER(m_pScrolledWindow), pNewViewport); + gtk_container_add(GTK_CONTAINER(pNewViewport), pChild); +#endif + g_object_unref(pChild); + m_pOrigViewport = pViewport; + enable_notify_events(); + } + + virtual void hadjustment_configure(int value, int lower, int upper, + int step_increment, int page_increment, + int page_size) override + { + disable_notify_events(); + if (SwapForRTL()) + value = upper - (value - lower + page_size); + gtk_adjustment_configure(m_pHAdjustment, value, lower, upper, step_increment, page_increment, page_size); + enable_notify_events(); + } + + virtual int hadjustment_get_value() const override + { + int value = gtk_adjustment_get_value(m_pHAdjustment); + + if (SwapForRTL()) + { + int upper = gtk_adjustment_get_upper(m_pHAdjustment); + int lower = gtk_adjustment_get_lower(m_pHAdjustment); + int page_size = gtk_adjustment_get_page_size(m_pHAdjustment); + value = lower + (upper - value - page_size); + } + + return value; + } + + virtual void hadjustment_set_value(int value) override + { + disable_notify_events(); + + if (SwapForRTL()) + { + int upper = gtk_adjustment_get_upper(m_pHAdjustment); + int lower = gtk_adjustment_get_lower(m_pHAdjustment); + int page_size = gtk_adjustment_get_page_size(m_pHAdjustment); + value = upper - (value - lower + page_size); + } + + gtk_adjustment_set_value(m_pHAdjustment, value); + enable_notify_events(); + } + + virtual int hadjustment_get_upper() const override + { + return gtk_adjustment_get_upper(m_pHAdjustment); + } + + virtual void hadjustment_set_upper(int upper) override + { + disable_notify_events(); + gtk_adjustment_set_upper(m_pHAdjustment, upper); + enable_notify_events(); + } + + virtual int hadjustment_get_page_size() const override + { + return gtk_adjustment_get_page_size(m_pHAdjustment); + } + + virtual void hadjustment_set_page_size(int size) override + { + gtk_adjustment_set_page_size(m_pHAdjustment, size); + } + + virtual void hadjustment_set_page_increment(int size) override + { + gtk_adjustment_set_page_increment(m_pHAdjustment, size); + } + + virtual void hadjustment_set_step_increment(int size) override + { + gtk_adjustment_set_step_increment(m_pHAdjustment, size); + } + + virtual void set_hpolicy(VclPolicyType eHPolicy) override + { + GtkPolicyType eGtkVPolicy; + gtk_scrolled_window_get_policy(m_pScrolledWindow, nullptr, &eGtkVPolicy); + gtk_scrolled_window_set_policy(m_pScrolledWindow, VclToGtk(eHPolicy), eGtkVPolicy); + } + + virtual VclPolicyType get_hpolicy() const override + { + GtkPolicyType eGtkHPolicy; + gtk_scrolled_window_get_policy(m_pScrolledWindow, &eGtkHPolicy, nullptr); + return GtkToVcl(eGtkHPolicy); + } + + virtual void vadjustment_configure(int value, int lower, int upper, + int step_increment, int page_increment, + int page_size) override + { + disable_notify_events(); + gtk_adjustment_configure(m_pVAdjustment, value, lower, upper, step_increment, page_increment, page_size); + enable_notify_events(); + } + + virtual int vadjustment_get_value() const override + { + return gtk_adjustment_get_value(m_pVAdjustment); + } + + virtual void vadjustment_set_value(int value) override + { + disable_notify_events(); + gtk_adjustment_set_value(m_pVAdjustment, value); + enable_notify_events(); + } + + virtual int vadjustment_get_upper() const override + { + return gtk_adjustment_get_upper(m_pVAdjustment); + } + + virtual void vadjustment_set_upper(int upper) override + { + disable_notify_events(); + gtk_adjustment_set_upper(m_pVAdjustment, upper); + enable_notify_events(); + } + + virtual int vadjustment_get_lower() const override + { + return gtk_adjustment_get_lower(m_pVAdjustment); + } + + virtual void vadjustment_set_lower(int lower) override + { + disable_notify_events(); + gtk_adjustment_set_lower(m_pVAdjustment, lower); + enable_notify_events(); + } + + virtual int vadjustment_get_page_size() const override + { + return gtk_adjustment_get_page_size(m_pVAdjustment); + } + + virtual void vadjustment_set_page_size(int size) override + { + gtk_adjustment_set_page_size(m_pVAdjustment, size); + } + + virtual void vadjustment_set_page_increment(int size) override + { + gtk_adjustment_set_page_increment(m_pVAdjustment, size); + } + + virtual void vadjustment_set_step_increment(int size) override + { + gtk_adjustment_set_step_increment(m_pVAdjustment, size); + } + + virtual void set_vpolicy(VclPolicyType eVPolicy) override + { + GtkPolicyType eGtkHPolicy; + gtk_scrolled_window_get_policy(m_pScrolledWindow, &eGtkHPolicy, nullptr); + gtk_scrolled_window_set_policy(m_pScrolledWindow, eGtkHPolicy, VclToGtk(eVPolicy)); + } + + virtual VclPolicyType get_vpolicy() const override + { + GtkPolicyType eGtkVPolicy; + gtk_scrolled_window_get_policy(m_pScrolledWindow, nullptr, &eGtkVPolicy); + return GtkToVcl(eGtkVPolicy); + } + + virtual int get_scroll_thickness() const override + { + if (gtk_scrolled_window_get_overlay_scrolling(m_pScrolledWindow)) + return 0; + GtkRequisition size; + gtk_widget_get_preferred_size(gtk_scrolled_window_get_vscrollbar(m_pScrolledWindow), nullptr, &size); + return size.width; + } + + virtual void set_scroll_thickness(int nThickness) override + { + GtkWidget *pHorzBar = gtk_scrolled_window_get_hscrollbar(m_pScrolledWindow); + GtkWidget *pVertBar = gtk_scrolled_window_get_vscrollbar(m_pScrolledWindow); + gtk_widget_set_size_request(pHorzBar, -1, nThickness); + gtk_widget_set_size_request(pVertBar, nThickness, -1); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pVAdjustment, m_nVAdjustChangedSignalId); + g_signal_handler_block(m_pHAdjustment, m_nHAdjustChangedSignalId); + GtkInstanceContainer::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceContainer::enable_notify_events(); + g_signal_handler_unblock(m_pVAdjustment, m_nVAdjustChangedSignalId); + g_signal_handler_unblock(m_pHAdjustment, m_nHAdjustChangedSignalId); + } + + virtual void customize_scrollbars(const Color& rBackgroundColor, + const Color& rShadowColor, + const Color& rFaceColor) override + { + GtkWidget *pHorzBar = gtk_scrolled_window_get_hscrollbar(m_pScrolledWindow); + GtkWidget *pVertBar = gtk_scrolled_window_get_vscrollbar(m_pScrolledWindow); + GtkStyleContext *pHorzContext = gtk_widget_get_style_context(pHorzBar); + GtkStyleContext *pVertContext = gtk_widget_get_style_context(pVertBar); + if (m_pScrollBarCssProvider) + { + gtk_style_context_remove_provider(pHorzContext, GTK_STYLE_PROVIDER(m_pScrollBarCssProvider)); + gtk_style_context_remove_provider(pVertContext, GTK_STYLE_PROVIDER(m_pScrollBarCssProvider)); + } + + m_pScrollBarCssProvider = gtk_css_provider_new(); + // intentionally 'trough' a long, narrow open container. + OUString aBuffer = "scrollbar contents trough { background-color: #" + rBackgroundColor.AsRGBHexString() + "; } " + "scrollbar contents trough slider { background-color: #" + rShadowColor.AsRGBHexString() + "; } " + "scrollbar contents button { background-color: #" + rFaceColor.AsRGBHexString() + "; } " + "scrollbar contents button { color: #000000; } " + "scrollbar contents button:disabled { color: #7f7f7f; }"; + OString aResult = OUStringToOString(aBuffer, RTL_TEXTENCODING_UTF8); + css_provider_load_from_data(m_pScrollBarCssProvider, aResult.getStr(), aResult.getLength()); + + gtk_style_context_add_provider(pHorzContext, GTK_STYLE_PROVIDER(m_pScrollBarCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + gtk_style_context_add_provider(pVertContext, GTK_STYLE_PROVIDER(m_pScrollBarCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + + virtual ~GtkInstanceScrolledWindow() override + { + // we use GtkInstanceContainer::[disable|enable]_notify_events later on + // to avoid touching these removed handlers + g_signal_handler_disconnect(m_pVAdjustment, m_nVAdjustChangedSignalId); + g_signal_handler_disconnect(m_pHAdjustment, m_nHAdjustChangedSignalId); + + if (m_pScrollBarCssProvider) + { + GtkStyleContext *pHorzContext = gtk_widget_get_style_context(gtk_scrolled_window_get_hscrollbar(m_pScrolledWindow)); + GtkStyleContext *pVertContext = gtk_widget_get_style_context(gtk_scrolled_window_get_vscrollbar(m_pScrolledWindow)); + gtk_style_context_remove_provider(pHorzContext, GTK_STYLE_PROVIDER(m_pScrollBarCssProvider)); + gtk_style_context_remove_provider(pVertContext, GTK_STYLE_PROVIDER(m_pScrollBarCssProvider)); + m_pScrollBarCssProvider = nullptr; + } + + //put it back the way it was + if (!m_pOrigViewport) + return; + + GtkInstanceContainer::disable_notify_events(); + + // force in new adjustment to drop the built-in handlers on value-changed + // which are getting called eventually by the gtk_container_add call + // and which access the scrolled window indicators which, in the case + // of user-managed scrolling windows in toolbar popups during popdown + // are nullptr causing crashes when the scrolling windows is not at its + // initial 0,0 position + GtkAdjustment *pVAdjustment = gtk_adjustment_new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + gtk_scrolled_window_set_vadjustment(m_pScrolledWindow, pVAdjustment); + GtkAdjustment *pHAdjustment = gtk_adjustment_new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + gtk_scrolled_window_set_hadjustment(m_pScrolledWindow, pHAdjustment); + +#if GTK_CHECK_VERSION(4, 0, 0) + GtkWidget *pViewport = gtk_scrolled_window_get_child(m_pScrolledWindow); +#else + GtkWidget *pViewport = gtk_bin_get_child(GTK_BIN(m_pScrolledWindow)); +#endif + assert(IMMOBILIZED_IS_VIEWPORT(pViewport)); +#if GTK_CHECK_VERSION(4, 0, 0) + GtkWidget *pChild= gtk_viewport_get_child(GTK_VIEWPORT(pViewport)); +#else + GtkWidget *pChild = gtk_bin_get_child(GTK_BIN(pViewport)); +#endif + g_object_ref(pChild); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_viewport_set_child(GTK_VIEWPORT(pViewport), nullptr); +#else + gtk_container_remove(GTK_CONTAINER(pViewport), pChild); +#endif + g_object_ref(pViewport); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_scrolled_window_set_child(m_pScrolledWindow, nullptr); +#else + gtk_container_remove(GTK_CONTAINER(m_pScrolledWindow), pViewport); +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_scrolled_window_set_child(m_pScrolledWindow, m_pOrigViewport); +#else + gtk_container_add(GTK_CONTAINER(m_pScrolledWindow), m_pOrigViewport); +#endif + // coverity[freed_arg : FALSE] - this does not free m_pOrigViewport, it is reffed by m_pScrolledWindow + g_object_unref(m_pOrigViewport); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_viewport_set_child(GTK_VIEWPORT(m_pOrigViewport), pChild); +#else + gtk_container_add(GTK_CONTAINER(m_pOrigViewport), pChild); +#endif + g_object_unref(pChild); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_destroy(pViewport); +#endif + g_object_unref(pViewport); + m_pOrigViewport = nullptr; + GtkInstanceContainer::enable_notify_events(); + } +}; + +class GtkInstanceScrollbar final : public GtkInstanceWidget, public virtual weld::Scrollbar +{ +private: + GtkScrollbar* m_pScrollbar; + GtkAdjustment* m_pAdjustment; + GtkCssProvider* m_pThicknessCssProvider; + gulong m_nAdjustChangedSignalId; + + static void signalAdjustValueChanged(GtkAdjustment*, gpointer widget) + { + GtkInstanceScrollbar* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_adjustment_changed(); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + // if the widget is inside a GtkSalFrame then ensure the event is processed by the GtkSalFrame and not the + // GtkScrollbar + static gboolean signalScroll(GtkEventControllerScroll* pController, double delta_x, double delta_y, gpointer widget) + { + GtkInstanceScrollbar* pThis = static_cast(widget); + + GtkWidget* pParent = widget_get_toplevel(GTK_WIDGET(pThis->m_pScrollbar)); + GtkSalFrame* pFrame = pParent ? GtkSalFrame::getFromWindow(pParent) : nullptr; + + return pFrame && pFrame->event_controller_scroll_forward(pController, delta_x, delta_y); + } +#else + static gboolean signalScroll(GtkWidget* pWidget, GdkEventScroll* /*pEvent*/, gpointer widget) + { + GtkInstanceScrollbar* pThis = static_cast(widget); + + GtkWidget* pParent = widget_get_toplevel(GTK_WIDGET(pThis->m_pScrollbar)); + GtkSalFrame* pFrame = pParent ? GtkSalFrame::getFromWindow(pParent) : nullptr; + + if (pFrame) + g_signal_stop_emission_by_name(pWidget, "scroll-event"); + + return false; + } +#endif + +public: + GtkInstanceScrollbar(GtkScrollbar* pScrollbar, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pScrollbar), pBuilder, bTakeOwnership) + , m_pScrollbar(pScrollbar) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_pAdjustment(gtk_scrollbar_get_adjustment(m_pScrollbar)) +#else + , m_pAdjustment(gtk_range_get_adjustment(GTK_RANGE(m_pScrollbar))) +#endif + , m_pThicknessCssProvider(nullptr) + , m_nAdjustChangedSignalId(g_signal_connect(m_pAdjustment, "value-changed", G_CALLBACK(signalAdjustValueChanged), this)) + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController* pScrollController = gtk_event_controller_scroll_new(GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES); + gtk_event_controller_set_propagation_phase(pScrollController, GTK_PHASE_CAPTURE); + g_signal_connect(pScrollController, "scroll", G_CALLBACK(signalScroll), this); + gtk_widget_add_controller(GTK_WIDGET(pScrollbar), pScrollController); +#else + g_signal_connect(pScrollbar, "scroll-event", G_CALLBACK(signalScroll), this); +#endif + } + + virtual void adjustment_configure(int value, int lower, int upper, + int step_increment, int page_increment, + int page_size) override + { + disable_notify_events(); + gtk_adjustment_configure(m_pAdjustment, value, lower, upper, step_increment, page_increment, page_size); + enable_notify_events(); + } + + virtual int adjustment_get_value() const override + { + return gtk_adjustment_get_value(m_pAdjustment); + } + + virtual void adjustment_set_value(int value) override + { + disable_notify_events(); + gtk_adjustment_set_value(m_pAdjustment, value); + enable_notify_events(); + } + + virtual int adjustment_get_upper() const override + { + return gtk_adjustment_get_upper(m_pAdjustment); + } + + virtual void adjustment_set_upper(int upper) override + { + disable_notify_events(); + gtk_adjustment_set_upper(m_pAdjustment, upper); + enable_notify_events(); + } + + virtual int adjustment_get_lower() const override + { + return gtk_adjustment_get_lower(m_pAdjustment); + } + + virtual void adjustment_set_lower(int lower) override + { + disable_notify_events(); + gtk_adjustment_set_lower(m_pAdjustment, lower); + enable_notify_events(); + } + + virtual int adjustment_get_page_size() const override + { + return gtk_adjustment_get_page_size(m_pAdjustment); + } + + virtual void adjustment_set_page_size(int size) override + { + gtk_adjustment_set_page_size(m_pAdjustment, size); + } + + virtual int adjustment_get_page_increment() const override + { + return gtk_adjustment_get_page_increment(m_pAdjustment); + } + + virtual void adjustment_set_page_increment(int size) override + { + gtk_adjustment_set_page_increment(m_pAdjustment, size); + } + + virtual int adjustment_get_step_increment() const override + { + return gtk_adjustment_get_step_increment(m_pAdjustment); + } + + virtual void adjustment_set_step_increment(int size) override + { + gtk_adjustment_set_step_increment(m_pAdjustment, size); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pAdjustment, m_nAdjustChangedSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pAdjustment, m_nAdjustChangedSignalId); + } + + virtual ScrollType get_scroll_type() const override + { + // tdf#153049 want a mousewheel spin to be treated as DontKnow + return has_grab() ? ScrollType::Drag : ScrollType::DontKnow; + } + + virtual int get_scroll_thickness() const override + { + if (gtk_orientable_get_orientation(GTK_ORIENTABLE(m_pScrollbar)) == GTK_ORIENTATION_HORIZONTAL) + return gtk_widget_get_allocated_height(GTK_WIDGET(m_pScrollbar)); + return gtk_widget_get_allocated_width(GTK_WIDGET(m_pScrollbar)); + } + + virtual void set_scroll_thickness(int nThickness) override + { + GtkStyleContext *pStyleContext = gtk_widget_get_style_context(GTK_WIDGET(m_pScrollbar)); + + if (m_pThicknessCssProvider) + { + gtk_style_context_remove_provider(pStyleContext, GTK_STYLE_PROVIDER(m_pThicknessCssProvider)); + m_pThicknessCssProvider = nullptr; + } + + m_pThicknessCssProvider = gtk_css_provider_new(); + int nSlider = nThickness > 6 ? nThickness - 6 : 1; + const OString sData = "slider { min-height: " + OString::number(nSlider) + "px;" + " min-width: " + OString::number(nSlider) + "px; }"; + css_provider_load_from_data(m_pThicknessCssProvider, sData.getStr(), sData.getLength()); + gtk_style_context_add_provider(pStyleContext, GTK_STYLE_PROVIDER(m_pThicknessCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + if (gtk_orientable_get_orientation(GTK_ORIENTABLE(m_pScrollbar)) == GTK_ORIENTATION_HORIZONTAL) + gtk_widget_set_size_request(GTK_WIDGET(m_pScrollbar), -1, nThickness); + else + gtk_widget_set_size_request(GTK_WIDGET(m_pScrollbar), nThickness, -1); + } + + virtual ~GtkInstanceScrollbar() override + { + g_signal_handler_disconnect(m_pAdjustment, m_nAdjustChangedSignalId); + if (m_pThicknessCssProvider) + { + GtkStyleContext *pStyleContext = gtk_widget_get_style_context(GTK_WIDGET(m_pScrollbar)); + gtk_style_context_remove_provider(pStyleContext, GTK_STYLE_PROVIDER(m_pThicknessCssProvider)); + } + } +}; + +} + +namespace { + +class GtkInstanceNotebook : public GtkInstanceWidget, public virtual weld::Notebook +{ +private: + GtkNotebook* m_pNotebook; + GtkBox* m_pOverFlowBox; + GtkNotebook* m_pOverFlowNotebook; + gulong m_nSwitchPageSignalId; + gulong m_nOverFlowSwitchPageSignalId; +#if GTK_CHECK_VERSION(4, 0, 0) + NotifyingLayout* m_pLayout; +#else + gulong m_nNotebookSizeAllocateSignalId; + gulong m_nFocusSignalId; +#endif + gulong m_nChangeCurrentPageId; + guint m_nLaunchSplitTimeoutId; + bool m_bOverFlowBoxActive; + bool m_bOverFlowBoxIsStart; + bool m_bInternalPageChange; + int m_nStartTabCount; + int m_nEndTabCount; + mutable std::vector> m_aPages; + + static void signalSwitchPage(GtkNotebook*, GtkWidget*, guint nNewPage, gpointer widget) + { + GtkInstanceNotebook* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_switch_page(nNewPage); + } + + static gboolean launch_overflow_switch_page(GtkInstanceNotebook* pThis) + { + SolarMutexGuard aGuard; + pThis->signal_overflow_switch_page(); + return false; + } + + static void signalOverFlowSwitchPage(GtkNotebook*, GtkWidget*, guint, gpointer widget) + { + g_timeout_add_full(G_PRIORITY_HIGH_IDLE, 0, reinterpret_cast(launch_overflow_switch_page), widget, nullptr); + } + + void signal_switch_page(int nNewPage) + { + if (m_bOverFlowBoxIsStart) + { + auto nOverFlowLen = m_bOverFlowBoxActive ? gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1 : 0; + // add count of overflow pages, minus the extra tab + nNewPage += nOverFlowLen; + } + + bool bAllow = m_bInternalPageChange || !m_aLeavePageHdl.IsSet() || m_aLeavePageHdl.Call(get_current_page_ident()); + if (!bAllow) + { + g_signal_stop_emission_by_name(m_pNotebook, "switch-page"); + return; + } + if (m_bOverFlowBoxActive) + gtk_notebook_set_current_page(m_pOverFlowNotebook, gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1); + OUString sNewIdent(get_page_ident(nNewPage)); + if (!m_bInternalPageChange) + m_aEnterPageHdl.Call(sNewIdent); + } + + void unsplit_notebooks() + { + int nOverFlowPages = gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1; + int nMainPages = gtk_notebook_get_n_pages(m_pNotebook); + int nPageIndex = 0; + if (!m_bOverFlowBoxIsStart) + nPageIndex += nMainPages; + + // take the overflow pages, and put them back at the end of the normal one + int i = nMainPages; + while (nOverFlowPages) + { + OUString sIdent(get_page_ident(m_pOverFlowNotebook, 0)); + OUString sLabel(get_tab_label_text(m_pOverFlowNotebook, 0)); + remove_page(m_pOverFlowNotebook, sIdent); + + GtkWidget* pPage = m_aPages[nPageIndex]->getWidget(); + insert_page(m_pNotebook, sIdent, sLabel, pPage, -1); + + GtkWidget* pTabWidget = gtk_notebook_get_tab_label(m_pNotebook, + gtk_notebook_get_nth_page(m_pNotebook, i)); + gtk_widget_set_hexpand(pTabWidget, true); + --nOverFlowPages; + ++i; + ++nPageIndex; + } + + // remove the dangling placeholder tab page + remove_page(m_pOverFlowNotebook, u"useless"); + } + + // a tab has been selected on the overflow notebook + void signal_overflow_switch_page() + { + int nNewPage = gtk_notebook_get_current_page(m_pOverFlowNotebook); + int nOverFlowPages = gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1; + if (nNewPage == nOverFlowPages) + { + // the useless tab which is there because there has to be an active tab + return; + } + + // check if we are allowed leave before attempting to resplit the notebooks + bool bAllow = !m_aLeavePageHdl.IsSet() || m_aLeavePageHdl.Call(get_current_page_ident()); + if (!bAllow) + return; + + disable_notify_events(); + + // take the overflow pages, and put them back at the end of the normal one + unsplit_notebooks(); + + // now redo the split, the pages will be split the other way around this time + std::swap(m_nStartTabCount, m_nEndTabCount); + split_notebooks(); + + // coverity[pass_freed_arg : FALSE] - m_pNotebook is not freed here + gtk_notebook_set_current_page(m_pNotebook, nNewPage); + + enable_notify_events(); + + // trigger main notebook switch-page callback + OUString sNewIdent(get_page_ident(m_pNotebook, nNewPage)); + m_aEnterPageHdl.Call(sNewIdent); + } + + static OUString get_page_ident(GtkNotebook *pNotebook, guint nPage) + { + const GtkWidget* pTabWidget = gtk_notebook_get_tab_label(pNotebook, gtk_notebook_get_nth_page(pNotebook, nPage)); + return ::get_buildable_id(GTK_BUILDABLE(pTabWidget)); + } + + static gint get_page_number(GtkNotebook *pNotebook, std::u16string_view ident) + { + gint nPages = gtk_notebook_get_n_pages(pNotebook); + for (gint i = 0; i < nPages; ++i) + { + const GtkWidget* pTabWidget = gtk_notebook_get_tab_label(pNotebook, gtk_notebook_get_nth_page(pNotebook, i)); + OUString sBuildableName = ::get_buildable_id(GTK_BUILDABLE(pTabWidget)); + if (sBuildableName == ident) + return i; + } + return -1; + } + + int remove_page(GtkNotebook *pNotebook, std::u16string_view ident) + { + disable_notify_events(); + int nPageNumber = get_page_number(pNotebook, ident); + assert(nPageNumber != -1 && "asked to remove page that doesn't exist"); + gtk_notebook_remove_page(pNotebook, nPageNumber); + enable_notify_events(); + return nPageNumber; + } + + static OUString get_tab_label_text(GtkNotebook *pNotebook, guint nPage) + { + const gchar* pStr = gtk_notebook_get_tab_label_text(pNotebook, gtk_notebook_get_nth_page(pNotebook, nPage)); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + static void set_tab_label_text(GtkNotebook *pNotebook, guint nPage, const OUString& rText) + { + OString sUtf8(rText.toUtf8()); + + GtkWidget* pPage = gtk_notebook_get_nth_page(pNotebook, nPage); + + // tdf#128241 if there's already a label here, reuse it so the buildable + // name remains the same, gtk_notebook_set_tab_label_text will replace + // the label widget with a new one + GtkWidget* pTabWidget = gtk_notebook_get_tab_label(pNotebook, pPage); + if (pTabWidget && GTK_IS_LABEL(pTabWidget)) + { + gtk_label_set_label(GTK_LABEL(pTabWidget), sUtf8.getStr()); + return; + } + + gtk_notebook_set_tab_label_text(pNotebook, pPage, sUtf8.getStr()); + } + + void append_useless_page(GtkNotebook *pNotebook) + { + disable_notify_events(); + + GtkWidget *pTabWidget = gtk_fixed_new(); + ::set_buildable_id(GTK_BUILDABLE(pTabWidget), "useless"); + + GtkWidget *pChild = gtk_grid_new(); + gtk_notebook_append_page(pNotebook, pChild, pTabWidget); + gtk_widget_show(pChild); + gtk_widget_show(pTabWidget); + + enable_notify_events(); + } + + void insert_page(GtkNotebook *pNotebook, const OUString& rIdent, const OUString& rLabel, GtkWidget *pChild, int nPos) + { + disable_notify_events(); + + GtkWidget *pTabWidget = gtk_label_new_with_mnemonic(MapToGtkAccelerator(rLabel).getStr()); + ::set_buildable_id(GTK_BUILDABLE(pTabWidget), rIdent); + gtk_notebook_insert_page(pNotebook, pChild, pTabWidget, nPos); + gtk_widget_show(pChild); + gtk_widget_show(pTabWidget); + + if (nPos != -1) + { + unsigned int nPageIndex = static_cast(nPos); + if (nPageIndex < m_aPages.size()) + m_aPages.insert(m_aPages.begin() + nPageIndex, nullptr); + } + + enable_notify_events(); + } + + void make_overflow_boxes() + { + m_pOverFlowBox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0)); + GtkWidget* pParent = gtk_widget_get_parent(GTK_WIDGET(m_pNotebook)); + container_add(pParent, GTK_WIDGET(m_pOverFlowBox)); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_box_append(m_pOverFlowBox, GTK_WIDGET(m_pOverFlowNotebook)); +#else + gtk_box_pack_start(m_pOverFlowBox, GTK_WIDGET(m_pOverFlowNotebook), false, false, 0); +#endif + g_object_ref(m_pNotebook); + container_remove(pParent, GTK_WIDGET(m_pNotebook)); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_box_append(m_pOverFlowBox, GTK_WIDGET(m_pNotebook)); +#else + gtk_box_pack_start(m_pOverFlowBox, GTK_WIDGET(m_pNotebook), true, true, 0); +#endif + // coverity[freed_arg : FALSE] - this does not free m_pNotebook , it is reffed by pParent + g_object_unref(m_pNotebook); + gtk_widget_show(GTK_WIDGET(m_pOverFlowBox)); + } + + void split_notebooks() + { + // get the original preferred size for the notebook, the sane width + // expected here depends on the notebooks all initially having + // scrollable tabs enabled + GtkAllocation alloc; + gtk_widget_get_allocation(GTK_WIDGET(m_pNotebook), &alloc); + + // toggle the direction of the split since the last time + m_bOverFlowBoxIsStart = !m_bOverFlowBoxIsStart; + if (!m_pOverFlowBox) + make_overflow_boxes(); + + // don't scroll the tabs anymore + // coverity[pass_freed_arg : FALSE] - m_pNotebook is not freed here + gtk_notebook_set_scrollable(m_pNotebook, false); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_freeze_child_notify(GTK_WIDGET(m_pNotebook)); + gtk_widget_freeze_child_notify(GTK_WIDGET(m_pOverFlowNotebook)); +#else + g_object_freeze_notify(G_OBJECT(m_pNotebook)); + g_object_freeze_notify(G_OBJECT(m_pOverFlowNotebook)); +#endif + + gtk_widget_show(GTK_WIDGET(m_pOverFlowNotebook)); + + gint nPages; + + GtkRequisition size1, size2; + + if (!m_nStartTabCount && !m_nEndTabCount) + { + nPages = gtk_notebook_get_n_pages(m_pNotebook); + + std::vector aLabelWidths; + //move tabs to the overflow notebook + for (int i = 0; i < nPages; ++i) + { + OUString sLabel(get_tab_label_text(m_pNotebook, i)); + aLabelWidths.push_back(get_pixel_size(sLabel).Width()); + } + int row_width = std::accumulate(aLabelWidths.begin(), aLabelWidths.end(), 0) / 2; + int count = 0; + for (int i = 0; i < nPages; ++i) + { + count += aLabelWidths[i]; + if (count >= row_width) + { + m_nStartTabCount = i; + break; + } + } + + m_nEndTabCount = nPages - m_nStartTabCount; + } + + //move the tabs to the overflow notebook + int i = 0; + int nOverFlowPages = m_nStartTabCount; + while (nOverFlowPages) + { + OUString sIdent(get_page_ident(m_pNotebook, 0)); + OUString sLabel(get_tab_label_text(m_pNotebook, 0)); + remove_page(m_pNotebook, sIdent); + insert_page(m_pOverFlowNotebook, sIdent, sLabel, gtk_grid_new(), -1); + GtkWidget* pTabWidget = gtk_notebook_get_tab_label(m_pOverFlowNotebook, + gtk_notebook_get_nth_page(m_pOverFlowNotebook, i)); + gtk_widget_set_hexpand(pTabWidget, true); + + --nOverFlowPages; + ++i; + } + + for (i = 0; i < m_nEndTabCount; ++i) + { + GtkWidget* pTabWidget = gtk_notebook_get_tab_label(m_pNotebook, + gtk_notebook_get_nth_page(m_pNotebook, i)); + gtk_widget_set_hexpand(pTabWidget, true); + } + + // have to have some tab as the active tab of the overflow notebook + append_useless_page(m_pOverFlowNotebook); + gtk_notebook_set_current_page(m_pOverFlowNotebook, gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1); + if (gtk_widget_has_focus(GTK_WIDGET(m_pOverFlowNotebook))) + gtk_widget_grab_focus(GTK_WIDGET(m_pNotebook)); + + // add this temporarily to the normal notebook to measure how wide + // the row would be if switched to the other notebook + append_useless_page(m_pNotebook); + + gtk_widget_get_preferred_size(GTK_WIDGET(m_pNotebook), nullptr, &size1); + gtk_widget_get_preferred_size(GTK_WIDGET(m_pOverFlowNotebook), nullptr, &size2); + + auto nWidth = std::max(size1.width, size2.width); + gtk_widget_set_size_request(GTK_WIDGET(m_pNotebook), nWidth, alloc.height); + gtk_widget_set_size_request(GTK_WIDGET(m_pOverFlowNotebook), nWidth, -1); + + // remove it once we've measured it + remove_page(m_pNotebook, u"useless"); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_thaw_child_notify(GTK_WIDGET(m_pOverFlowNotebook)); + gtk_widget_thaw_child_notify(GTK_WIDGET(m_pNotebook)); +#else + g_object_thaw_notify(G_OBJECT(m_pOverFlowNotebook)); + g_object_thaw_notify(G_OBJECT(m_pNotebook)); +#endif + + m_bOverFlowBoxActive = true; + } + + static gboolean launch_split_notebooks(GtkInstanceNotebook* pThis) + { + int nCurrentPage = pThis->get_current_page(); + pThis->split_notebooks(); + pThis->set_current_page(nCurrentPage); + pThis->m_nLaunchSplitTimeoutId = 0; + return false; + } + + // tdf#120371 + // https://developer.gnome.org/hig-book/unstable/controls-notebooks.html.en#controls-too-many-tabs + // if no of tabs > 6, but only if the notebook would auto-scroll, then split the tabs over + // two notebooks. Checking for the auto-scroll allows themes like Ambience under Ubuntu 16.04 to keep + // tabs in a single row when they would fit + void signal_notebook_size_allocate() + { + if (m_bOverFlowBoxActive || m_nLaunchSplitTimeoutId) + return; + disable_notify_events(); + gint nPages = gtk_notebook_get_n_pages(m_pNotebook); + if (nPages > 6 && gtk_notebook_get_tab_pos(m_pNotebook) == GTK_POS_TOP) + { + for (gint i = 0; i < nPages; ++i) + { + GtkWidget* pTabWidget = gtk_notebook_get_tab_label(m_pNotebook, gtk_notebook_get_nth_page(m_pNotebook, i)); +#if GTK_CHECK_VERSION(4, 0, 0) + bool bTabVisible = gtk_widget_get_child_visible(gtk_widget_get_parent(pTabWidget)); +#else + bool bTabVisible = gtk_widget_get_child_visible(pTabWidget); +#endif + if (!bTabVisible) + { + m_nLaunchSplitTimeoutId = g_timeout_add_full(G_PRIORITY_HIGH_IDLE, 0, reinterpret_cast(launch_split_notebooks), this, nullptr); + break; + } + } + } + enable_notify_events(); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + DECL_LINK(SizeAllocateHdl, void*, void); +#else + static void signalSizeAllocate(GtkWidget*, GdkRectangle*, gpointer widget) + { + GtkInstanceNotebook* pThis = static_cast(widget); + pThis->signal_notebook_size_allocate(); + } +#endif + + bool signal_focus(GtkDirectionType direction) + { + if (!m_bOverFlowBoxActive) + return false; + + int nPage = gtk_notebook_get_current_page(m_pNotebook); + if (direction == GTK_DIR_LEFT && nPage == 0) + { + auto nOverFlowLen = gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1; + gtk_notebook_set_current_page(m_pOverFlowNotebook, nOverFlowLen - 1); + return true; + } + else if (direction == GTK_DIR_RIGHT && nPage == gtk_notebook_get_n_pages(m_pNotebook) - 1) + { + gtk_notebook_set_current_page(m_pOverFlowNotebook, 0); + return true; + } + + return false; + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static gboolean signalFocus(GtkNotebook* notebook, GtkDirectionType direction, gpointer widget) + { + // if the notebook widget itself has focus + if (gtk_widget_is_focus(GTK_WIDGET(notebook))) + { + GtkInstanceNotebook* pThis = static_cast(widget); + return pThis->signal_focus(direction); + } + return false; + } +#endif + + // ctrl + page_up/ page_down + bool signal_change_current_page(gint arg1) + { + bool bHandled = signal_focus(arg1 < 0 ? GTK_DIR_LEFT : GTK_DIR_RIGHT); + if (bHandled) + g_signal_stop_emission_by_name(m_pNotebook, "change-current-page"); + return false; + } + + static gboolean signalChangeCurrentPage(GtkNotebook*, gint arg1, gpointer widget) + { + if (arg1 == 0) + return true; + GtkInstanceNotebook* pThis = static_cast(widget); + return pThis->signal_change_current_page(arg1); + } + +public: + GtkInstanceNotebook(GtkNotebook* pNotebook, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pNotebook), pBuilder, bTakeOwnership) + , m_pNotebook(pNotebook) + , m_pOverFlowBox(nullptr) + , m_pOverFlowNotebook(GTK_NOTEBOOK(gtk_notebook_new())) + , m_nSwitchPageSignalId(g_signal_connect(pNotebook, "switch-page", G_CALLBACK(signalSwitchPage), this)) + , m_nOverFlowSwitchPageSignalId(g_signal_connect(m_pOverFlowNotebook, "switch-page", G_CALLBACK(signalOverFlowSwitchPage), this)) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_pLayout(nullptr) +#else + , m_nNotebookSizeAllocateSignalId(0) + , m_nFocusSignalId(g_signal_connect(pNotebook, "focus", G_CALLBACK(signalFocus), this)) +#endif + , m_nChangeCurrentPageId(g_signal_connect(pNotebook, "change-current-page", G_CALLBACK(signalChangeCurrentPage), this)) + , m_nLaunchSplitTimeoutId(0) + , m_bOverFlowBoxActive(false) + , m_bOverFlowBoxIsStart(false) + , m_bInternalPageChange(false) + , m_nStartTabCount(0) + , m_nEndTabCount(0) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_add_events(GTK_WIDGET(pNotebook), GDK_SCROLL_MASK); +#endif + gint nPages = gtk_notebook_get_n_pages(m_pNotebook); + if (nPages > 6) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + m_nNotebookSizeAllocateSignalId = g_signal_connect_after(pNotebook, "size-allocate", G_CALLBACK(signalSizeAllocate), this); +#else + m_pLayout = NOTIFYING_LAYOUT(g_object_new(notifying_layout_get_type(), nullptr)); + notifying_layout_start_watch(m_pLayout, GTK_WIDGET(pNotebook), LINK(this, GtkInstanceNotebook, SizeAllocateHdl)); +#endif + } + gtk_notebook_set_show_border(m_pOverFlowNotebook, false); + + // tdf#122623 it's nigh impossible to have a GtkNotebook without an active (checked) tab, so try and theme + // the unwanted tab into invisibility via the 'overflow' class themed by global CreateStyleProvider + GtkStyleContext *pNotebookContext = gtk_widget_get_style_context(GTK_WIDGET(m_pOverFlowNotebook)); + gtk_style_context_add_class(pNotebookContext, "overflow"); + } + + virtual int get_current_page() const override + { + int nPage = gtk_notebook_get_current_page(m_pNotebook); + if (nPage == -1) + return nPage; + if (m_bOverFlowBoxIsStart) + { + auto nOverFlowLen = m_bOverFlowBoxActive ? gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1 : 0; + // add count of overflow pages, minus the extra tab + nPage += nOverFlowLen; + } + return nPage; + } + + virtual OUString get_page_ident(int nPage) const override + { + auto nMainLen = gtk_notebook_get_n_pages(m_pNotebook); + auto nOverFlowLen = m_bOverFlowBoxActive ? gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1 : 0; + if (m_bOverFlowBoxIsStart) + { + if (nPage < nOverFlowLen) + return get_page_ident(m_pOverFlowNotebook, nPage); + nPage -= nOverFlowLen; + return get_page_ident(m_pNotebook, nPage); + } + else + { + if (nPage < nMainLen) + return get_page_ident(m_pNotebook, nPage); + nPage -= nMainLen; + return get_page_ident(m_pOverFlowNotebook, nPage); + } + } + + virtual OUString get_current_page_ident() const override + { + const int nPage = get_current_page(); + return nPage != -1 ? get_page_ident(nPage) : OUString(); + } + + virtual int get_page_index(const OUString& rIdent) const override + { + auto nMainIndex = get_page_number(m_pNotebook, rIdent); + auto nOverFlowIndex = get_page_number(m_pOverFlowNotebook, rIdent); + + if (nMainIndex == -1 && nOverFlowIndex == -1) + return -1; + + if (m_bOverFlowBoxIsStart) + { + if (nOverFlowIndex != -1) + return nOverFlowIndex; + else + { + auto nOverFlowLen = m_bOverFlowBoxActive ? gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1 : 0; + return nMainIndex + nOverFlowLen; + } + } + else + { + if (nMainIndex != -1) + return nMainIndex; + else + { + auto nMainLen = gtk_notebook_get_n_pages(m_pNotebook); + return nOverFlowIndex + nMainLen; + } + } + } + + virtual weld::Container* get_page(const OUString& rIdent) const override + { + int nPage = get_page_index(rIdent); + if (nPage < 0) + return nullptr; + + GtkWidget* pChild; + if (m_bOverFlowBoxIsStart) + { + auto nOverFlowLen = m_bOverFlowBoxActive ? gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1 : 0; + if (nPage < nOverFlowLen) + pChild = gtk_notebook_get_nth_page(m_pOverFlowNotebook, nPage); + else + { + nPage -= nOverFlowLen; + pChild = gtk_notebook_get_nth_page(m_pNotebook, nPage); + } + } + else + { + auto nMainLen = gtk_notebook_get_n_pages(m_pNotebook); + if (nPage < nMainLen) + pChild = gtk_notebook_get_nth_page(m_pNotebook, nPage); + else + { + nPage -= nMainLen; + pChild = gtk_notebook_get_nth_page(m_pOverFlowNotebook, nPage); + } + } + + unsigned int nPageIndex = static_cast(nPage); + if (m_aPages.size() < nPageIndex + 1) + m_aPages.resize(nPageIndex + 1); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!m_aPages[nPageIndex]) + m_aPages[nPageIndex].reset(new GtkInstanceContainer(GTK_CONTAINER(pChild), m_pBuilder, false)); +#else + if (!m_aPages[nPageIndex]) + m_aPages[nPageIndex].reset(new GtkInstanceContainer(pChild, m_pBuilder, false)); +#endif + return m_aPages[nPageIndex].get(); + } + + virtual void set_current_page(int nPage) override + { + // normally we'd call disable_notify_events/enable_notify_events here, + // but the notebook is complicated by the need to support the + // double-decker hackery so for simplicity just flag that the page + // change is not a directly user-triggered one + bool bInternalPageChange = m_bInternalPageChange; + m_bInternalPageChange = true; + + if (m_bOverFlowBoxIsStart) + { + auto nOverFlowLen = m_bOverFlowBoxActive ? gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1 : 0; + if (nPage < nOverFlowLen) + gtk_notebook_set_current_page(m_pOverFlowNotebook, nPage); + else + { + nPage -= nOverFlowLen; + gtk_notebook_set_current_page(m_pNotebook, nPage); + } + } + else + { + auto nMainLen = gtk_notebook_get_n_pages(m_pNotebook); + if (nPage < nMainLen) + gtk_notebook_set_current_page(m_pNotebook, nPage); + else + { + nPage -= nMainLen; + gtk_notebook_set_current_page(m_pOverFlowNotebook, nPage); + } + } + + m_bInternalPageChange = bInternalPageChange; + } + + virtual void set_current_page(const OUString& rIdent) override + { + gint nPage = get_page_index(rIdent); + set_current_page(nPage); + } + + virtual int get_n_pages() const override + { + int nLen = gtk_notebook_get_n_pages(m_pNotebook); + if (m_bOverFlowBoxActive) + nLen += gtk_notebook_get_n_pages(m_pOverFlowNotebook) - 1; + return nLen; + } + + virtual OUString get_tab_label_text(const OUString& rIdent) const override + { + gint nPageNum = get_page_number(m_pNotebook, rIdent); + if (nPageNum != -1) + return get_tab_label_text(m_pNotebook, nPageNum); + nPageNum = get_page_number(m_pOverFlowNotebook, rIdent); + if (nPageNum != -1) + return get_tab_label_text(m_pOverFlowNotebook, nPageNum); + return OUString(); + } + + virtual void set_tab_label_text(const OUString& rIdent, const OUString& rText) override + { + gint nPageNum = get_page_number(m_pNotebook, rIdent); + if (nPageNum != -1) + { + set_tab_label_text(m_pNotebook, nPageNum, rText); + return; + } + nPageNum = get_page_number(m_pOverFlowNotebook, rIdent); + if (nPageNum != -1) + { + set_tab_label_text(m_pOverFlowNotebook, nPageNum, rText); + } + } + + virtual void set_show_tabs(bool bShow) override + { + if (m_bOverFlowBoxActive) + { + unsplit_notebooks(); + reset_split_data(); + } + + gtk_notebook_set_show_tabs(m_pNotebook, bShow); + gtk_notebook_set_show_tabs(m_pOverFlowNotebook, bShow); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pNotebook, m_nSwitchPageSignalId); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_block(m_pNotebook, m_nFocusSignalId); +#endif + g_signal_handler_block(m_pNotebook, m_nChangeCurrentPageId); + g_signal_handler_block(m_pOverFlowNotebook, m_nOverFlowSwitchPageSignalId); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_freeze_child_notify(GTK_WIDGET(m_pOverFlowNotebook)); +#endif + g_object_freeze_notify(G_OBJECT(m_pOverFlowNotebook)); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_object_thaw_notify(G_OBJECT(m_pOverFlowNotebook)); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_thaw_child_notify(GTK_WIDGET(m_pOverFlowNotebook)); +#endif + g_signal_handler_unblock(m_pOverFlowNotebook, m_nOverFlowSwitchPageSignalId); + g_signal_handler_unblock(m_pNotebook, m_nSwitchPageSignalId); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_unblock(m_pNotebook, m_nFocusSignalId); +#endif + g_signal_handler_unblock(m_pNotebook, m_nChangeCurrentPageId); + } + + void reset_split_data() + { + // reset overflow and allow it to be recalculated if necessary + gtk_widget_hide(GTK_WIDGET(m_pOverFlowNotebook)); + m_bOverFlowBoxActive = false; + m_nStartTabCount = 0; + m_nEndTabCount = 0; + } + + virtual void remove_page(const OUString& rIdent) override + { + if (m_bOverFlowBoxActive) + { + unsplit_notebooks(); + reset_split_data(); + } + + unsigned int nPageIndex = remove_page(m_pNotebook, rIdent); + if (nPageIndex < m_aPages.size()) + m_aPages.erase(m_aPages.begin() + nPageIndex); + } + + virtual void insert_page(const OUString& rIdent, const OUString& rLabel, int nPos) override + { + if (m_bOverFlowBoxActive) + { + unsplit_notebooks(); + reset_split_data(); + } + + // reset overflow and allow it to be recalculated if necessary + gtk_widget_hide(GTK_WIDGET(m_pOverFlowNotebook)); + m_bOverFlowBoxActive = false; + + insert_page(m_pNotebook, rIdent, rLabel, gtk_grid_new(), nPos); + } + + virtual ~GtkInstanceNotebook() override + { + if (m_nLaunchSplitTimeoutId) + g_source_remove(m_nLaunchSplitTimeoutId); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (m_nNotebookSizeAllocateSignalId) + g_signal_handler_disconnect(m_pNotebook, m_nNotebookSizeAllocateSignalId); +#else + if (m_pLayout) + { + // put it back how we found it initially + notifying_layout_stop_watch(m_pLayout); + } +#endif + g_signal_handler_disconnect(m_pNotebook, m_nSwitchPageSignalId); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pNotebook, m_nFocusSignalId); +#endif + g_signal_handler_disconnect(m_pNotebook, m_nChangeCurrentPageId); + g_signal_handler_disconnect(m_pOverFlowNotebook, m_nOverFlowSwitchPageSignalId); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_destroy(GTK_WIDGET(m_pOverFlowNotebook)); +#else + GtkWidget* pOverFlowWidget = GTK_WIDGET(m_pOverFlowNotebook); + g_clear_pointer(&pOverFlowWidget, gtk_widget_unparent); +#endif + if (!m_pOverFlowBox) + return; + + // put it back to how we found it initially + GtkWidget* pParent = gtk_widget_get_parent(GTK_WIDGET(m_pOverFlowBox)); + g_object_ref(m_pNotebook); + container_remove(GTK_WIDGET(m_pOverFlowBox), GTK_WIDGET(m_pNotebook)); + container_add(GTK_WIDGET(pParent), GTK_WIDGET(m_pNotebook)); + g_object_unref(m_pNotebook); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_destroy(GTK_WIDGET(m_pOverFlowBox)); +#else + GtkWidget* pOverFlowBox = GTK_WIDGET(m_pOverFlowBox); + g_clear_pointer(&pOverFlowBox, gtk_widget_unparent); +#endif + } +}; + +#if GTK_CHECK_VERSION(4, 0, 0) +IMPL_LINK_NOARG(GtkInstanceNotebook, SizeAllocateHdl, void*, void) +{ + signal_notebook_size_allocate(); +} +#endif + + +OUString vcl_font_to_css(const vcl::Font& rFont) +{ + OUStringBuffer sCSS( + "font-family: \"" + rFont.GetFamilyName() + "\"; " + "font-size: " + OUString::number(rFont.GetFontSize().Height()) + "pt; "); + switch (rFont.GetItalic()) + { + case ITALIC_NONE: + sCSS.append("font-style: normal; "); + break; + case ITALIC_NORMAL: + sCSS.append("font-style: italic; "); + break; + case ITALIC_OBLIQUE: + sCSS.append("font-style: oblique; "); + break; + default: + break; + } + switch (rFont.GetWeight()) + { + case WEIGHT_ULTRALIGHT: + sCSS.append("font-weight: 200; "); + break; + case WEIGHT_LIGHT: + sCSS.append("font-weight: 300; "); + break; + case WEIGHT_NORMAL: + sCSS.append("font-weight: 400; "); + break; + case WEIGHT_BOLD: + sCSS.append("font-weight: 700; "); + break; + case WEIGHT_ULTRABOLD: + sCSS.append("font-weight: 800; "); + break; + default: + break; + } + switch (rFont.GetWidthType()) + { + case WIDTH_ULTRA_CONDENSED: + sCSS.append("font-stretch: ultra-condensed; "); + break; + case WIDTH_EXTRA_CONDENSED: + sCSS.append("font-stretch: extra-condensed; "); + break; + case WIDTH_CONDENSED: + sCSS.append("font-stretch: condensed; "); + break; + case WIDTH_SEMI_CONDENSED: + sCSS.append("font-stretch: semi-condensed; "); + break; + case WIDTH_NORMAL: + sCSS.append("font-stretch: normal; "); + break; + case WIDTH_SEMI_EXPANDED: + sCSS.append("font-stretch: semi-expanded; "); + break; + case WIDTH_EXPANDED: + sCSS.append("font-stretch: expanded; "); + break; + case WIDTH_EXTRA_EXPANDED: + sCSS.append("font-stretch: extra-expanded; "); + break; + case WIDTH_ULTRA_EXPANDED: + sCSS.append("font-stretch: ultra-expanded; "); + break; + default: + break; + } + return sCSS.toString(); +} + +void update_attr_list(PangoAttrList* pAttrList, const vcl::Font& rFont) +{ + pango_attr_list_change(pAttrList, pango_attr_family_new(OUStringToOString(rFont.GetFamilyName(), RTL_TEXTENCODING_UTF8).getStr())); + pango_attr_list_change(pAttrList, pango_attr_size_new(rFont.GetFontSize().Height() * PANGO_SCALE)); + + switch (rFont.GetItalic()) + { + case ITALIC_NONE: + pango_attr_list_change(pAttrList, pango_attr_style_new(PANGO_STYLE_NORMAL)); + break; + case ITALIC_NORMAL: + pango_attr_list_change(pAttrList, pango_attr_style_new(PANGO_STYLE_ITALIC)); + break; + case ITALIC_OBLIQUE: + pango_attr_list_change(pAttrList, pango_attr_style_new(PANGO_STYLE_OBLIQUE)); + break; + default: + break; + } + switch (rFont.GetWeight()) + { + case WEIGHT_ULTRALIGHT: + pango_attr_list_change(pAttrList, pango_attr_weight_new(PANGO_WEIGHT_ULTRALIGHT)); + break; + case WEIGHT_LIGHT: + pango_attr_list_change(pAttrList, pango_attr_weight_new(PANGO_WEIGHT_LIGHT)); + break; + case WEIGHT_NORMAL: + pango_attr_list_change(pAttrList, pango_attr_weight_new(PANGO_WEIGHT_NORMAL)); + break; + case WEIGHT_BOLD: + pango_attr_list_change(pAttrList, pango_attr_weight_new(PANGO_WEIGHT_BOLD)); + break; + case WEIGHT_ULTRABOLD: + pango_attr_list_change(pAttrList, pango_attr_weight_new(PANGO_WEIGHT_ULTRABOLD)); + break; + default: + break; + } + switch (rFont.GetWidthType()) + { + case WIDTH_ULTRA_CONDENSED: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_ULTRA_CONDENSED)); + break; + case WIDTH_EXTRA_CONDENSED: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_EXTRA_CONDENSED)); + break; + case WIDTH_CONDENSED: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_CONDENSED)); + break; + case WIDTH_SEMI_CONDENSED: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_SEMI_CONDENSED)); + break; + case WIDTH_NORMAL: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_NORMAL)); + break; + case WIDTH_SEMI_EXPANDED: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_SEMI_EXPANDED)); + break; + case WIDTH_EXPANDED: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_EXPANDED)); + break; + case WIDTH_EXTRA_EXPANDED: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_EXTRA_EXPANDED)); + break; + case WIDTH_ULTRA_EXPANDED: + pango_attr_list_change(pAttrList, pango_attr_stretch_new(PANGO_STRETCH_ULTRA_EXPANDED)); + break; + default: + break; + } +} + +gboolean filter_pango_attrs(PangoAttribute *attr, gpointer data) +{ + PangoAttrType* pFilterAttrs = static_cast(data); + while (*pFilterAttrs) + { + if (attr->klass->type == *pFilterAttrs) + return true; + ++pFilterAttrs; + } + return false; +} + +void set_font(GtkLabel* pLabel, const vcl::Font& rFont) +{ + PangoAttrList* pOrigList = gtk_label_get_attributes(pLabel); + PangoAttrList* pAttrList = pOrigList ? pango_attr_list_copy(pOrigList) : pango_attr_list_new(); + + if (pOrigList) + { + // tdf#143443 remove both PANGO_ATTR_ABSOLUTE_SIZE and PANGO_ATTR_SIZE + // because pango_attr_list_change(..., pango_attr_size_new...) isn't + // sufficient on its own to ensure a new size sticks. + PangoAttrType aFilterAttrs[] = {PANGO_ATTR_ABSOLUTE_SIZE, PANGO_ATTR_SIZE, PANGO_ATTR_INVALID}; + PangoAttrList* pRemovedAttrs = pango_attr_list_filter(pAttrList, filter_pango_attrs, &aFilterAttrs); + pango_attr_list_unref(pRemovedAttrs); + } + + update_attr_list(pAttrList, rFont); + gtk_label_set_attributes(pLabel, pAttrList); + pango_attr_list_unref(pAttrList); +} + +} + +namespace { + +class WidgetBackground +{ +private: + GtkWidget* m_pWidget; + GtkCssProvider* m_pCustomCssProvider; + std::unique_ptr m_xCustomImage; + +public: + // See: https://developer.gnome.org/Buttons/ + void use_custom_content(const VirtualDevice* pDevice) + { + GtkStyleContext *pWidgetContext = gtk_widget_get_style_context(m_pWidget); + + if (m_pCustomCssProvider) + { + gtk_style_context_remove_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pCustomCssProvider)); + m_pCustomCssProvider = nullptr; + } + + m_xCustomImage.reset(); + + if (!pDevice) + return; + + m_xCustomImage.reset(new utl::TempFileNamed); + m_xCustomImage->EnableKillingFile(true); + + cairo_surface_t* surface = get_underlying_cairo_surface(*pDevice); + Size aSize = pDevice->GetOutputSizePixel(); + cairo_surface_write_to_png(surface, OUStringToOString(m_xCustomImage->GetFileName(), osl_getThreadTextEncoding()).getStr()); + + m_pCustomCssProvider = gtk_css_provider_new(); + OUString aBuffer = "* { background-image: url(\"" + m_xCustomImage->GetURL() + "\"); " + "background-size: " + OUString::number(aSize.Width()) + "px " + OUString::number(aSize.Height()) + "px; " + "border-radius: 0; border-width: 0; }"; + OString aResult = OUStringToOString(aBuffer, RTL_TEXTENCODING_UTF8); + css_provider_load_from_data(m_pCustomCssProvider, aResult.getStr(), aResult.getLength()); + gtk_style_context_add_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pCustomCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + +public: + WidgetBackground(GtkWidget* pWidget) + : m_pWidget(pWidget) + , m_pCustomCssProvider(nullptr) + { + } + + ~WidgetBackground() + { + if (m_pCustomCssProvider) + use_custom_content(nullptr); + assert(!m_pCustomCssProvider); + } +}; + +class WidgetFont +{ +private: + GtkWidget* m_pWidget; + GtkCssProvider* m_pFontCssProvider; + std::unique_ptr m_xFont; +public: + WidgetFont(GtkWidget* pWidget) + : m_pWidget(pWidget) + , m_pFontCssProvider(nullptr) + { + } + + void use_custom_font(const vcl::Font* pFont, std::u16string_view rCSSSelector) + { + GtkStyleContext *pWidgetContext = gtk_widget_get_style_context(m_pWidget); + if (m_pFontCssProvider) + { + gtk_style_context_remove_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pFontCssProvider)); + m_pFontCssProvider = nullptr; + } + + m_xFont.reset(); + + if (!pFont) + return; + + m_xFont.reset(new vcl::Font(*pFont)); + m_pFontCssProvider = gtk_css_provider_new(); + OUString aBuffer = rCSSSelector + OUString::Concat(" { ") + vcl_font_to_css(*pFont) + OUString::Concat(" }"); + OString aResult = OUStringToOString(aBuffer, RTL_TEXTENCODING_UTF8); + css_provider_load_from_data(m_pFontCssProvider, aResult.getStr(), aResult.getLength()); + gtk_style_context_add_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pFontCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + + const vcl::Font* get_custom_font() const + { + return m_xFont.get(); + } + + ~WidgetFont() + { + if (m_pFontCssProvider) + use_custom_font(nullptr, u""); + assert(!m_pFontCssProvider); + } +}; + +class GtkInstanceButton : public GtkInstanceWidget, public virtual weld::Button +{ +private: + GtkButton* m_pButton; + gulong m_nSignalId; + std::optional m_xFont; + WidgetBackground m_aCustomBackground; + + static void signalClicked(GtkButton*, gpointer widget) + { + GtkInstanceButton* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_clicked(); + } + + virtual void ensureMouseEventWidget() override + { + // The GtkButton is sufficient to get mouse events without an intermediate GtkEventBox + if (!m_pMouseEventBox) + m_pMouseEventBox = m_pWidget; + } + +public: + GtkInstanceButton(GtkButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pButton), pBuilder, bTakeOwnership) + , m_pButton(pButton) + , m_nSignalId(g_signal_connect(pButton, "clicked", G_CALLBACK(signalClicked), this)) + , m_aCustomBackground(GTK_WIDGET(pButton)) + { + g_object_set_data(G_OBJECT(m_pButton), "g-lo-GtkInstanceButton", this); + } + + virtual void set_label(const OUString& rText) override + { + ::button_set_label(m_pButton, rText); + } + + virtual void set_image(VirtualDevice* pDevice) override + { + ::button_set_image(m_pButton, pDevice); + } + + virtual void set_from_icon_name(const OUString& rIconName) override + { + ::button_set_from_icon_name(m_pButton, rIconName); + } + + virtual void set_image(const css::uno::Reference& rImage) override + { + ::button_set_image(m_pButton, rImage); + } + + virtual void set_custom_button(VirtualDevice* pDevice) override + { + m_aCustomBackground.use_custom_content(pDevice); + } + + virtual OUString get_label() const override + { + return ::button_get_label(m_pButton); + } + + virtual void set_font(const vcl::Font& rFont) override + { + m_xFont = rFont; + GtkLabel* pChild = ::get_label_widget(GTK_WIDGET(m_pButton)); + ::set_font(pChild, rFont); + } + + virtual vcl::Font get_font() override + { + if (m_xFont) + return *m_xFont; + return GtkInstanceWidget::get_font(); + } + + // allow us to block buttons with click handlers making dialogs return a response + bool has_click_handler() const + { + return m_aClickHdl.IsSet(); + } + + void clear_click_handler() + { + m_aClickHdl = Link(); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pButton, m_nSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pButton, m_nSignalId); + } + + virtual ~GtkInstanceButton() override + { + g_object_steal_data(G_OBJECT(m_pButton), "g-lo-GtkInstanceButton"); + g_signal_handler_disconnect(m_pButton, m_nSignalId); + } +}; + +} + +void GtkInstanceDialog::asyncresponse(gint ret) +{ + SolarMutexGuard aGuard; + + if (ret == GTK_RESPONSE_HELP) + { + help(); + return; + } + + GtkInstanceButton* pClickHandler = has_click_handler(ret); + if (pClickHandler) + { + // make GTK_RESPONSE_DELETE_EVENT act as if cancel button was pressed + if (ret == GTK_RESPONSE_DELETE_EVENT) + close(false); + return; + } + + if (get_modal()) + m_aDialogRun.dec_modal_count(); + hide(); + + // move the self pointer, otherwise it might be de-allocated by time we try to reset it + auto xRunAsyncSelf = std::move(m_xRunAsyncSelf); + auto xDialogController = std::move(m_xDialogController); + auto aFunc = std::move(m_aFunc); + + auto nResponseSignalId = m_nResponseSignalId; + auto nCancelSignalId = m_nCancelSignalId; + auto nSignalDeleteId = m_nSignalDeleteId; + m_nResponseSignalId = 0; + m_nCancelSignalId = 0; + m_nSignalDeleteId = 0; + + if (aFunc) + aFunc(GtkToVcl(ret)); + + if (nResponseSignalId) + g_signal_handler_disconnect(m_pDialog, nResponseSignalId); + if (nCancelSignalId) + g_signal_handler_disconnect(m_pDialog, nCancelSignalId); + if (nSignalDeleteId) + g_signal_handler_disconnect(m_pDialog, nSignalDeleteId); + + xDialogController.reset(); + xRunAsyncSelf.reset(); +} + +int GtkInstanceDialog::run() +{ + // tdf#150723 "run" will make the dialog visible so drop m_aPosWhileInvis like show + m_aPosWhileInvis.reset(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + if (GTK_IS_DIALOG(m_pDialog)) + sort_native_button_order(GTK_BOX(gtk_dialog_get_action_area(GTK_DIALOG(m_pDialog)))); +#endif + int ret; + while (true) + { + ret = m_aDialogRun.run(); + if (ret == GTK_RESPONSE_HELP) + { + help(); + continue; + } + else if (has_click_handler(ret)) + continue; + break; + } + hide(); + return GtkToVcl(ret); +} + +weld::Button* GtkInstanceDialog::weld_widget_for_response(int nVclResponse) +{ + GtkButton* pButton = get_widget_for_response(VclToGtk(nVclResponse)); + if (!pButton) + return nullptr; + return new GtkInstanceButton(pButton, m_pBuilder, false); +} + +void GtkInstanceDialog::response(int nResponse) +{ + int nGtkResponse = VclToGtk(nResponse); + //unblock this response now when activated through code + if (GtkButton* pWidget = get_widget_for_response(nGtkResponse)) + { + void* pData = g_object_get_data(G_OBJECT(pWidget), "g-lo-GtkInstanceButton"); + GtkInstanceButton* pButton = static_cast(pData); + if (pButton) + pButton->clear_click_handler(); + } + if (GTK_IS_DIALOG(m_pDialog)) + gtk_dialog_response(GTK_DIALOG(m_pDialog), nGtkResponse); + else if (GTK_IS_ASSISTANT(m_pDialog)) + { + if (!m_aDialogRun.loop_is_running()) + asyncresponse(nGtkResponse); + else + { + m_aDialogRun.m_nResponseId = nGtkResponse; + m_aDialogRun.loop_quit(); + } + } +} + +void GtkInstanceDialog::close(bool bCloseSignal) +{ + GtkInstanceButton* pClickHandler = has_click_handler(GTK_RESPONSE_CANCEL); + if (pClickHandler) + { + if (bCloseSignal) + g_signal_stop_emission_by_name(m_pDialog, "close"); + // make esc (bCloseSignal == true) or window-delete (bCloseSignal == false) + // act as if cancel button was pressed + pClickHandler->clicked(); + return; + } + response(RET_CANCEL); +} + +GtkInstanceButton* GtkInstanceDialog::has_click_handler(int nResponse) +{ + GtkInstanceButton* pButton = nullptr; + // e.g. map GTK_RESPONSE_DELETE_EVENT to GTK_RESPONSE_CANCEL + nResponse = VclToGtk(GtkToVcl(nResponse)); + if (GtkButton* pWidget = get_widget_for_response(nResponse)) + { + void* pData = g_object_get_data(G_OBJECT(pWidget), "g-lo-GtkInstanceButton"); + pButton = static_cast(pData); + if (pButton && !pButton->has_click_handler()) + pButton = nullptr; + } + return pButton; +} + +namespace { + +class GtkInstanceToggleButton : public GtkInstanceButton, public virtual weld::ToggleButton +{ +protected: + GtkToggleButton* m_pToggleButton; + gulong m_nToggledSignalId; +private: + static void signalToggled(GtkToggleButton*, gpointer widget) + { + GtkInstanceToggleButton* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_toggled(); + } +public: + GtkInstanceToggleButton(GtkToggleButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceButton(GTK_BUTTON(pButton), pBuilder, bTakeOwnership) + , m_pToggleButton(pButton) + , m_nToggledSignalId(g_signal_connect(m_pToggleButton, "toggled", G_CALLBACK(signalToggled), this)) + { + } + + virtual void set_active(bool active) override + { + disable_notify_events(); + set_inconsistent(false); + gtk_toggle_button_set_active(m_pToggleButton, active); + enable_notify_events(); + } + + virtual bool get_active() const override + { + return gtk_toggle_button_get_active(m_pToggleButton); + } + + virtual void set_inconsistent(bool inconsistent) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + if (inconsistent) + gtk_widget_set_state_flags(GTK_WIDGET(m_pToggleButton), GTK_STATE_FLAG_INCONSISTENT, false); + else + gtk_widget_unset_state_flags(GTK_WIDGET(m_pToggleButton), GTK_STATE_FLAG_INCONSISTENT); +#else + gtk_toggle_button_set_inconsistent(m_pToggleButton, inconsistent); +#endif + } + + virtual bool get_inconsistent() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_widget_get_state_flags(GTK_WIDGET(m_pToggleButton)) & GTK_STATE_FLAG_INCONSISTENT; +#else + return gtk_toggle_button_get_inconsistent(m_pToggleButton); +#endif + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pToggleButton, m_nToggledSignalId); + GtkInstanceButton::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceButton::enable_notify_events(); + g_signal_handler_unblock(m_pToggleButton, m_nToggledSignalId); + } + + virtual ~GtkInstanceToggleButton() override + { + g_signal_handler_disconnect(m_pToggleButton, m_nToggledSignalId); + } +}; + +} + +#if !GTK_CHECK_VERSION(4, 0, 0) + +namespace { + +void do_grab(GtkWidget* pWidget) +{ + GdkDisplay *pDisplay = gtk_widget_get_display(pWidget); + GdkSeat* pSeat = gdk_display_get_default_seat(pDisplay); + gdk_seat_grab(pSeat, widget_get_surface(pWidget), + GDK_SEAT_CAPABILITY_KEYBOARD, true, nullptr, nullptr, nullptr, nullptr); +} + +void do_ungrab(GtkWidget* pWidget) +{ + GdkDisplay *pDisplay = gtk_widget_get_display(pWidget); + GdkSeat* pSeat = gdk_display_get_default_seat(pDisplay); + gdk_seat_ungrab(pSeat); +} + +GtkPositionType show_menu_older_gtk(GtkWidget* pMenuButton, GtkWindow* pMenu, const GdkRectangle& rAnchor, + weld::Placement ePlace, bool bTryShrink) +{ + //place the toplevel just below its launcher button + GtkWidget* pToplevel = widget_get_toplevel(pMenuButton); + gtk_coord x, y, absx, absy; + gtk_widget_translate_coordinates(pMenuButton, pToplevel, rAnchor.x, rAnchor.y, &x, &y); + GdkSurface* pWindow = widget_get_surface(pToplevel); + gdk_window_get_position(pWindow, &absx, &absy); + + x += absx; + y += absy; + + gint nButtonHeight = rAnchor.height; + gint nButtonWidth = rAnchor.width; + if (ePlace == weld::Placement::Under) + y += nButtonHeight; + else + x += nButtonWidth; + + gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(pToplevel)), pMenu); + gtk_window_set_transient_for(pMenu, GTK_WINDOW(pToplevel)); + + gint nMenuWidth, nMenuHeight; + gtk_widget_get_size_request(GTK_WIDGET(pMenu), &nMenuWidth, &nMenuHeight); + + if (nMenuWidth == -1 || nMenuHeight == -1) + { + GtkRequisition req; + gtk_widget_get_preferred_size(GTK_WIDGET(pMenu), nullptr, &req); + if (nMenuWidth == -1) + nMenuWidth = req.width; + if (nMenuHeight == -1) + nMenuHeight = req.height; + } + + bool bSwapForRTL = SwapForRTL(pMenuButton); + if (bSwapForRTL) + { + if (ePlace == weld::Placement::Under) + x += nButtonWidth; + else + x -= nButtonWidth; + x -= nMenuWidth; + } + + tools::Rectangle aWorkArea(::get_monitor_workarea(pMenuButton)); + + // shrink it a little, I find it reassuring to see a little margin with a + // long menu to know the menu is fully on screen + aWorkArea.AdjustTop(8); + aWorkArea.AdjustBottom(-8); + aWorkArea.AdjustLeft(8); + aWorkArea.AdjustRight(-8); + + GtkPositionType ePosUsed; + + if (ePlace == weld::Placement::Under) + { + gint endx = x + nMenuWidth; + if (endx > aWorkArea.Right()) + x -= endx - aWorkArea.Right(); + if (x < 0) + x = 0; + + ePosUsed = GTK_POS_BOTTOM; + gint endy = y + nMenuHeight; + gint nMissingBelow = endy - aWorkArea.Bottom(); + if (nMissingBelow > 0) + { + gint nNewY = y - (nButtonHeight + nMenuHeight); + gint nMissingAbove = aWorkArea.Top() - nNewY; + if (nMissingAbove > 0) + { + if (bTryShrink) + { + if (nMissingBelow <= nMissingAbove) + nMenuHeight -= nMissingBelow; + else + { + nMenuHeight -= nMissingAbove; + y = aWorkArea.Top(); + ePosUsed = GTK_POS_TOP; + } + gtk_widget_set_size_request(GTK_WIDGET(pMenu), nMenuWidth, nMenuHeight); + } + else + { + if (nMissingBelow <= nMissingAbove) + y -= nMissingBelow; + else + { + y = aWorkArea.Top(); + ePosUsed = GTK_POS_TOP; + } + } + } + else + { + y = nNewY; + ePosUsed = GTK_POS_TOP; + } + } + } + else + { + if (!bSwapForRTL) + { + ePosUsed = GTK_POS_RIGHT; + gint endx = x + nMenuWidth; + gint nMissingAfter = endx - aWorkArea.Right(); + if (nMissingAfter > 0) + { + gint nNewX = x - (nButtonWidth + nMenuWidth); + if (nNewX >= aWorkArea.Left()) + { + x = nNewX; + ePosUsed = GTK_POS_LEFT; + } + } + } + else + { + ePosUsed = GTK_POS_LEFT; + gint startx = x; + gint nMissingBefore = aWorkArea.Left() - startx; + if (nMissingBefore > 0) + { + gint nNewX = x + (nButtonWidth + nMenuWidth); + if (nNewX + nMenuWidth < aWorkArea.Right()) + { + x = nNewX; + ePosUsed = GTK_POS_RIGHT; + } + } + } + } + + gtk_window_move(pMenu, x, y); + + return ePosUsed; +} + +bool show_menu_newer_gtk(GtkWidget* pComboBox, GtkWindow* pMenu, const GdkRectangle &rAnchor, + weld::Placement ePlace, bool bTryShrink) +{ + static auto window_move_to_rect = reinterpret_cast( + dlsym(nullptr, "gdk_window_move_to_rect")); + if (!window_move_to_rect) + return false; + + // under wayland gdk_window_move_to_rect works great for me, but in my current + // gtk 3.24 under X it leaves part of long menus outside the work area + GdkDisplay *pDisplay = gtk_widget_get_display(pComboBox); + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + return false; + + //place the toplevel just below its launcher button + GtkWidget* pToplevel = widget_get_toplevel(pComboBox); + gtk_coord x, y; + gtk_widget_translate_coordinates(pComboBox, pToplevel, rAnchor.x, rAnchor.y, &x, &y); + + gtk_widget_realize(GTK_WIDGET(pMenu)); + gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(pToplevel)), pMenu); + gtk_window_set_transient_for(pMenu, GTK_WINDOW(pToplevel)); + + bool bSwapForRTL = SwapForRTL(GTK_WIDGET(pComboBox)); + + GdkGravity rect_anchor; + GdkGravity menu_anchor; + + if (ePlace == weld::Placement::Under) + { + rect_anchor = !bSwapForRTL ? GDK_GRAVITY_SOUTH_WEST : GDK_GRAVITY_SOUTH_EAST; + menu_anchor = !bSwapForRTL ? GDK_GRAVITY_NORTH_WEST : GDK_GRAVITY_NORTH_EAST; + } + else + { + rect_anchor = !bSwapForRTL ? GDK_GRAVITY_NORTH_EAST : GDK_GRAVITY_NORTH_WEST; + menu_anchor = !bSwapForRTL ? GDK_GRAVITY_NORTH_WEST : GDK_GRAVITY_NORTH_EAST; + } + + GdkAnchorHints anchor_hints = static_cast(GDK_ANCHOR_FLIP | GDK_ANCHOR_SLIDE); + if (bTryShrink) + anchor_hints = static_cast(anchor_hints | GDK_ANCHOR_RESIZE); + GdkRectangle rect {x, y, rAnchor.width, rAnchor.height}; + GdkSurface* toplevel = widget_get_surface(GTK_WIDGET(pMenu)); + + window_move_to_rect(toplevel, &rect, rect_anchor, menu_anchor, anchor_hints, + 0, 0); + + return true; +} + +GtkPositionType show_menu(GtkWidget* pMenuButton, GtkWindow* pMenu, const GdkRectangle& rAnchor, + weld::Placement ePlace, bool bTryShrink) +{ + // we only use ePosUsed in the replacement-for-X-popover case of a + // MenuButton, so we only need it when show_menu_older_gtk is used + GtkPositionType ePosUsed = GTK_POS_BOTTOM; + + // tdf#120764 It isn't allowed under wayland to have two visible popups that share + // the same top level parent. The problem is that since gtk 3.24 tooltips are also + // implemented as popups, which means that we cannot show any popup if there is a + // visible tooltip. + GtkWidget* pParent = widget_get_toplevel(pMenuButton); + GtkSalFrame* pFrame = pParent ? GtkSalFrame::getFromWindow(pParent) : nullptr; + if (pFrame) + { + // hide any current tooltip + pFrame->HideTooltip(); + // don't allow any more to appear until menu is dismissed + pFrame->BlockTooltip(); + } + + // try with gdk_window_move_to_rect, but if that's not available, try without + if (!show_menu_newer_gtk(pMenuButton, pMenu, rAnchor, ePlace, bTryShrink)) + ePosUsed = show_menu_older_gtk(pMenuButton, pMenu, rAnchor, ePlace, bTryShrink); + gtk_widget_show_all(GTK_WIDGET(pMenu)); + gtk_widget_grab_focus(GTK_WIDGET(pMenu)); + do_grab(GTK_WIDGET(pMenu)); + + return ePosUsed; +} + +} +#endif + +namespace { + +#if !GTK_CHECK_VERSION(4, 0, 0) +bool button_event_is_outside(GtkWidget* pMenuHack, GdkEventButton* pEvent) +{ + //we want to pop down if the button was released outside our popup + gdouble x = pEvent->x_root; + gdouble y = pEvent->y_root; + + gint window_x, window_y; + GdkSurface* pWindow = widget_get_surface(pMenuHack); + gdk_window_get_position(pWindow, &window_x, &window_y); + + GtkAllocation alloc; + gtk_widget_get_allocation(pMenuHack, &alloc); + gint x1 = window_x; + gint y1 = window_y; + gint x2 = x1 + alloc.width; + gint y2 = y1 + alloc.height; + + if (x > x1 && x < x2 && y > y1 && y < y2) + return false; + + return true; +} + +GtkPositionType MovePopoverContentsToWindow(GtkWidget* pPopover, GtkWindow* pMenuHack, GtkWidget* pAnchor, + const GdkRectangle& rAnchor, weld::Placement ePlace) +{ + //set border width + gtk_container_set_border_width(GTK_CONTAINER(pMenuHack), gtk_container_get_border_width(GTK_CONTAINER(pPopover))); + + //steal popover contents and smuggle into toplevel display window + GtkWidget* pChild = gtk_bin_get_child(GTK_BIN(pPopover)); + g_object_ref(pChild); + gtk_container_remove(GTK_CONTAINER(pPopover), pChild); + gtk_container_add(GTK_CONTAINER(pMenuHack), pChild); + g_object_unref(pChild); + + GtkPositionType eRet = show_menu(pAnchor, pMenuHack, rAnchor, ePlace, false); + + gtk_grab_add(GTK_WIDGET(pMenuHack)); + + GdkSurface* pSurface = widget_get_surface(GTK_WIDGET(pMenuHack)); + g_object_set_data(G_OBJECT(pSurface), "g-lo-InstancePopup", GINT_TO_POINTER(true)); + + return eRet; +} + +void MoveWindowContentsToPopover(GtkWindow* pMenuHack, GtkWidget* pPopover, GtkWidget* pAnchor) +{ + bool bHadFocus = gtk_window_has_toplevel_focus(pMenuHack); + + do_ungrab(GTK_WIDGET(pMenuHack)); + + gtk_grab_remove(GTK_WIDGET(pMenuHack)); + + gtk_widget_hide(GTK_WIDGET(pMenuHack)); + //put contents back from where the came from + GtkWidget* pChild = gtk_bin_get_child(GTK_BIN(pMenuHack)); + g_object_ref(pChild); + gtk_container_remove(GTK_CONTAINER(pMenuHack), pChild); + gtk_container_add(GTK_CONTAINER(pPopover), pChild); + g_object_unref(pChild); + + GdkSurface* pSurface = widget_get_surface(GTK_WIDGET(pMenuHack)); + g_object_set_data(G_OBJECT(pSurface), "g-lo-InstancePopup", GINT_TO_POINTER(false)); + + // so gdk_window_move_to_rect will work again the next time + gtk_widget_unrealize(GTK_WIDGET(pMenuHack)); + + gtk_widget_set_size_request(GTK_WIDGET(pMenuHack), -1, -1); + + // undo show_menu tooltip blocking + GtkWidget* pParent = widget_get_toplevel(pAnchor); + GtkSalFrame* pFrame = pParent ? GtkSalFrame::getFromWindow(pParent) : nullptr; + if (pFrame) + pFrame->UnblockTooltip(); + + if (bHadFocus) + { + GdkSurface* pParentSurface = pParent ? widget_get_surface(pParent) : nullptr; + void* pParentIsPopover = pParentSurface ? g_object_get_data(G_OBJECT(pParentSurface), "g-lo-InstancePopup") : nullptr; + if (pParentIsPopover) + do_grab(pAnchor); + gtk_widget_grab_focus(pAnchor); + } +} + +#endif + +/* four types of uses of this + a) textual menubutton, always with pan-down symbol, e.g. math, format, font, modify + b) image + text, always with additional pan-down symbol, e.g. writer, format, watermark + c) gear menu, never with text and without pan-down symbol where there is a replacement + icon for pan-down, e.g. file, new, templates + d) image, always with additional pan-down symbol, e.g. calc, insert, header/footer */ +#if !GTK_CHECK_VERSION(4, 0, 0) +class GtkInstanceMenuButton : public GtkInstanceToggleButton, public MenuHelper, public virtual weld::MenuButton +#else +class GtkInstanceMenuButton : public GtkInstanceWidget, public MenuHelper, public virtual weld::MenuButton +#endif +{ +protected: + GtkMenuButton* m_pMenuButton; +private: + GtkBox* m_pBox; +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkImage* m_pImage; +#else + GtkPicture* m_pImage; + GtkToggleButton* m_pMenuButtonToggleButton; +#endif + GtkWidget* m_pLabel; +#if !GTK_CHECK_VERSION(4, 0, 0) + //popover cannot escape dialog under X so stick up own window instead + GtkWindow* m_pMenuHack; + //when doing so, if it's a toolbar menubutton align the menu to the full toolitem + GtkWidget* m_pMenuHackAlign; + bool m_nButtonPressSeen; + gulong m_nSignalId; +#endif + GtkWidget* m_pPopover; +#if GTK_CHECK_VERSION(4, 0, 0) + gulong m_nToggledSignalId; + std::optional m_xFont; + WidgetBackground m_aCustomBackground; +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + static void signalMenuButtonToggled(GtkWidget*, gpointer widget) + { + GtkInstanceMenuButton* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->menu_toggled(); + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + void menu_toggled() + { + if (!m_pMenuHack) + return; + if (!get_active()) + { + m_nButtonPressSeen = false; + MoveWindowContentsToPopover(m_pMenuHack, m_pPopover, GTK_WIDGET(m_pMenuButton)); + } + else + { + GtkWidget* pAnchor = m_pMenuHackAlign ? m_pMenuHackAlign : GTK_WIDGET(m_pMenuButton); + GdkRectangle aAnchor {0, 0, gtk_widget_get_allocated_width(pAnchor), gtk_widget_get_allocated_height(pAnchor) }; + GtkPositionType ePosUsed = MovePopoverContentsToWindow(m_pPopover, m_pMenuHack, pAnchor, aAnchor, weld::Placement::Under); + // tdf#132540 keep the placeholder popover on this same side as the replacement menu + gtk_popover_set_position(gtk_menu_button_get_popover(m_pMenuButton), ePosUsed); + } + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + static void signalGrabBroken(GtkWidget*, GdkEventGrabBroken *pEvent, gpointer widget) + { + GtkInstanceMenuButton* pThis = static_cast(widget); + pThis->grab_broken(pEvent); + } + + void grab_broken(const GdkEventGrabBroken *event) + { + if (event->grab_window == nullptr) + { + set_active(false); + } + else if (!g_object_get_data(G_OBJECT(event->grab_window), "g-lo-InstancePopup")) // another LibreOffice popover took a grab + { + //try and regrab, so when we lose the grab to the menu of the color palette + //combobox we regain it so the color palette doesn't itself disappear on next + //click on the color palette combobox + do_grab(GTK_WIDGET(m_pMenuHack)); + } + } + + static gboolean signalButtonPress(GtkWidget* /*pWidget*/, GdkEventButton* /*pEvent*/, gpointer widget) + { + GtkInstanceMenuButton* pThis = static_cast(widget); + pThis->m_nButtonPressSeen = true; + return false; + } + + static gboolean signalButtonRelease(GtkWidget* /*pWidget*/, GdkEventButton* pEvent, gpointer widget) + { + GtkInstanceMenuButton* pThis = static_cast(widget); + if (pThis->m_nButtonPressSeen && button_event_is_outside(GTK_WIDGET(pThis->m_pMenuHack), pEvent)) + pThis->set_active(false); + return false; + } + + static gboolean keyPress(GtkWidget*, GdkEventKey* pEvent, gpointer widget) + { + GtkInstanceMenuButton* pThis = static_cast(widget); + return pThis->key_press(pEvent); + } + + bool key_press(const GdkEventKey* pEvent) + { + if (pEvent->keyval == GDK_KEY_Escape) + { + set_active(false); + return true; + } + return false; + } +#endif + + void ensure_image_widget() + { + if (m_pImage) + return; + +#if !GTK_CHECK_VERSION(4, 0, 0) + m_pImage = GTK_IMAGE(gtk_image_new()); + gtk_box_pack_start(m_pBox, GTK_WIDGET(m_pImage), false, false, 0); + gtk_box_reorder_child(m_pBox, GTK_WIDGET(m_pImage), 0); +#else + m_pImage = GTK_PICTURE(gtk_picture_new()); + gtk_widget_set_halign(GTK_WIDGET(m_pImage), GTK_ALIGN_CENTER); + gtk_widget_set_valign(GTK_WIDGET(m_pImage), GTK_ALIGN_CENTER); + gtk_box_prepend(m_pBox, GTK_WIDGET(m_pImage)); + gtk_widget_set_halign(m_pLabel, GTK_ALIGN_START); +#endif + gtk_widget_show(GTK_WIDGET(m_pImage)); + } + + static void signalFlagsChanged(GtkToggleButton* pToggleButton, GtkStateFlags flags, gpointer widget) + { + GtkInstanceMenuButton* pThis = static_cast(widget); + bool bOldChecked = flags & GTK_STATE_FLAG_CHECKED; + bool bNewChecked = gtk_widget_get_state_flags(GTK_WIDGET(pToggleButton)) & GTK_STATE_FLAG_CHECKED; + if (bOldChecked == bNewChecked) + return; + if (bOldChecked && gtk_widget_get_focus_on_click(GTK_WIDGET(pToggleButton))) + { + // grab focus back to the toggle button if the menu was popped down + gtk_widget_grab_focus(GTK_WIDGET(pToggleButton)); + } + SolarMutexGuard aGuard; + pThis->signal_toggled(); + } + +public: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkInstanceMenuButton(GtkMenuButton* pMenuButton, GtkWidget* pMenuAlign, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceToggleButton(GTK_TOGGLE_BUTTON(pMenuButton), pBuilder, bTakeOwnership) + , MenuHelper(gtk_menu_button_get_popup(pMenuButton), false) +#else + GtkInstanceMenuButton(GtkMenuButton* pMenuButton, GtkWidget* pMenuAlign, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pMenuButton), pBuilder, bTakeOwnership) + , MenuHelper(GTK_POPOVER_MENU(gtk_menu_button_get_popover(pMenuButton)), false) +#endif + , m_pMenuButton(pMenuButton) + , m_pImage(nullptr) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_pMenuHack(nullptr) + , m_pMenuHackAlign(pMenuAlign) + , m_nButtonPressSeen(true) + , m_nSignalId(0) +#endif + , m_pPopover(nullptr) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_aCustomBackground(GTK_WIDGET(pMenuButton)) +#endif + { +#if !GTK_CHECK_VERSION(4, 0, 0) + // tdf#142924 "toggled" is too late to use to populate changes to the menu, + // so use "state-flag-changed" on GTK_STATE_FLAG_CHECKED instead which + // happens before "toggled" + g_signal_handler_disconnect(m_pToggleButton, m_nToggledSignalId); + m_nToggledSignalId = g_signal_connect(m_pToggleButton, "state-flags-changed", G_CALLBACK(signalFlagsChanged), this); + + m_pLabel = gtk_bin_get_child(GTK_BIN(m_pMenuButton)); + m_pImage = get_image_widget(GTK_WIDGET(m_pMenuButton)); + m_pBox = formatMenuButton(m_pLabel); +#else + GtkWidget* pToggleButton = gtk_widget_get_first_child(GTK_WIDGET(m_pMenuButton)); + assert(GTK_IS_TOGGLE_BUTTON(pToggleButton)); + m_pMenuButtonToggleButton = GTK_TOGGLE_BUTTON(pToggleButton); + m_nToggledSignalId = g_signal_connect(m_pMenuButtonToggleButton, "state-flags-changed", G_CALLBACK(signalFlagsChanged), this); + GtkWidget* pChild = gtk_button_get_child(GTK_BUTTON(pToggleButton)); + m_pBox = GTK_IS_BOX(pChild) ? GTK_BOX(pChild) : nullptr; + m_pLabel = m_pBox ? gtk_widget_get_first_child(GTK_WIDGET(m_pBox)) : nullptr; + (void)pMenuAlign; +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_insert_action_group(GTK_WIDGET(m_pMenuButton), "menu", m_pActionGroup); + + update_action_group_from_popover_model(); +#endif + } + + virtual void set_size_request(int nWidth, int nHeight) override + { + // tweak the label to get a narrower size to stick + if (GTK_IS_LABEL(m_pLabel)) + gtk_label_set_ellipsize(GTK_LABEL(m_pLabel), PANGO_ELLIPSIZE_MIDDLE); + gtk_widget_set_size_request(m_pWidget, nWidth, nHeight); + } + + virtual void set_label(const OUString& rText) override + { + ::set_label(GTK_LABEL(m_pLabel), rText); + } + + virtual OUString get_label() const override + { + return ::get_label(GTK_LABEL(m_pLabel)); + } + + virtual void set_image(VirtualDevice* pDevice) override + { + ensure_image_widget(); +#if GTK_CHECK_VERSION(4, 0, 0) + picture_set_from_virtual_device(m_pImage, pDevice); +#else + image_set_from_virtual_device(m_pImage, pDevice); +#endif + } + + virtual void set_image(const css::uno::Reference& rImage) override + { + ensure_image_widget(); +#if GTK_CHECK_VERSION(4, 0, 0) + picture_set_from_xgraphic(m_pImage, rImage); +#else + image_set_from_xgraphic(m_pImage, rImage); +#endif + } + +#if GTK_CHECK_VERSION(4, 0, 0) + virtual void set_from_icon_name(const OUString& rIconName) override + { + ensure_image_widget(); + picture_set_from_icon_name(m_pImage, rIconName); + } + + virtual void set_custom_button(VirtualDevice* pDevice) override + { + m_aCustomBackground.use_custom_content(pDevice); + } + + virtual void set_inconsistent(bool inconsistent) override + { + if (inconsistent) + gtk_widget_set_state_flags(GTK_WIDGET(m_pMenuButton), GTK_STATE_FLAG_INCONSISTENT, false); + else + gtk_widget_unset_state_flags(GTK_WIDGET(m_pMenuButton), GTK_STATE_FLAG_INCONSISTENT); + } + + virtual bool get_inconsistent() const override + { + return gtk_widget_get_state_flags(GTK_WIDGET(m_pMenuButton)) & GTK_STATE_FLAG_INCONSISTENT; + } + + virtual void set_active(bool active) override + { + disable_notify_events(); + set_inconsistent(false); + if (active) + gtk_menu_button_popup(m_pMenuButton); + else + gtk_menu_button_popdown(m_pMenuButton); + enable_notify_events(); + } + + virtual bool get_active() const override + { + GtkPopover* pPopover = gtk_menu_button_get_popover(m_pMenuButton); + return pPopover && gtk_widget_get_visible(GTK_WIDGET(pPopover)); + } + + virtual void set_font(const vcl::Font& rFont) override + { + m_xFont = rFont; + GtkLabel* pChild = ::get_label_widget(GTK_WIDGET(m_pMenuButton)); + ::set_font(pChild, rFont); + } + + virtual vcl::Font get_font() override + { + if (m_xFont) + return *m_xFont; + return GtkInstanceWidget::get_font(); + } +#else + virtual void set_active(bool bActive) override + { + bool bWasActive = get_active(); + GtkInstanceToggleButton::set_active(bActive); + if (bWasActive && !bActive && gtk_widget_get_focus_on_click(GTK_WIDGET(m_pMenuButton))) + { + // grab focus back to the toggle button if the menu was popped down + gtk_widget_grab_focus(GTK_WIDGET(m_pMenuButton)); + } + } +#endif + + virtual void insert_item(int pos, const OUString& rId, const OUString& rStr, + const OUString* pIconName, VirtualDevice* pImageSurface, TriState eCheckRadioFalse) override + { + MenuHelper::insert_item(pos, rId, rStr, pIconName, pImageSurface, eCheckRadioFalse); + } + + virtual void insert_separator(int pos, const OUString& rId) override + { + MenuHelper::insert_separator(pos, rId); + } + + virtual void remove_item(const OUString& rId) override + { + MenuHelper::remove_item(rId); + } + + virtual void clear() override + { + MenuHelper::clear_items(); + } + + virtual void set_item_active(const OUString& rIdent, bool bActive) override + { + MenuHelper::set_item_active(rIdent, bActive); + } + + virtual void set_item_sensitive(const OUString& rIdent, bool bSensitive) override + { + MenuHelper::set_item_sensitive(rIdent, bSensitive); + } + + virtual void set_item_label(const OUString& rIdent, const OUString& rLabel) override + { + MenuHelper::set_item_label(rIdent, rLabel); + } + + virtual OUString get_item_label(const OUString& rIdent) const override + { + return MenuHelper::get_item_label(rIdent); + } + + virtual void set_item_visible(const OUString& rIdent, bool bVisible) override + { + MenuHelper::set_item_visible(rIdent, bVisible); + } + + virtual void signal_item_activate(const OUString& rIdent) override + { + signal_selected(rIdent); + } + + virtual void set_popover(weld::Widget* pPopover) override + { + GtkInstanceWidget* pPopoverWidget = dynamic_cast(pPopover); + m_pPopover = pPopoverWidget ? pPopoverWidget->getWidget() : nullptr; + +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_menu_button_set_popover(m_pMenuButton, m_pPopover); + update_action_group_from_popover_model(); + return; +#else + + if (!m_pPopover) + { + gtk_menu_button_set_popover(m_pMenuButton, nullptr); + return; + } + + if (!m_pMenuHack) + { + //under wayland a Popover will work to "escape" the parent dialog, not + //so under X, so come up with this hack to use a raw GtkWindow + GdkDisplay *pDisplay = gtk_widget_get_display(m_pWidget); + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay) && gtk_popover_get_constrain_to(GTK_POPOVER(m_pPopover)) == GTK_POPOVER_CONSTRAINT_NONE) + { + m_pMenuHack = GTK_WINDOW(gtk_window_new(GTK_WINDOW_POPUP)); + gtk_window_set_type_hint(m_pMenuHack, GDK_WINDOW_TYPE_HINT_COMBO); + // See writer "format, watermark" for true here. Can't interact with the replacement popover otherwise. + gtk_window_set_modal(m_pMenuHack, true); + gtk_window_set_resizable(m_pMenuHack, false); + m_nSignalId = g_signal_connect(GTK_TOGGLE_BUTTON(m_pMenuButton), "toggled", G_CALLBACK(signalMenuButtonToggled), this); + g_signal_connect(m_pMenuHack, "key-press-event", G_CALLBACK(keyPress), this); + g_signal_connect(m_pMenuHack, "grab-broken-event", G_CALLBACK(signalGrabBroken), this); + g_signal_connect(m_pMenuHack, "button-press-event", G_CALLBACK(signalButtonPress), this); + g_signal_connect(m_pMenuHack, "button-release-event", G_CALLBACK(signalButtonRelease), this); + } + } + + if (m_pMenuHack) + { + GtkWidget* pPlaceHolder = gtk_popover_new(GTK_WIDGET(m_pMenuButton)); + gtk_popover_set_transitions_enabled(GTK_POPOVER(pPlaceHolder), false); + + // tdf#132540 theme the unwanted popover into invisibility + GtkStyleContext *pPopoverContext = gtk_widget_get_style_context(pPlaceHolder); + GtkCssProvider *pProvider = gtk_css_provider_new(); + static const gchar data[] = "popover { box-shadow: none; padding: 0 0 0 0; margin: 0 0 0 0; border-image: none; border-image-width: 0 0 0 0; background-image: none; background-color: transparent; border-radius: 0 0 0 0; border-width: 0 0 0 0; border-style: none; border-color: transparent; opacity: 0; min-height: 0; min-width: 0; }"; + css_provider_load_from_data(pProvider, data, -1); + gtk_style_context_add_provider(pPopoverContext, GTK_STYLE_PROVIDER(pProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + gtk_menu_button_set_popover(m_pMenuButton, pPlaceHolder); + } + else + { + gtk_menu_button_set_popover(m_pMenuButton, m_pPopover); + gtk_widget_show_all(m_pPopover); + } +#endif + } + + void set_menu(weld::Menu* pMenu); + + static GtkBox* formatMenuButton(GtkWidget* pLabel) + { + // format the GtkMenuButton "manually" so we can have the dropdown image in GtkMenuButtons shown + // on the right at the same time as an image is shown on the left + g_object_ref(pLabel); + GtkWidget* pContainer = gtk_widget_get_parent(pLabel); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_remove(GTK_CONTAINER(pContainer), pLabel); +#else + gtk_box_remove(GTK_BOX(pContainer), pLabel); +#endif + + gint nImageSpacing(2); +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkStyleContext *pContext = gtk_widget_get_style_context(pContainer); + gtk_style_context_get_style(pContext, "image-spacing", &nImageSpacing, nullptr); +#endif + GtkBox* pBox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, nImageSpacing)); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_box_pack_start(pBox, pLabel, true, true, 0); +#else + gtk_widget_set_halign(pLabel, GTK_ALIGN_START); + gtk_box_prepend(pBox, pLabel); +#endif + g_object_unref(pLabel); + +#if !GTK_CHECK_VERSION(4, 0, 0) + if (gtk_toggle_button_get_mode(GTK_TOGGLE_BUTTON(pContainer))) + gtk_box_pack_end(pBox, gtk_image_new_from_icon_name("pan-down-symbolic", GTK_ICON_SIZE_BUTTON), false, false, 0); +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_add(GTK_CONTAINER(pContainer), GTK_WIDGET(pBox)); +#else + gtk_box_prepend(GTK_BOX(pContainer), GTK_WIDGET(pBox)); +#endif +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_show_all(GTK_WIDGET(pBox)); +#else + gtk_widget_show(GTK_WIDGET(pBox)); +#endif + + return pBox; + } + +#if GTK_CHECK_VERSION(4, 0, 0) + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pMenuButtonToggleButton, m_nToggledSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pMenuButtonToggleButton, m_nToggledSignalId); + } +#endif + + virtual ~GtkInstanceMenuButton() override + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pMenuButtonToggleButton, m_nToggledSignalId); + gtk_widget_insert_action_group(GTK_WIDGET(m_pMenuButton), "menu", nullptr); +#else + if (m_pMenuHack) + { + g_signal_handler_disconnect(m_pMenuButton, m_nSignalId); + gtk_menu_button_set_popover(m_pMenuButton, nullptr); + gtk_widget_destroy(GTK_WIDGET(m_pMenuHack)); + } +#endif + } +}; + +class GtkInstanceMenuToggleButton : public GtkInstanceToggleButton, public MenuHelper + , public virtual weld::MenuToggleButton +{ +private: + GtkBox* m_pContainer; + GtkButton* m_pToggleMenuButton; + GtkMenuButton* m_pMenuButton; + gulong m_nMenuBtnClickedId; + gulong m_nToggleStateFlagsChangedId; + gulong m_nMenuBtnStateFlagsChangedId; + + static void signalToggleStateFlagsChanged(GtkWidget* pWidget, GtkStateFlags /*eFlags*/, gpointer widget) + { + GtkInstanceMenuToggleButton* pThis = static_cast(widget); + // mirror togglebutton state to menubutton + gtk_widget_set_state_flags(GTK_WIDGET(pThis->m_pToggleMenuButton), gtk_widget_get_state_flags(pWidget), true); + } + + static void signalMenuBtnStateFlagsChanged(GtkWidget* pWidget, GtkStateFlags /*eFlags*/, gpointer widget) + { + GtkInstanceMenuToggleButton* pThis = static_cast(widget); + // mirror menubutton to togglebutton, keeping depressed state of menubutton + GtkStateFlags eToggleFlags = gtk_widget_get_state_flags(GTK_WIDGET(pThis->m_pToggleButton)); + GtkStateFlags eFlags = gtk_widget_get_state_flags(pWidget); + GtkStateFlags eFinalFlags = static_cast((eFlags & ~GTK_STATE_FLAG_ACTIVE) | + (eToggleFlags & GTK_STATE_FLAG_ACTIVE)); + gtk_widget_set_state_flags(GTK_WIDGET(pThis->m_pToggleButton), eFinalFlags, true); + } + + static void signalMenuBtnClicked(GtkButton*, gpointer widget) + { + GtkInstanceMenuToggleButton* pThis = static_cast(widget); + pThis->launch_menu(); + } + + void launch_menu() + { + gtk_widget_set_state_flags(GTK_WIDGET(m_pToggleMenuButton), gtk_widget_get_state_flags(GTK_WIDGET(m_pToggleButton)), true); + GtkWidget* pWidget = GTK_WIDGET(m_pToggleButton); + + //run in a sub main loop because we need to keep vcl PopupMenu alive to use + //it during DispatchCommand, returning now to the outer loop causes the + //launching PopupMenu to be destroyed, instead run the subloop here + //until the gtk menu is destroyed + GMainLoop* pLoop = g_main_loop_new(nullptr, true); + +#if GTK_CHECK_VERSION(4, 0, 0) + gulong nSignalId = g_signal_connect_swapped(G_OBJECT(m_pMenu), "closed", G_CALLBACK(g_main_loop_quit), pLoop); + + g_object_ref(m_pMenu); + gtk_menu_button_set_popover(m_pMenuButton, nullptr); + gtk_widget_set_parent(GTK_WIDGET(m_pMenu), pWidget); + gtk_popover_set_position(GTK_POPOVER(m_pMenu), GTK_POS_BOTTOM); + gtk_popover_popup(GTK_POPOVER(m_pMenu)); +#else + gulong nSignalId = g_signal_connect_swapped(G_OBJECT(m_pMenu), "deactivate", G_CALLBACK(g_main_loop_quit), pLoop); + +#if GTK_CHECK_VERSION(3,22,0) + if (gtk_check_version(3, 22, 0) == nullptr) + { + // Send a keyboard event through gtk_main_do_event to toggle any active tooltip offs + // before trying to launch the menu + // https://gitlab.gnome.org/GNOME/gtk/issues/1785 + // Fixed in GTK 2.34 + GdkEvent *pKeyEvent = GtkSalFrame::makeFakeKeyPress(pWidget); + gtk_main_do_event(pKeyEvent); + + GdkEvent *pTriggerEvent = gtk_get_current_event(); + if (!pTriggerEvent) + pTriggerEvent = pKeyEvent; + + gtk_menu_popup_at_widget(m_pMenu, pWidget, GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, pTriggerEvent); + + gdk_event_free(pKeyEvent); + } + else +#endif + { + guint nButton; + guint32 nTime; + + //typically there is an event, and we can then distinguish if this was + //launched from the keyboard (gets auto-mnemoniced) or the mouse (which + //doesn't) + GdkEvent *pEvent = gtk_get_current_event(); + if (pEvent) + { + gdk_event_get_button(pEvent, &nButton); + nTime = gdk_event_get_time(pEvent); + } + else + { + nButton = 0; + nTime = GtkSalFrame::GetLastInputEventTime(); + } + + gtk_menu_popup(m_pMenu, nullptr, nullptr, nullptr, nullptr, nButton, nTime); + } +#endif + + if (g_main_loop_is_running(pLoop)) + main_loop_run(pLoop); + + g_main_loop_unref(pLoop); + g_signal_handler_disconnect(m_pMenu, nSignalId); + +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_unparent(GTK_WIDGET(m_pMenu)); + gtk_menu_button_set_popover(m_pMenuButton, GTK_WIDGET(m_pMenu)); + g_object_unref(m_pMenu); +#endif + + } + + static gboolean signalMenuToggleButton(GtkWidget*, gboolean bGroupCycling, gpointer widget) + { + GtkInstanceMenuToggleButton* pThis = static_cast(widget); + return gtk_widget_mnemonic_activate(GTK_WIDGET(pThis->m_pToggleButton), bGroupCycling); + } + +public: + GtkInstanceMenuToggleButton(GtkBuilder* pMenuToggleButtonBuilder, GtkMenuButton* pMenuButton, + GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceToggleButton(GTK_TOGGLE_BUTTON(gtk_builder_get_object(pMenuToggleButtonBuilder, "togglebutton")), + pBuilder, bTakeOwnership) +#if !GTK_CHECK_VERSION(4, 0, 0) + , MenuHelper(gtk_menu_button_get_popup(pMenuButton), false) +#else + , MenuHelper(GTK_POPOVER_MENU(gtk_menu_button_get_popover(pMenuButton)), false) +#endif + , m_pContainer(GTK_BOX(gtk_builder_get_object(pMenuToggleButtonBuilder, "box"))) + , m_pToggleMenuButton(GTK_BUTTON(gtk_builder_get_object(pMenuToggleButtonBuilder, "menubutton"))) + , m_pMenuButton(pMenuButton) + , m_nMenuBtnClickedId(g_signal_connect(m_pToggleMenuButton, "clicked", G_CALLBACK(signalMenuBtnClicked), this)) + , m_nToggleStateFlagsChangedId(g_signal_connect(m_pToggleButton, "state-flags-changed", G_CALLBACK(signalToggleStateFlagsChanged), this)) + , m_nMenuBtnStateFlagsChangedId(g_signal_connect(m_pToggleMenuButton, "state-flags-changed", G_CALLBACK(signalMenuBtnStateFlagsChanged), this)) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkInstanceMenuButton::formatMenuButton(gtk_bin_get_child(GTK_BIN(m_pMenuButton))); +#endif + + insertAsParent(GTK_WIDGET(m_pMenuButton), GTK_WIDGET(m_pContainer)); + gtk_widget_hide(GTK_WIDGET(m_pMenuButton)); + + // move the first GtkMenuButton child, as created by GtkInstanceMenuButton ctor, into the GtkToggleButton + // instead, leaving just the indicator behind in the GtkMenuButton +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pButtonBox = gtk_bin_get_child(GTK_BIN(m_pMenuButton)); + GList* pChildren = gtk_container_get_children(GTK_CONTAINER(pButtonBox)); + int nGroup = 0; + for (GList* pChild = g_list_first(pChildren); pChild && nGroup < 2; pChild = g_list_next(pChild), ++nGroup) + { + GtkWidget* pWidget = static_cast(pChild->data); + g_object_ref(pWidget); + gtk_container_remove(GTK_CONTAINER(pButtonBox), pWidget); + if (nGroup == 0) + gtk_container_add(GTK_CONTAINER(m_pToggleButton), pWidget); + else + gtk_container_add(GTK_CONTAINER(m_pToggleMenuButton), pWidget); + gtk_widget_show_all(pWidget); + g_object_unref(pWidget); + } + g_list_free(pChildren); +#else + GtkWidget* pChild; + if (gtk_check_version(4, 5, 0) == nullptr) + { + pChild = gtk_widget_get_first_child(GTK_WIDGET(m_pMenuButton)); + pChild = gtk_widget_get_first_child(pChild); + pChild = gtk_widget_get_first_child(pChild); + } + else + pChild = gtk_widget_get_last_child(GTK_WIDGET(m_pMenuButton)); + g_object_ref(pChild); + gtk_widget_unparent(pChild); + gtk_button_set_child(GTK_BUTTON(m_pToggleButton), pChild); + g_object_unref(pChild); +#endif + + // match the GtkToggleButton relief to the GtkMenuButton +#if !GTK_CHECK_VERSION(4, 0, 0) + const GtkReliefStyle eStyle = gtk_button_get_relief(GTK_BUTTON(m_pMenuButton)); + gtk_button_set_relief(GTK_BUTTON(m_pToggleButton), eStyle); + gtk_button_set_relief(GTK_BUTTON(m_pToggleMenuButton), eStyle); +#else + const bool bStyle = gtk_menu_button_get_has_frame(GTK_MENU_BUTTON(m_pMenuButton)); + gtk_button_set_has_frame(GTK_BUTTON(m_pToggleButton), bStyle); + gtk_button_set_has_frame(GTK_BUTTON(m_pToggleMenuButton), bStyle); +#endif + + // move the GtkMenuButton margins up to the new parent + gtk_widget_set_margin_top(GTK_WIDGET(m_pContainer), + gtk_widget_get_margin_top(GTK_WIDGET(m_pMenuButton))); + gtk_widget_set_margin_bottom(GTK_WIDGET(m_pContainer), + gtk_widget_get_margin_bottom(GTK_WIDGET(m_pMenuButton))); + gtk_widget_set_margin_start(GTK_WIDGET(m_pContainer), + gtk_widget_get_margin_start(GTK_WIDGET(m_pMenuButton))); + gtk_widget_set_margin_end(GTK_WIDGET(m_pContainer), + gtk_widget_get_margin_end(GTK_WIDGET(m_pMenuButton))); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_menu_detach(m_pMenu); + gtk_menu_attach_to_widget(m_pMenu, GTK_WIDGET(m_pToggleButton), nullptr); +#else + gtk_widget_insert_action_group(GTK_WIDGET(m_pContainer), "menu", m_pActionGroup); + + update_action_group_from_popover_model(); +#endif + + g_signal_connect(m_pContainer, "mnemonic-activate", G_CALLBACK(signalMenuToggleButton), this); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pToggleMenuButton, m_nMenuBtnClickedId); + GtkInstanceToggleButton::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceToggleButton::enable_notify_events(); + g_signal_handler_unblock(m_pToggleMenuButton, m_nMenuBtnClickedId); + } + + virtual ~GtkInstanceMenuToggleButton() + { + g_signal_handler_disconnect(m_pToggleButton, m_nToggleStateFlagsChangedId); + g_signal_handler_disconnect(m_pToggleMenuButton, m_nMenuBtnStateFlagsChangedId); + g_signal_handler_disconnect(m_pToggleMenuButton, m_nMenuBtnClickedId); + +#if GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pChild = gtk_button_get_child(GTK_BUTTON(m_pToggleButton)); + g_object_ref(pChild); + gtk_button_set_child(GTK_BUTTON(m_pToggleButton), nullptr); + gtk_widget_unparent(pChild); + gtk_widget_set_parent(pChild, GTK_WIDGET(m_pMenuButton)); + g_object_unref(pChild); +#endif + } + + virtual void insert_item(int pos, const OUString& rId, const OUString& rStr, + const OUString* pIconName, VirtualDevice* pImageSurface, TriState eCheckRadioFalse) override + { + MenuHelper::insert_item(pos, rId, rStr, pIconName, pImageSurface, eCheckRadioFalse); + } + + virtual void insert_separator(int pos, const OUString& rId) override + { + MenuHelper::insert_separator(pos, rId); + } + + virtual void remove_item(const OUString& rId) override + { + MenuHelper::remove_item(rId); + } + + virtual void clear() override + { + MenuHelper::clear_items(); + } + + virtual void set_item_active(const OUString& rIdent, bool bActive) override + { + MenuHelper::set_item_active(rIdent, bActive); + } + + virtual void set_item_sensitive(const OUString& rIdent, bool bSensitive) override + { + MenuHelper::set_item_sensitive(rIdent, bSensitive); + } + + virtual void set_item_label(const OUString& rIdent, const OUString& rLabel) override + { + MenuHelper::set_item_label(rIdent, rLabel); + } + + virtual OUString get_item_label(const OUString& rIdent) const override + { + return MenuHelper::get_item_label(rIdent); + } + + virtual void set_item_visible(const OUString& rIdent, bool bVisible) override + { + MenuHelper::set_item_visible(rIdent, bVisible); + } + + virtual void signal_item_activate(const OUString& rIdent) override + { + signal_selected(rIdent); + } + + virtual void set_popover(weld::Widget* /*pPopover*/) override + { + assert(false && "not implemented"); + } +}; + +class GtkInstanceMenu : public MenuHelper, public virtual weld::Menu +{ +protected: +#if !GTK_CHECK_VERSION(4, 0, 0) + std::vector m_aExtraItems; +#endif + OUString m_sActivated; +#if !GTK_CHECK_VERSION(4, 0, 0) + MenuHelper* m_pTopLevelMenuHelper; +#endif + +private: + virtual void signal_item_activate(const OUString& rIdent) override + { + m_sActivated = rIdent; + weld::Menu::signal_activate(m_sActivated); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + void clear_extras() + { + if (m_aExtraItems.empty()) + return; + if (m_pTopLevelMenuHelper) + { + for (auto a : m_aExtraItems) + m_pTopLevelMenuHelper->remove_from_map(a); + } + m_aExtraItems.clear(); + } +#endif + +public: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkInstanceMenu(GtkMenu* pMenu, bool bTakeOwnership) +#else + GtkInstanceMenu(GtkPopoverMenu* pMenu, bool bTakeOwnership) +#endif + : MenuHelper(pMenu, bTakeOwnership) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_pTopLevelMenuHelper(nullptr) +#endif + { + g_object_set_data(G_OBJECT(m_pMenu), "g-lo-GtkInstanceMenu", this); +#if !GTK_CHECK_VERSION(4, 0, 0) + // tdf#122527 if we're welding a submenu of a menu of a MenuButton, + // then find that MenuButton parent so that when adding items to this + // menu we can inform the MenuButton of their addition + GtkMenu* pTopLevelMenu = pMenu; + while (true) + { + GtkWidget* pAttached = gtk_menu_get_attach_widget(pTopLevelMenu); + if (!pAttached || !GTK_IS_MENU_ITEM(pAttached)) + break; + GtkWidget* pParent = gtk_widget_get_parent(pAttached); + if (!pParent || !GTK_IS_MENU(pParent)) + break; + pTopLevelMenu = GTK_MENU(pParent); + } + if (pTopLevelMenu == pMenu) + return; + + // maybe the toplevel is a menubutton + GtkWidget* pAttached = gtk_menu_get_attach_widget(pTopLevelMenu); + if (pAttached && GTK_IS_MENU_BUTTON(pAttached)) + { + void* pData = g_object_get_data(G_OBJECT(pAttached), "g-lo-GtkInstanceButton"); + m_pTopLevelMenuHelper = dynamic_cast(static_cast(pData)); + } + // or maybe a menu + if (!m_pTopLevelMenuHelper) + { + void* pData = g_object_get_data(G_OBJECT(pTopLevelMenu), "g-lo-GtkInstanceMenu"); + m_pTopLevelMenuHelper = static_cast(pData); + } +#else + update_action_group_from_popover_model(); +#endif + } + + virtual OUString popup_at_rect(weld::Widget* pParent, const tools::Rectangle& rRect, weld::Placement ePlace) override + { + m_sActivated.clear(); + + GtkInstanceWidget* pGtkWidget = dynamic_cast(pParent); + assert(pGtkWidget); + GtkWidget* pWidget = pGtkWidget->getWidget(); + + //run in a sub main loop because we need to keep vcl PopupMenu alive to use + //it during DispatchCommand, returning now to the outer loop causes the + //launching PopupMenu to be destroyed, instead run the subloop here + //until the gtk menu is destroyed + GMainLoop* pLoop = g_main_loop_new(nullptr, true); + +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_insert_action_group(pWidget, "menu", m_pActionGroup); + + gulong nSignalId = g_signal_connect_swapped(G_OBJECT(m_pMenu), "closed", G_CALLBACK(g_main_loop_quit), pLoop); + + GdkRectangle aRect; + pWidget = getPopupRect(pWidget, rRect, aRect); + + GtkWidget* pOrigParent = gtk_widget_get_parent(GTK_WIDGET(m_pMenu)); + gtk_widget_set_parent(GTK_WIDGET(m_pMenu), pWidget); + gtk_popover_set_pointing_to(GTK_POPOVER(m_pMenu), &aRect); + if (ePlace == weld::Placement::Under) + gtk_popover_set_position(GTK_POPOVER(m_pMenu), GTK_POS_BOTTOM); + else + { + if (SwapForRTL(pWidget)) + gtk_popover_set_position(GTK_POPOVER(m_pMenu), GTK_POS_LEFT); + else + gtk_popover_set_position(GTK_POPOVER(m_pMenu), GTK_POS_RIGHT); + } + gtk_popover_popup(GTK_POPOVER(m_pMenu)); +#else + gulong nSignalId = g_signal_connect_swapped(G_OBJECT(m_pMenu), "deactivate", G_CALLBACK(g_main_loop_quit), pLoop); + +#if GTK_CHECK_VERSION(3,22,0) + if (gtk_check_version(3, 22, 0) == nullptr) + { + GdkRectangle aRect; + pWidget = getPopupRect(pWidget, rRect, aRect); + gtk_menu_attach_to_widget(m_pMenu, pWidget, nullptr); + + // Send a keyboard event through gtk_main_do_event to toggle any active tooltip offs + // before trying to launch the menu + // https://gitlab.gnome.org/GNOME/gtk/issues/1785 + // Fixed in GTK 2.34 + GdkEvent *pKeyEvent = GtkSalFrame::makeFakeKeyPress(pWidget); + gtk_main_do_event(pKeyEvent); + + GdkEvent *pTriggerEvent = gtk_get_current_event(); + if (!pTriggerEvent) + pTriggerEvent = pKeyEvent; + + bool bSwapForRTL = SwapForRTL(pWidget); + + if (ePlace == weld::Placement::Under) + { + if (bSwapForRTL) + gtk_menu_popup_at_rect(m_pMenu, widget_get_surface(pWidget), &aRect, GDK_GRAVITY_SOUTH_EAST, GDK_GRAVITY_NORTH_EAST, pTriggerEvent); + else + gtk_menu_popup_at_rect(m_pMenu, widget_get_surface(pWidget), &aRect, GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, pTriggerEvent); + } + else + { + if (bSwapForRTL) + gtk_menu_popup_at_rect(m_pMenu, widget_get_surface(pWidget), &aRect, GDK_GRAVITY_NORTH_WEST, GDK_GRAVITY_NORTH_EAST, pTriggerEvent); + else + gtk_menu_popup_at_rect(m_pMenu, widget_get_surface(pWidget), &aRect, GDK_GRAVITY_NORTH_EAST, GDK_GRAVITY_NORTH_WEST, pTriggerEvent); + } + + gdk_event_free(pKeyEvent); + } + else +#else + (void) rRect; +#endif + { + gtk_menu_attach_to_widget(m_pMenu, pWidget, nullptr); + + guint nButton; + guint32 nTime; + + //typically there is an event, and we can then distinguish if this was + //launched from the keyboard (gets auto-mnemoniced) or the mouse (which + //doesn't) + GdkEvent *pEvent = gtk_get_current_event(); + if (pEvent) + { + if (!gdk_event_get_button(pEvent, &nButton)) + nButton = 0; + nTime = gdk_event_get_time(pEvent); + } + else + { + nButton = 0; + nTime = GtkSalFrame::GetLastInputEventTime(); + } + + gtk_menu_popup(m_pMenu, nullptr, nullptr, nullptr, nullptr, nButton, nTime); + } +#endif + + if (g_main_loop_is_running(pLoop)) + main_loop_run(pLoop); + + g_main_loop_unref(pLoop); + g_signal_handler_disconnect(m_pMenu, nSignalId); + +#if GTK_CHECK_VERSION(4, 0, 0) + if (!pOrigParent) + gtk_widget_unparent(GTK_WIDGET(m_pMenu)); + else + gtk_widget_set_parent(GTK_WIDGET(m_pMenu), pOrigParent); + + gtk_widget_insert_action_group(pWidget, "menu", nullptr); +#else + gtk_menu_detach(m_pMenu); +#endif + + return m_sActivated; + } + + virtual void set_sensitive(const OUString& rIdent, bool bSensitive) override + { + set_item_sensitive(rIdent, bSensitive); + } + + virtual bool get_sensitive(const OUString& rIdent) const override + { + return get_item_sensitive(rIdent); + } + + virtual void set_active(const OUString& rIdent, bool bActive) override + { + set_item_active(rIdent, bActive); + } + + virtual bool get_active(const OUString& rIdent) const override + { + return get_item_active(rIdent); + } + + virtual void set_visible(const OUString& rIdent, bool bShow) override + { + set_item_visible(rIdent, bShow); + } + + virtual void set_label(const OUString& rIdent, const OUString& rLabel) override + { + set_item_label(rIdent, rLabel); + } + + virtual OUString get_label(const OUString& rIdent) const override + { + return get_item_label(rIdent); + } + + virtual void insert_separator(int pos, const OUString& rId) override + { + MenuHelper::insert_separator(pos, rId); + } + + virtual void clear() override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + clear_extras(); +#endif + MenuHelper::clear_items(); + } + + virtual void insert(int pos, const OUString& rId, const OUString& rStr, + const OUString* pIconName, VirtualDevice* pImageSurface, + const css::uno::Reference& rGraphic, + TriState eCheckRadioFalse) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pImage = nullptr; + if (pIconName) + pImage = image_new_from_icon_name(*pIconName); + else if (pImageSurface) + { + pImage = image_new_from_virtual_device(*pImageSurface); + } + else if (rGraphic) + { + pImage = image_new_from_xgraphic(rGraphic, false); + } + + GtkWidget *pItem; + if (pImage) + { + GtkBox *pBox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6)); + GtkWidget *pLabel = gtk_label_new_with_mnemonic(MapToGtkAccelerator(rStr).getStr()); + gtk_label_set_xalign(GTK_LABEL(pLabel), 0.0); + pItem = eCheckRadioFalse != TRISTATE_INDET ? gtk_check_menu_item_new() : gtk_menu_item_new(); + gtk_box_pack_start(pBox, pImage, false, true, 0); + gtk_box_pack_start(pBox, pLabel, true, true, 0); + gtk_container_add(GTK_CONTAINER(pItem), GTK_WIDGET(pBox)); + gtk_widget_show_all(pItem); + } + else + { + pItem = eCheckRadioFalse != TRISTATE_INDET ? gtk_check_menu_item_new_with_mnemonic(MapToGtkAccelerator(rStr).getStr()) + : gtk_menu_item_new_with_mnemonic(MapToGtkAccelerator(rStr).getStr()); + } + + if (eCheckRadioFalse == TRISTATE_FALSE) + gtk_check_menu_item_set_draw_as_radio(GTK_CHECK_MENU_ITEM(pItem), true); + + ::set_buildable_id(GTK_BUILDABLE(pItem), rId); + gtk_menu_shell_append(GTK_MENU_SHELL(m_pMenu), pItem); + gtk_widget_show(pItem); + GtkMenuItem* pMenuItem = GTK_MENU_ITEM(pItem); + m_aExtraItems.push_back(pMenuItem); + add_to_map(pMenuItem); + if (m_pTopLevelMenuHelper) + m_pTopLevelMenuHelper->add_to_map(pMenuItem); + if (pos != -1) + gtk_menu_reorder_child(m_pMenu, pItem, pos); +#else + SAL_WARN("vcl.gtk", "needs to be implemented for gtk4"); + (void)pIconName; + (void)pImageSurface; + (void)rGraphic; + + if (GMenuModel* pMenuModel = m_pMenu ? gtk_popover_menu_get_menu_model(m_pMenu) : nullptr) + { + auto aSectionAndPos = get_section_and_pos_for(pMenuModel, pos); + GMenu* pMenu = G_MENU(aSectionAndPos.first); + // action with a target value ... the action name and target value are separated by a double + // colon ... For example: "app.action::target" + OUString sActionAndTarget; + if (eCheckRadioFalse == TRISTATE_INDET) + sActionAndTarget = "menu.normal." + rId + "::" + rId; + else + sActionAndTarget = "menu.radio." + rId + "::" + rId; + g_menu_insert(pMenu, aSectionAndPos.second, MapToGtkAccelerator(rStr).getStr(), sActionAndTarget.toUtf8().getStr()); + + assert(eCheckRadioFalse == TRISTATE_INDET); // come back to this later + + // TODO not redo entire group + update_action_group_from_popover_model(); + } + +#endif + } + + virtual OUString get_id(int pos) const override + { + return get_item_id(pos); + } + + virtual int n_children() const override + { + return get_n_children(); + } + + virtual void set_item_help_id(const OUString& rIdent, const OUString& rHelpId) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + ::set_help_id(GTK_WIDGET(m_aMap[rIdent]), rHelpId); +#else + (void)rIdent; + (void)rHelpId; +#endif + } + + void remove(const OUString& rIdent) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!m_aExtraItems.empty()) + { + GtkMenuItem* pMenuItem = m_aMap[rIdent]; + auto iter = std::find(m_aExtraItems.begin(), m_aExtraItems.end(), pMenuItem); + if (iter != m_aExtraItems.end()) + { + if (m_pTopLevelMenuHelper) + m_pTopLevelMenuHelper->remove_from_map(pMenuItem); + m_aExtraItems.erase(iter); + } + } +#endif + MenuHelper::remove_item(rIdent); + } + + virtual ~GtkInstanceMenu() override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + clear_extras(); +#endif + g_object_steal_data(G_OBJECT(m_pMenu), "g-lo-GtkInstanceMenu"); + } +}; + +#if !GTK_CHECK_VERSION(4, 0, 0) + vcl::ImageType GtkToVcl(GtkIconSize eSize) + { + vcl::ImageType eRet; + switch (eSize) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + case GTK_ICON_SIZE_MENU: + case GTK_ICON_SIZE_SMALL_TOOLBAR: + case GTK_ICON_SIZE_BUTTON: + eRet = vcl::ImageType::Size16; + break; + case GTK_ICON_SIZE_LARGE_TOOLBAR: + eRet = vcl::ImageType::Size26; + break; + case GTK_ICON_SIZE_DND: + case GTK_ICON_SIZE_DIALOG: + eRet = vcl::ImageType::Size32; + break; + default: + case GTK_ICON_SIZE_INVALID: + eRet = vcl::ImageType::Small; + break; +#else + case GTK_ICON_SIZE_LARGE: + eRet = vcl::ImageType::Size32; + break; + case GTK_ICON_SIZE_NORMAL: + default: + eRet = vcl::ImageType::Size16; + break; +#endif + } + return eRet; + } + + GtkIconSize VclToGtk(vcl::ImageType eSize) + { + GtkIconSize eRet; +#if !GTK_CHECK_VERSION(4, 0, 0) + switch (eSize) + { + case vcl::ImageType::Size16: + eRet = GTK_ICON_SIZE_SMALL_TOOLBAR; + break; + case vcl::ImageType::Size26: + eRet = GTK_ICON_SIZE_LARGE_TOOLBAR; + break; + case vcl::ImageType::Size32: + eRet = GTK_ICON_SIZE_DIALOG; + break; + default: + O3TL_UNREACHABLE; + } +#else + switch (eSize) + { + case vcl::ImageType::Size26: + case vcl::ImageType::Size32: + eRet = GTK_ICON_SIZE_LARGE; + break; + case vcl::ImageType::Size16: + default: + eRet = GTK_ICON_SIZE_NORMAL; + break; + } +#endif + return eRet; + } + + // tdf#153885 for wayland if the popover window is the application + // window, constrain it within the application window so it won't + // be cut off screen. Leave dialog hosted ones alone, like + // format, watermark, which are likely presented in the middle + // of the screen and are too small to constrain the popover inside. + void ConstrainApplicationWindowPopovers(GtkToggleButton* pItem) + { +#if defined(GDK_WINDOWING_WAYLAND) + GdkDisplay *pDisplay = gtk_widget_get_display(GTK_WIDGET(pItem)); + if (DLSYM_GDK_IS_WAYLAND_DISPLAY(pDisplay) && GTK_IS_MENU_BUTTON(pItem)) + { + GtkMenuButton* pMenuButton = GTK_MENU_BUTTON(pItem); + if (GtkPopover* pPopover = gtk_menu_button_get_popover(pMenuButton)) + { + if (gtk_popover_get_constrain_to(pPopover) == GTK_POPOVER_CONSTRAINT_NONE) + { + GtkWidget* pTopLevel = widget_get_toplevel(GTK_WIDGET(pItem)); + GtkSalFrame* pFrame = pTopLevel ? GtkSalFrame::getFromWindow(pTopLevel) : nullptr; + if (pFrame) + { + // the toplevel is an application window + gtk_popover_set_constrain_to(pPopover, GTK_POPOVER_CONSTRAINT_WINDOW); + } + } + } + } +#else + (void)pItem; +#endif + } + +#endif +} + +void GtkInstanceMenuButton::set_menu(weld::Menu* pMenu) +{ + GtkInstanceMenu* pPopoverWidget = dynamic_cast(pMenu); + m_pPopover = nullptr; + m_pMenu = pPopoverWidget ? pPopoverWidget->getMenu() : nullptr; + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_menu_button_set_popup(m_pMenuButton, GTK_WIDGET(m_pMenu)); +#else + gtk_menu_button_set_popover(m_pMenuButton, GTK_WIDGET(m_pMenu)); + update_action_group_from_popover_model(); +#endif +} + +namespace { + +class GtkInstanceToolbar : public GtkInstanceWidget, public virtual weld::Toolbar +{ +private: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkToolbar* m_pToolbar; +#else + GtkBox* m_pToolbar; + vcl::ImageType m_eImageType; +#endif + GtkCssProvider *m_pMenuButtonProvider; + + std::map m_aMap; + std::map> m_aMenuButtonMap; + std::map m_aMirroredMap; + +#if !GTK_CHECK_VERSION(4, 0, 0) + // at the time of writing there is no gtk_menu_tool_button_set_popover available + // though there will be in the future + // https://gitlab.gnome.org/GNOME/gtk/commit/03e30431a8af9a947a0c4ccab545f24da16bfe17?w=1 + static void find_menu_button(GtkWidget *pWidget, gpointer user_data) + { + if (g_strcmp0(gtk_widget_get_name(pWidget), "GtkMenuButton") == 0) + { + GtkWidget **ppToggleButton = static_cast(user_data); + *ppToggleButton = pWidget; + } + else if (GTK_IS_CONTAINER(pWidget)) + gtk_container_forall(GTK_CONTAINER(pWidget), find_menu_button, user_data); + } + + static void find_menupeer_button(GtkWidget *pWidget, gpointer user_data) + { + if (g_strcmp0(gtk_widget_get_name(pWidget), "GtkButton") == 0) + { + GtkWidget **ppButton = static_cast(user_data); + *ppButton = pWidget; + } + else if (GTK_IS_CONTAINER(pWidget)) + gtk_container_forall(GTK_CONTAINER(pWidget), find_menupeer_button, user_data); + } +#endif + + static void collect(GtkWidget* pItem, gpointer widget) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!GTK_IS_TOOL_ITEM(pItem)) + return; +#endif + GtkInstanceToolbar* pThis = static_cast(widget); + + GtkMenuButton* pMenuButton = nullptr; +#if !GTK_CHECK_VERSION(4, 0, 0) + if (GTK_IS_MENU_TOOL_BUTTON(pItem)) + find_menu_button(pItem, &pMenuButton); +#else + if (GTK_IS_MENU_BUTTON(pItem)) + pMenuButton = GTK_MENU_BUTTON(pItem); +#endif + + pThis->add_to_map(pItem, pMenuButton); + } + + void add_to_map(GtkWidget* pToolItem, GtkMenuButton* pMenuButton) + { + OUString id = ::get_buildable_id(GTK_BUILDABLE(pToolItem)); + m_aMap[id] = pToolItem; + if (pMenuButton) + { + m_aMenuButtonMap[id] = std::make_unique(pMenuButton, GTK_WIDGET(pToolItem), m_pBuilder, false); + // so that, e.g. with focus initially in writer main document then + // after clicking the heading menu in the writer navigator focus is + // left in the main document and not in the toolbar +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_button_set_focus_on_click(GTK_BUTTON(pMenuButton), false); + g_signal_connect(pMenuButton, "toggled", G_CALLBACK(signalItemToggled), this); +#else + gtk_widget_set_focus_on_click(GTK_WIDGET(pMenuButton), false); + + GtkWidget* pToggleButton = gtk_widget_get_first_child(GTK_WIDGET(pMenuButton)); + assert(GTK_IS_TOGGLE_BUTTON(pToggleButton)); + g_signal_connect(pToggleButton, "toggled", G_CALLBACK(signalItemToggled), this); +#endif + + // by default the GtkMenuButton down arrow button is as wide as + // a normal button and LibreOffice's original ones are very + // narrow, that assumption is fairly baked into the toolbar and + // sidebar designs, try and minimize the width of the dropdown + // zone. + GtkStyleContext *pButtonContext = gtk_widget_get_style_context(GTK_WIDGET(pMenuButton)); + + if (!m_pMenuButtonProvider) + { + m_pMenuButtonProvider = gtk_css_provider_new(); + static const gchar data[] = "* { " + "padding: 0;" + "margin-left: 0px;" + "margin-right: 0px;" + "min-width: 4px;" + "}"; + css_provider_load_from_data(m_pMenuButtonProvider, data, -1); + } + + gtk_style_context_add_provider(pButtonContext, + GTK_STYLE_PROVIDER(m_pMenuButtonProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!GTK_IS_TOOL_BUTTON(pToolItem)) +#else + if (!GTK_IS_BUTTON(pToolItem)) +#endif + { + return; + } + g_signal_connect(pToolItem, "clicked", G_CALLBACK(signalItemClicked), this); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static void signalItemClicked(GtkToolButton* pItem, gpointer widget) +#else + static void signalItemClicked(GtkButton* pItem, gpointer widget) +#endif + { + GtkInstanceToolbar* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_item_clicked(pItem); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + void signal_item_clicked(GtkToolButton* pItem) +#else + void signal_item_clicked(GtkButton* pItem) +#endif + { + signal_clicked(::get_buildable_id(GTK_BUILDABLE(pItem))); + } + + static void signalItemToggled(GtkToggleButton* pItem, gpointer widget) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + ConstrainApplicationWindowPopovers(pItem); +#endif + GtkInstanceToolbar* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_item_toggled(pItem); + } + + void signal_item_toggled(GtkToggleButton* pItem) + { + for (const auto& a : m_aMenuButtonMap) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + if (a.second->getWidget() == GTK_WIDGET(pItem)) +#else + if (a.second->getWidget() == gtk_widget_get_parent(GTK_WIDGET(pItem))) +#endif + { + signal_toggle_menu(a.first); + break; + } + } + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void set_item_image(GtkWidget* pItem, GtkWidget* pImage) + { + if (GTK_IS_BUTTON(pItem)) + gtk_button_set_child(GTK_BUTTON(pItem), pImage); + else if (GTK_IS_MENU_BUTTON(pItem)) + { + // TODO after gtk 4.6 is released require that version and drop this + static auto menu_button_set_child = reinterpret_cast(dlsym(nullptr, "gtk_menu_button_set_child")); + if (menu_button_set_child) + menu_button_set_child(GTK_MENU_BUTTON(pItem), pImage); + } + // versions of gtk4 > 4.2.1 might do this on their own + gtk_widget_remove_css_class(pItem, "text-button"); + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + static void set_item_image(GtkToolButton* pItem, const css::uno::Reference& rIcon, bool bMirror) +#else + static void set_item_image(GtkWidget* pItem, const css::uno::Reference& rIcon, bool bMirror) +#endif + { + GtkWidget* pImage = image_new_from_xgraphic(rIcon, bMirror); + if (pImage) + gtk_widget_show(pImage); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_tool_button_set_icon_widget(pItem, pImage); +#else + set_item_image(pItem, pImage); +#endif + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + void set_item_image(GtkToolButton* pItem, const VirtualDevice* pDevice) +#else + void set_item_image(GtkWidget* pItem, const VirtualDevice* pDevice) +#endif + { + GtkWidget* pImage = nullptr; + + if (pDevice) + { +#if GTK_CHECK_VERSION(4, 0, 0) + pImage = picture_new_from_virtual_device(*pDevice); +#else + pImage = image_new_from_virtual_device(*pDevice); +#endif + gtk_widget_show(pImage); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_tool_button_set_icon_widget(pItem, pImage); +#else + set_item_image(pItem, pImage); +#endif + gtk_widget_queue_draw(GTK_WIDGET(m_pToolbar)); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* toolbar_get_nth_item(int nIndex) const + { + return GTK_WIDGET(gtk_toolbar_get_nth_item(m_pToolbar, nIndex)); + } +#else + GtkWidget* toolbar_get_nth_item(int nIndex) const + { + int i = 0; + for (GtkWidget* pChild = gtk_widget_get_first_child(GTK_WIDGET(m_pToolbar)); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + if (i == nIndex) + return pChild; + ++i; + } + return nullptr; + } +#endif +public: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkInstanceToolbar(GtkToolbar* pToolbar, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) +#else + GtkInstanceToolbar(GtkBox* pToolbar, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) +#endif + : GtkInstanceWidget(GTK_WIDGET(pToolbar), pBuilder, bTakeOwnership) + , m_pToolbar(pToolbar) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_eImageType(vcl::ImageType::Size16) +#endif + , m_pMenuButtonProvider(nullptr) + { +#if GTK_CHECK_VERSION(4, 0, 0) + for (GtkWidget* pChild = gtk_widget_get_first_child(GTK_WIDGET(pToolbar)); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + collect(pChild, this); + } +#else + gtk_container_foreach(GTK_CONTAINER(pToolbar), collect, this); +#endif + } + + void disable_item_notify_events() + { + for (auto& a : m_aMap) + { + g_signal_handlers_block_by_func(a.second, reinterpret_cast(signalItemClicked), this); + } + } + + void enable_item_notify_events() + { + for (auto& a : m_aMap) + { + g_signal_handlers_unblock_by_func(a.second, reinterpret_cast(signalItemClicked), this); + } + } + + virtual void set_item_sensitive(const OUString& rIdent, bool bSensitive) override + { + disable_item_notify_events(); + gtk_widget_set_sensitive(GTK_WIDGET(m_aMap[rIdent]), bSensitive); + enable_item_notify_events(); + } + + virtual bool get_item_sensitive(const OUString& rIdent) const override + { + return gtk_widget_get_sensitive(GTK_WIDGET(m_aMap.find(rIdent)->second)); + } + + virtual void set_item_visible(const OUString& rIdent, bool bVisible) override + { + disable_item_notify_events(); + gtk_widget_set_visible(GTK_WIDGET(m_aMap[rIdent]), bVisible); + enable_item_notify_events(); + } + + virtual void set_item_help_id(const OUString& rIdent, const OUString& rHelpId) override + { + ::set_help_id(GTK_WIDGET(m_aMap[rIdent]), rHelpId); + } + + virtual bool get_item_visible(const OUString& rIdent) const override + { + return gtk_widget_get_visible(GTK_WIDGET(m_aMap.find(rIdent)->second)); + } + + virtual void set_item_active(const OUString& rIdent, bool bActive) override + { + disable_item_notify_events(); + + GtkWidget* pToolButton = m_aMap.find(rIdent)->second; + +#if !GTK_CHECK_VERSION(4, 0, 0) + if (GTK_IS_TOGGLE_TOOL_BUTTON(pToolButton)) + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(pToolButton), bActive); + else + { + GtkButton* pButton = nullptr; + // there is no GtkMenuToggleToolButton so abuse the CHECKED state of the GtkMenuToolButton button + // to emulate one + find_menupeer_button(GTK_WIDGET(pToolButton), &pButton); + if (pButton) + { + auto eState = gtk_widget_get_state_flags(GTK_WIDGET(pButton)) & ~GTK_STATE_FLAG_CHECKED; + if (bActive) + eState |= GTK_STATE_FLAG_CHECKED; + gtk_widget_set_state_flags(GTK_WIDGET(pButton), static_cast(eState), true); + } + } +#else + GtkWidget* pWidget; + if (GTK_IS_MENU_BUTTON(pToolButton)) + { + pWidget = gtk_widget_get_first_child(pToolButton); + assert(GTK_IS_TOGGLE_BUTTON(pWidget)); + } + else + pWidget = pToolButton; + auto eState = gtk_widget_get_state_flags(pWidget) & ~GTK_STATE_FLAG_CHECKED; + if (bActive) + eState |= GTK_STATE_FLAG_CHECKED; + gtk_widget_set_state_flags(pWidget, static_cast(eState), true); +#endif + + enable_item_notify_events(); + } + + virtual bool get_item_active(const OUString& rIdent) const override + { + GtkWidget* pToolButton = m_aMap.find(rIdent)->second; + +#if !GTK_CHECK_VERSION(4, 0, 0) + if (GTK_IS_TOGGLE_TOOL_BUTTON(pToolButton)) + return gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(pToolButton)); + else + { + GtkButton* pButton = nullptr; + // there is no GtkMenuToggleToolButton so abuse the CHECKED state of the GtkMenuToolButton button + // to emulate one + find_menupeer_button(GTK_WIDGET(pToolButton), &pButton); + if (pButton) + { + return gtk_widget_get_state_flags(GTK_WIDGET(pButton)) & GTK_STATE_FLAG_CHECKED; + } + } +#else + GtkWidget* pWidget; + if (GTK_IS_MENU_BUTTON(pToolButton)) + { + pWidget = gtk_widget_get_first_child(pToolButton); + assert(GTK_IS_TOGGLE_BUTTON(pWidget)); + } + else + pWidget = pToolButton; + return gtk_widget_get_state_flags(pWidget) & GTK_STATE_FLAG_CHECKED; +#endif + + return false; + } + + virtual void set_menu_item_active(const OUString& rIdent, bool bActive) override + { + disable_item_notify_events(); + + auto aFind = m_aMenuButtonMap.find(rIdent); + assert (aFind != m_aMenuButtonMap.end()); + aFind->second->set_active(bActive); + + enable_item_notify_events(); + } + + virtual bool get_menu_item_active(const OUString& rIdent) const override + { + auto aFind = m_aMenuButtonMap.find(rIdent); + assert (aFind != m_aMenuButtonMap.end()); + return aFind->second->get_active(); + } + + virtual void insert_item(int pos, const OUString& rId) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkToolItem* pItem = gtk_tool_button_new(nullptr, OUStringToOString(rId, RTL_TEXTENCODING_UTF8).getStr()); +#else + GtkWidget* pItem = gtk_button_new(); +#endif + ::set_buildable_id(GTK_BUILDABLE(pItem), rId); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_toolbar_insert(m_pToolbar, pItem, pos); +#else + gtk_box_insert_child_after(m_pToolbar, pItem, toolbar_get_nth_item(pos - 1)); +#endif + gtk_widget_show(GTK_WIDGET(pItem)); + add_to_map(GTK_WIDGET(pItem), nullptr); + } + + virtual void insert_separator(int pos, const OUString& rId) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkToolItem* pItem = gtk_separator_tool_item_new(); +#else + GtkWidget* pItem = gtk_separator_new(GTK_ORIENTATION_VERTICAL); +#endif + ::set_buildable_id(GTK_BUILDABLE(pItem), rId); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_toolbar_insert(m_pToolbar, pItem, pos); +#else + gtk_box_insert_child_after(m_pToolbar, pItem, toolbar_get_nth_item(pos - 1)); +#endif + gtk_widget_show(GTK_WIDGET(pItem)); + } + + virtual void set_item_popover(const OUString& rIdent, weld::Widget* pPopover) override + { + m_aMenuButtonMap[rIdent]->set_popover(pPopover); + } + + virtual void set_item_menu(const OUString& rIdent, weld::Menu* pMenu) override + { + m_aMenuButtonMap[rIdent]->set_menu(pMenu); + } + + virtual int get_n_items() const override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + return gtk_toolbar_get_n_items(m_pToolbar); +#else + int n_items = 0; + for (GtkWidget* pChild = gtk_widget_get_first_child(GTK_WIDGET(m_pToolbar)); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + ++n_items; + } + return n_items; +#endif + } + + virtual OUString get_item_ident(int nIndex) const override + { + auto* pItem = toolbar_get_nth_item(nIndex); + return ::get_buildable_id(GTK_BUILDABLE(pItem)); + } + + virtual void set_item_ident(int nIndex, const OUString& rIdent) override + { + OUString sOldIdent(get_item_ident(nIndex)); + m_aMap.erase(m_aMap.find(sOldIdent)); + + auto* pItem = toolbar_get_nth_item(nIndex); + ::set_buildable_id(GTK_BUILDABLE(pItem), rIdent); + + // to keep the ids unique, if the new id is already in use by an item, + // change the id of that item to the now unused old ident of this item + auto aFind = m_aMap.find(rIdent); + if (aFind != m_aMap.end()) + { + GtkWidget* pDupIdItem = aFind->second; + ::set_buildable_id(GTK_BUILDABLE(pDupIdItem), sOldIdent); + m_aMap[sOldIdent] = pDupIdItem; + } + + m_aMap[rIdent] = pItem; + } + + virtual void set_item_label(int nIndex, const OUString& rLabel) override + { + auto* pItem = toolbar_get_nth_item(nIndex); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!GTK_IS_TOOL_BUTTON(pItem)) + return; + gtk_tool_button_set_label(GTK_TOOL_BUTTON(pItem), MapToGtkAccelerator(rLabel).getStr()); +#else + if (!GTK_IS_BUTTON(pItem)) + return; + ::button_set_label(GTK_BUTTON(pItem), rLabel); +#endif + } + + virtual void set_item_label(const OUString& rIdent, const OUString& rLabel) override + { + GtkWidget* pItem = m_aMap[rIdent]; +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!pItem || !GTK_IS_TOOL_BUTTON(pItem)) + return; + gtk_tool_button_set_label(GTK_TOOL_BUTTON(pItem), MapToGtkAccelerator(rLabel).getStr()); +#else + if (!pItem || !GTK_IS_BUTTON(pItem)) + return; + ::button_set_label(GTK_BUTTON(pItem), rLabel); +#endif + } + + OUString get_item_label(const OUString& rIdent) const override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + const gchar* pText = gtk_tool_button_get_label(GTK_TOOL_BUTTON(m_aMap.find(rIdent)->second)); +#else + const gchar* pText = gtk_button_get_label(GTK_BUTTON(m_aMap.find(rIdent)->second)); +#endif + return OUString(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual void set_item_icon_name(const OUString& rIdent, const OUString& rIconName) override + { + GtkWidget* pItem = m_aMap[rIdent]; +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!pItem || !GTK_IS_TOOL_BUTTON(pItem)) + return; +#else + if (!pItem || !GTK_IS_BUTTON(pItem)) + return; +#endif + + GtkWidget* pImage = image_new_from_icon_name(rIconName); + if (pImage) + gtk_widget_show(pImage); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_tool_button_set_icon_widget(GTK_TOOL_BUTTON(pItem), pImage); +#else + gtk_button_set_child(GTK_BUTTON(pItem), pImage); + // versions of gtk4 > 4.2.1 might do this on their own + gtk_widget_remove_css_class(GTK_WIDGET(pItem), "text-button"); +#endif + } + + virtual void set_item_image_mirrored(const OUString& rIdent, bool bMirrored) override + { + m_aMirroredMap[rIdent] = bMirrored; + } + + virtual void set_item_image(const OUString& rIdent, const css::uno::Reference& rIcon) override + { + GtkWidget* pItem = m_aMap[rIdent]; + auto it = m_aMirroredMap.find(rIdent); + bool bMirrored = it != m_aMirroredMap.end() && it->second; +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!pItem || !GTK_IS_TOOL_BUTTON(pItem)) + return; + set_item_image(GTK_TOOL_BUTTON(pItem), rIcon, bMirrored); +#else + if (!pItem) + return; + set_item_image(pItem, rIcon, bMirrored); +#endif + } + + virtual void set_item_image(const OUString& rIdent, VirtualDevice* pDevice) override + { + GtkWidget* pItem = m_aMap[rIdent]; +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!pItem || !GTK_IS_TOOL_BUTTON(pItem)) + return; + set_item_image(GTK_TOOL_BUTTON(pItem), pDevice); +#else + if (!pItem) + return; + set_item_image(pItem, pDevice); +#endif + } + + virtual void set_item_image(int nIndex, const css::uno::Reference& rIcon) override + { + auto* pItem = toolbar_get_nth_item(nIndex); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!GTK_IS_TOOL_BUTTON(pItem)) + return; + set_item_image(GTK_TOOL_BUTTON(pItem), rIcon, false); +#else + set_item_image(pItem, rIcon, false); +#endif + } + + virtual void set_item_tooltip_text(int nIndex, const OUString& rTip) override + { + auto* pItem = toolbar_get_nth_item(nIndex); + gtk_widget_set_tooltip_text(GTK_WIDGET(pItem), OUStringToOString(rTip, RTL_TEXTENCODING_UTF8).getStr()); + } + + virtual void set_item_tooltip_text(const OUString& rIdent, const OUString& rTip) override + { + GtkWidget* pItem = GTK_WIDGET(m_aMap[rIdent]); + gtk_widget_set_tooltip_text(pItem, OUStringToOString(rTip, RTL_TEXTENCODING_UTF8).getStr()); + } + + virtual OUString get_item_tooltip_text(const OUString& rIdent) const override + { + GtkWidget* pItem = GTK_WIDGET(m_aMap.find(rIdent)->second); + const gchar* pStr = gtk_widget_get_tooltip_text(pItem); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual vcl::ImageType get_icon_size() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + return m_eImageType; +#else + return GtkToVcl(gtk_toolbar_get_icon_size(m_pToolbar)); +#endif + } + + virtual void set_icon_size(vcl::ImageType eType) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + m_eImageType = eType; +#else + gtk_toolbar_set_icon_size(m_pToolbar, VclToGtk(eType)); +#endif + } + + virtual sal_uInt16 get_modifier_state() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + GdkDisplay* pDisplay = gtk_widget_get_display(GTK_WIDGET(m_pToolbar)); + GdkSeat* pSeat = gdk_display_get_default_seat(pDisplay); + GdkDevice* pDevice = gdk_seat_get_keyboard(pSeat); + guint nState = gdk_device_get_modifier_state(pDevice); +#else + GdkKeymap* pKeymap = gdk_keymap_get_default(); + guint nState = gdk_keymap_get_modifier_state(pKeymap); +#endif + return GtkSalFrame::GetKeyModCode(nState); + } + + virtual int get_drop_index(const Point& rPoint) const override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + return gtk_toolbar_get_drop_index(m_pToolbar, rPoint.X(), rPoint.Y()); +#else + GtkWidget* pToolbar = GTK_WIDGET(m_pToolbar); + GtkWidget* pTarget = gtk_widget_pick(pToolbar, rPoint.X(), rPoint.Y(), GTK_PICK_DEFAULT); + if (!pTarget || pTarget == pToolbar) + return -1; + int i = 0; + for (GtkWidget* pChild = gtk_widget_get_first_child(GTK_WIDGET(m_pToolbar)); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + if (pChild == pTarget) + return i; + ++i; + } + return -1; +#endif + } + + virtual bool has_focus() const override + { + if (gtk_widget_has_focus(m_pWidget)) + return true; + + GtkWidget* pTopLevel = widget_get_toplevel(m_pWidget); + if (!GTK_IS_WINDOW(pTopLevel)) + return false; + GtkWidget* pFocus = gtk_window_get_focus(GTK_WINDOW(pTopLevel)); + if (!pFocus) + return false; + return gtk_widget_is_ancestor(pFocus, m_pWidget); + } + + virtual void grab_focus() override + { + if (has_focus()) + return; + gtk_widget_grab_focus(m_pWidget); +#if GTK_CHECK_VERSION(4, 0, 0) + bool bHasFocusChild = gtk_widget_get_focus_child(m_pWidget); +#else + bool bHasFocusChild = gtk_container_get_focus_child(GTK_CONTAINER(m_pWidget)); +#endif + if (!bHasFocusChild) + { + if (auto* pItem = toolbar_get_nth_item(0)) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_focus_child(m_pWidget, GTK_WIDGET(pItem)); +#else + gtk_container_set_focus_child(GTK_CONTAINER(m_pWidget), GTK_WIDGET(pItem)); +#endif + bHasFocusChild = true; + } + } + if (bHasFocusChild) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_child_focus(gtk_widget_get_focus_child(m_pWidget), GTK_DIR_TAB_FORWARD); +#else + gtk_widget_child_focus(gtk_container_get_focus_child(GTK_CONTAINER(m_pWidget)), GTK_DIR_TAB_FORWARD); +#endif + } + } + + virtual ~GtkInstanceToolbar() override + { + for (auto& a : m_aMap) + g_signal_handlers_disconnect_by_data(a.second, this); + } +}; + +} + +namespace { + +class GtkInstanceLinkButton : public GtkInstanceWidget, public virtual weld::LinkButton +{ +private: + GtkLinkButton* m_pButton; + gulong m_nSignalId; + + static bool signalActivateLink(GtkButton*, gpointer widget) + { + GtkInstanceLinkButton* pThis = static_cast(widget); + SolarMutexGuard aGuard; + return pThis->signal_activate_link(); + } + +public: + GtkInstanceLinkButton(GtkLinkButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pButton), pBuilder, bTakeOwnership) + , m_pButton(pButton) + , m_nSignalId(g_signal_connect(pButton, "activate-link", G_CALLBACK(signalActivateLink), this)) + { + } + + virtual void set_label(const OUString& rText) override + { + ::button_set_label(GTK_BUTTON(m_pButton), rText); + } + + virtual OUString get_label() const override + { + return ::button_get_label(GTK_BUTTON(m_pButton)); + } + + virtual void set_uri(const OUString& rText) override + { + gtk_link_button_set_uri(m_pButton, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); + } + + virtual void set_label_wrap(bool bWrap) override + { + GtkLabel* pChild = ::get_label_widget(GTK_WIDGET(m_pButton)); + ::set_label_wrap(pChild, bWrap); + gtk_label_set_max_width_chars(pChild, 1); + } + + virtual OUString get_uri() const override + { + const gchar* pStr = gtk_link_button_get_uri(m_pButton); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pButton, m_nSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pButton, m_nSignalId); + } + + virtual ~GtkInstanceLinkButton() override + { + g_signal_handler_disconnect(m_pButton, m_nSignalId); + } +}; + +} + +namespace { + +class GtkInstanceCheckButton : public GtkInstanceWidget, public virtual weld::CheckButton +{ +private: + GtkCheckButton* m_pCheckButton; + gulong m_nSignalId; + + static void signalToggled(void*, gpointer widget) + { + GtkInstanceCheckButton* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_toggled(); + } + +public: + GtkInstanceCheckButton(GtkCheckButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pButton), pBuilder, bTakeOwnership) + , m_pCheckButton(pButton) + , m_nSignalId(g_signal_connect(m_pCheckButton, "toggled", G_CALLBACK(signalToggled), this)) + { + } + + virtual void set_active(bool active) override + { + disable_notify_events(); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_check_button_set_inconsistent(m_pCheckButton, false); + gtk_check_button_set_active(m_pCheckButton, active); +#else + gtk_toggle_button_set_inconsistent(GTK_TOGGLE_BUTTON(m_pCheckButton), false); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pCheckButton), active); +#endif + enable_notify_events(); + } + + virtual bool get_active() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_check_button_get_active(m_pCheckButton); +#else + return gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(m_pCheckButton)); +#endif + } + + virtual void set_inconsistent(bool inconsistent) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_check_button_set_inconsistent(m_pCheckButton, inconsistent); +#else + gtk_toggle_button_set_inconsistent(GTK_TOGGLE_BUTTON(m_pCheckButton), inconsistent); +#endif + } + + virtual bool get_inconsistent() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_check_button_get_inconsistent(m_pCheckButton); +#else + return gtk_toggle_button_get_inconsistent(GTK_TOGGLE_BUTTON(m_pCheckButton)); +#endif + } + + virtual void set_label(const OUString& rText) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_check_button_set_label(m_pCheckButton, MapToGtkAccelerator(rText).getStr()); +#else + ::button_set_label(GTK_BUTTON(m_pCheckButton), rText); +#endif + } + + virtual OUString get_label() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + const gchar* pStr = gtk_check_button_get_label(m_pCheckButton); + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); +#else + return ::button_get_label(GTK_BUTTON(m_pCheckButton)); +#endif + } + + virtual void set_label_wrap(bool bWrap) override + { + GtkLabel* pChild = ::get_label_widget(GTK_WIDGET(m_pCheckButton)); + ::set_label_wrap(pChild, bWrap); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pCheckButton, m_nSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pCheckButton, m_nSignalId); + } + + virtual ~GtkInstanceCheckButton() override + { + g_signal_handler_disconnect(m_pCheckButton, m_nSignalId); + } +}; + +class GtkInstanceRadioButton : public GtkInstanceCheckButton, public virtual weld::RadioButton +{ +public: +#if GTK_CHECK_VERSION(4, 0, 0) + GtkInstanceRadioButton(GtkCheckButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceCheckButton(pButton, pBuilder, bTakeOwnership) +#else + GtkInstanceRadioButton(GtkRadioButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceCheckButton(GTK_CHECK_BUTTON(pButton), pBuilder, bTakeOwnership) +#endif + { + } +}; + +} + +namespace { + +class GtkInstanceScale : public GtkInstanceWidget, public virtual weld::Scale +{ +private: + GtkScale* m_pScale; + gulong m_nValueChangedSignalId; + + static void signalValueChanged(GtkScale*, gpointer widget) + { + GtkInstanceScale* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_value_changed(); + } + +public: + GtkInstanceScale(GtkScale* pScale, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pScale), pBuilder, bTakeOwnership) + , m_pScale(pScale) + , m_nValueChangedSignalId(g_signal_connect(m_pScale, "value-changed", G_CALLBACK(signalValueChanged), this)) + { + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pScale, m_nValueChangedSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pScale, m_nValueChangedSignalId); + } + + virtual void set_value(int value) override + { + disable_notify_events(); + gtk_range_set_value(GTK_RANGE(m_pScale), value); + enable_notify_events(); + } + + virtual void set_range(int min, int max) override + { + disable_notify_events(); + gtk_range_set_range(GTK_RANGE(m_pScale), min, max); + enable_notify_events(); + } + + virtual void set_increments(int step, int page) override + { + disable_notify_events(); + gtk_range_set_increments(GTK_RANGE(m_pScale), step, page); + enable_notify_events(); + } + + virtual void get_increments(int& step, int& page) const override + { + GtkAdjustment* pAdjustment = gtk_range_get_adjustment(GTK_RANGE(m_pScale)); + step = gtk_adjustment_get_step_increment(pAdjustment); + page = gtk_adjustment_get_page_increment(pAdjustment); + } + + virtual int get_value() const override + { + return gtk_range_get_value(GTK_RANGE(m_pScale)); + } + + virtual ~GtkInstanceScale() override + { + g_signal_handler_disconnect(m_pScale, m_nValueChangedSignalId); + } +}; + +class GtkInstanceProgressBar : public GtkInstanceWidget, public virtual weld::ProgressBar +{ +private: + GtkProgressBar* m_pProgressBar; + +public: + GtkInstanceProgressBar(GtkProgressBar* pProgressBar, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pProgressBar), pBuilder, bTakeOwnership) + , m_pProgressBar(pProgressBar) + { + } + + virtual void set_percentage(int value) override + { + gtk_progress_bar_set_fraction(m_pProgressBar, value / 100.0); + } + + virtual OUString get_text() const override + { + const gchar* pText = gtk_progress_bar_get_text(m_pProgressBar); + OUString sRet(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + return sRet; + } + + virtual void set_text(const OUString& rText) override + { + gtk_progress_bar_set_text(m_pProgressBar, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); + } +}; + +class GtkInstanceLevelBar : public GtkInstanceWidget, public virtual weld::LevelBar +{ +private: + GtkLevelBar* m_pLevelBar; + +public: + GtkInstanceLevelBar(GtkLevelBar* pLevelBar, GtkInstanceBuilder* pBuilder, + bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pLevelBar), pBuilder, bTakeOwnership) + , m_pLevelBar(pLevelBar) + { + } + + virtual void set_percentage(double fPercentage) override + { + gtk_level_bar_set_value(m_pLevelBar, fPercentage / 100.0); + } +}; + +class GtkInstanceSpinner : public GtkInstanceWidget, public virtual weld::Spinner +{ +private: + GtkSpinner* m_pSpinner; + +public: + GtkInstanceSpinner(GtkSpinner* pSpinner, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pSpinner), pBuilder, bTakeOwnership) + , m_pSpinner(pSpinner) + { + } + + virtual void start() override + { + gtk_spinner_start(m_pSpinner); + } + + virtual void stop() override + { + gtk_spinner_stop(m_pSpinner); + } +}; + +class GtkInstanceImage : public GtkInstanceWidget, public virtual weld::Image +{ +private: + GtkImage* m_pImage; + +public: + GtkInstanceImage(GtkImage* pImage, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pImage), pBuilder, bTakeOwnership) + , m_pImage(pImage) + { + } + + virtual void set_from_icon_name(const OUString& rIconName) override + { + image_set_from_icon_name(m_pImage, rIconName); + } + + virtual void set_image(VirtualDevice* pDevice) override + { + image_set_from_virtual_device(m_pImage, pDevice); + } + + virtual void set_image(const css::uno::Reference& rImage) override + { + image_set_from_xgraphic(m_pImage, rImage); + } +}; + +#if GTK_CHECK_VERSION(4, 0, 0) +class GtkInstancePicture: public GtkInstanceWidget, public virtual weld::Image +{ +private: + GtkPicture* m_pPicture; + +public: + GtkInstancePicture(GtkPicture* pPicture, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pPicture), pBuilder, bTakeOwnership) + , m_pPicture(pPicture) + { + gtk_picture_set_can_shrink(m_pPicture, true); + } + + virtual void set_from_icon_name(const OUString& rIconName) override + { + picture_set_from_icon_name(m_pPicture, rIconName); + } + + virtual void set_image(VirtualDevice* pDevice) override + { + picture_set_from_virtual_device(m_pPicture, pDevice); + } + + virtual void set_image(const css::uno::Reference& rPicture) override + { + picture_set_from_xgraphic(m_pPicture, rPicture); + } +}; +#endif + +class GtkInstanceCalendar : public GtkInstanceWidget, public virtual weld::Calendar +{ +private: + GtkCalendar* m_pCalendar; +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController* m_pKeyController; +#endif + gulong m_nDaySelectedSignalId; + gulong m_nDaySelectedDoubleClickSignalId; + gulong m_nKeyPressEventSignalId; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong m_nButtonPressEventSignalId; +#endif + + static void signalDaySelected(GtkCalendar*, gpointer widget) + { + GtkInstanceCalendar* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_selected(); + } + + static void signalDaySelectedDoubleClick(GtkCalendar*, gpointer widget) + { + GtkInstanceCalendar* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_activated(); + } + + bool signal_key_press(guint nKeyVal) + { + if (nKeyVal == GDK_KEY_Return || nKeyVal == GDK_KEY_KP_Enter) + { + SolarMutexGuard aGuard; + signal_activated(); + return true; + } + return false; + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static gboolean signalKeyPress(GtkEventControllerKey*, guint nKeyVal, guint /*nKeyCode*/, GdkModifierType, gpointer widget) + { + GtkInstanceCalendar* pThis = static_cast(widget); + return pThis->signal_key_press(nKeyVal); + } +#else + static gboolean signalKeyPress(GtkWidget*, GdkEventKey* pEvent, gpointer widget) + { + GtkInstanceCalendar* pThis = static_cast(widget); + return pThis->signal_key_press(pEvent->keyval); + } +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + static gboolean signalButton(GtkWidget*, GdkEventButton*, gpointer) + { + // don't let button press get to parent window, for the case of the + // ImplCFieldFloatWin floating window belonging to CalendarField where + // the click on the calendar continues to the parent GtkWindow and + // closePopup is called by GtkSalFrame::signalButton because the click + // window isn't that of the floating parent GtkWindow + return true; + } +#endif + +public: + GtkInstanceCalendar(GtkCalendar* pCalendar, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pCalendar), pBuilder, bTakeOwnership) + , m_pCalendar(pCalendar) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_pKeyController(gtk_event_controller_key_new()) +#endif + , m_nDaySelectedSignalId(g_signal_connect(pCalendar, "day-selected", G_CALLBACK(signalDaySelected), this)) + , m_nDaySelectedDoubleClickSignalId(g_signal_connect(pCalendar, "day-selected-double-click", G_CALLBACK(signalDaySelectedDoubleClick), this)) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_nKeyPressEventSignalId(g_signal_connect(m_pKeyController, "key-pressed", G_CALLBACK(signalKeyPress), this)) +#else + , m_nKeyPressEventSignalId(g_signal_connect(pCalendar, "key-press-event", G_CALLBACK(signalKeyPress), this)) + , m_nButtonPressEventSignalId(g_signal_connect_after(pCalendar, "button-press-event", G_CALLBACK(signalButton), this)) +#endif + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_add_controller(GTK_WIDGET(m_pCalendar), m_pKeyController); +#endif + } + + virtual void set_date(const Date& rDate) override + { + if (!rDate.IsValidAndGregorian()) + return; + + disable_notify_events(); +#if GTK_CHECK_VERSION(4, 0, 0) + GDateTime* pDateTime = g_date_time_new_local(rDate.GetYear(), rDate.GetMonth(), rDate.GetDay(), 0, 0, 0); + gtk_calendar_select_day(m_pCalendar, pDateTime); + g_date_time_unref(pDateTime); +#else + gtk_calendar_select_month(m_pCalendar, rDate.GetMonth() - 1, rDate.GetYear()); + gtk_calendar_select_day(m_pCalendar, rDate.GetDay()); +#endif + enable_notify_events(); + } + + virtual Date get_date() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + GDateTime* pDateTime = gtk_calendar_get_date(m_pCalendar); + Date aDate(g_date_time_get_day_of_month(pDateTime), + g_date_time_get_month(pDateTime), + g_date_time_get_year(pDateTime)); + g_date_time_unref(pDateTime); + return aDate; +#else + guint year, month, day; + gtk_calendar_get_date(m_pCalendar, &year, &month, &day); + return Date(day, month + 1, year); +#endif + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pCalendar, m_nDaySelectedDoubleClickSignalId); + g_signal_handler_block(m_pCalendar, m_nDaySelectedSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pCalendar, m_nDaySelectedSignalId); + g_signal_handler_unblock(m_pCalendar, m_nDaySelectedDoubleClickSignalId); + } + + virtual ~GtkInstanceCalendar() override + { +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pKeyController, m_nKeyPressEventSignalId); +#else + g_signal_handler_disconnect(m_pCalendar, m_nButtonPressEventSignalId); + g_signal_handler_disconnect(m_pCalendar, m_nKeyPressEventSignalId); +#endif + g_signal_handler_disconnect(m_pCalendar, m_nDaySelectedDoubleClickSignalId); + g_signal_handler_disconnect(m_pCalendar, m_nDaySelectedSignalId); + } +}; + +} + +namespace +{ + // CSS nodes: entry[.flat][.warning][.error] + void set_widget_css_message_type(GtkWidget* pWidget, weld::EntryMessageType eType) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_remove_css_class(pWidget, "error"); + gtk_widget_remove_css_class(pWidget, "warning"); +#else + GtkStyleContext *pWidgetContext = gtk_widget_get_style_context(pWidget); + gtk_style_context_remove_class(pWidgetContext, "error"); + gtk_style_context_remove_class(pWidgetContext, "warning"); +#endif + + switch (eType) + { + case weld::EntryMessageType::Normal: + break; + case weld::EntryMessageType::Warning: +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_add_css_class(pWidget, "warning"); +#else + gtk_style_context_add_class(pWidgetContext, "warning"); +#endif + break; + case weld::EntryMessageType::Error: +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_add_css_class(pWidget, "error"); +#else + gtk_style_context_add_class(pWidgetContext, "error"); +#endif + break; + } + } + + void set_entry_message_type(GtkEntry* pEntry, weld::EntryMessageType eType) + { + set_widget_css_message_type(GTK_WIDGET(pEntry), eType); + switch (eType) + { + case weld::EntryMessageType::Normal: + gtk_entry_set_icon_from_icon_name(pEntry, GTK_ENTRY_ICON_SECONDARY, nullptr); + break; + case weld::EntryMessageType::Warning: + gtk_entry_set_icon_from_icon_name(pEntry, GTK_ENTRY_ICON_SECONDARY, "dialog-warning"); + break; + case weld::EntryMessageType::Error: + gtk_entry_set_icon_from_icon_name(pEntry, GTK_ENTRY_ICON_SECONDARY, "dialog-error"); + break; + } + } +} + +namespace +{ + +class GtkInstanceEditable : public GtkInstanceWidget, public virtual weld::Entry +{ +protected: + GtkEditable* m_pEditable; + GtkWidget* m_pDelegate; + WidgetFont m_aCustomFont; +private: + gulong m_nChangedSignalId; + gulong m_nInsertTextSignalId; + gulong m_nCursorPosSignalId; + gulong m_nSelectionPosSignalId; + gulong m_nActivateSignalId; + + static void signalChanged(GtkEditable*, gpointer widget) + { + GtkInstanceEditable* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_changed(); + } + + static void signalInsertText(GtkEditable* pEditable, const gchar* pNewText, gint nNewTextLength, + gint* position, gpointer widget) + { + GtkInstanceEditable* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_insert_text(pEditable, pNewText, nNewTextLength, position); + } + + void signal_insert_text(GtkEditable* pEditable, const gchar* pNewText, gint nNewTextLength, gint* position) + { + if (!m_aInsertTextHdl.IsSet()) + return; + OUString sText(pNewText, nNewTextLength, RTL_TEXTENCODING_UTF8); + const bool bContinue = m_aInsertTextHdl.Call(sText); + if (bContinue && !sText.isEmpty()) + { + OString sFinalText(OUStringToOString(sText, RTL_TEXTENCODING_UTF8)); + g_signal_handlers_block_by_func(pEditable, reinterpret_cast(signalInsertText), this); + gtk_editable_insert_text(pEditable, sFinalText.getStr(), sFinalText.getLength(), position); + g_signal_handlers_unblock_by_func(pEditable, reinterpret_cast(signalInsertText), this); + } + g_signal_stop_emission_by_name(pEditable, "insert-text"); + } + + static void signalCursorPosition(void*, GParamSpec*, gpointer widget) + { + GtkInstanceEditable* pThis = static_cast(widget); + pThis->signal_cursor_position(); + } + + static void signalActivate(void*, gpointer widget) + { + GtkInstanceEditable* pThis = static_cast(widget); + pThis->signal_activate(); + } + + virtual void ensureMouseEventWidget() override + { + // The GtkEntry is sufficient to get mouse events without an intermediate GtkEventBox + if (!m_pMouseEventBox) + m_pMouseEventBox = m_pDelegate; + } + +protected: + + virtual void signal_activate() + { + if (m_aActivateHdl.IsSet()) + { + SolarMutexGuard aGuard; + if (m_aActivateHdl.Call(*this)) + g_signal_stop_emission_by_name(m_pDelegate, "activate"); + } + } + + PangoAttrList* get_attributes() + { +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_text_get_attributes(GTK_TEXT(m_pDelegate)); +#else + return gtk_entry_get_attributes(GTK_ENTRY(m_pDelegate)); +#endif + } + + void set_attributes(PangoAttrList* pAttrs) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_text_set_attributes(GTK_TEXT(m_pDelegate), pAttrs); +#else + gtk_entry_set_attributes(GTK_ENTRY(m_pDelegate), pAttrs); +#endif + } + +public: + GtkInstanceEditable(GtkWidget* pWidget, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(pWidget, pBuilder, bTakeOwnership) + , m_pEditable(GTK_EDITABLE(pWidget)) +#if GTK_CHECK_VERSION(4, 0, 0) + , m_pDelegate(GTK_WIDGET(gtk_editable_get_delegate(m_pEditable))) +#else + , m_pDelegate(pWidget) +#endif + , m_aCustomFont(m_pWidget) + , m_nChangedSignalId(g_signal_connect(m_pEditable, "changed", G_CALLBACK(signalChanged), this)) + , m_nInsertTextSignalId(g_signal_connect(m_pEditable, "insert-text", G_CALLBACK(signalInsertText), this)) + , m_nCursorPosSignalId(g_signal_connect(m_pEditable, "notify::cursor-position", G_CALLBACK(signalCursorPosition), this)) + , m_nSelectionPosSignalId(g_signal_connect(m_pEditable, "notify::selection-bound", G_CALLBACK(signalCursorPosition), this)) + , m_nActivateSignalId(g_signal_connect(m_pDelegate, "activate", G_CALLBACK(signalActivate), this)) + { + } + + virtual void set_text(const OUString& rText) override + { + disable_notify_events(); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_editable_set_text(m_pEditable, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); +#else + gtk_entry_set_text(GTK_ENTRY(m_pDelegate), OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); +#endif + enable_notify_events(); + } + + virtual OUString get_text() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + const gchar* pText = gtk_editable_get_text(m_pEditable); +#else + const gchar* pText = gtk_entry_get_text(GTK_ENTRY(m_pDelegate)); +#endif + OUString sRet(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + return sRet; + } + + virtual void set_width_chars(int nChars) override + { + disable_notify_events(); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_editable_set_width_chars(m_pEditable, nChars); + gtk_editable_set_max_width_chars(m_pEditable, nChars); +#else + gtk_entry_set_width_chars(GTK_ENTRY(m_pDelegate), nChars); + gtk_entry_set_max_width_chars(GTK_ENTRY(m_pDelegate), nChars); +#endif + enable_notify_events(); + } + + virtual int get_width_chars() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_editable_get_width_chars(m_pEditable); +#else + return gtk_entry_get_width_chars(GTK_ENTRY(m_pDelegate)); +#endif + } + + virtual void set_max_length(int nChars) override + { + disable_notify_events(); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_text_set_max_length(GTK_TEXT(m_pDelegate), nChars); +#else + gtk_entry_set_max_length(GTK_ENTRY(m_pDelegate), nChars); +#endif + enable_notify_events(); + } + + virtual void select_region(int nStartPos, int nEndPos) override + { + disable_notify_events(); + gtk_editable_select_region(m_pEditable, nStartPos, nEndPos); + enable_notify_events(); + } + + bool get_selection_bounds(int& rStartPos, int& rEndPos) override + { + return gtk_editable_get_selection_bounds(m_pEditable, &rStartPos, &rEndPos); + } + + virtual void replace_selection(const OUString& rText) override + { + disable_notify_events(); + gtk_editable_delete_selection(m_pEditable); + OString sText(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)); + gint position = gtk_editable_get_position(m_pEditable); + gtk_editable_insert_text(m_pEditable, sText.getStr(), sText.getLength(), + &position); + enable_notify_events(); + } + + virtual void set_position(int nCursorPos) override + { + disable_notify_events(); + gtk_editable_set_position(m_pEditable, nCursorPos); + enable_notify_events(); + } + + virtual int get_position() const override + { + return gtk_editable_get_position(m_pEditable); + } + + virtual void set_editable(bool bEditable) override + { + gtk_editable_set_editable(m_pEditable, bEditable); + } + + virtual bool get_editable() const override + { + return gtk_editable_get_editable(m_pEditable); + } + + virtual void set_overwrite_mode(bool bOn) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_text_set_overwrite_mode(GTK_TEXT(m_pDelegate), bOn); +#else + gtk_entry_set_overwrite_mode(GTK_ENTRY(m_pDelegate), bOn); +#endif + } + + virtual bool get_overwrite_mode() const override + { +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_text_get_overwrite_mode(GTK_TEXT(m_pDelegate)); +#else + return gtk_entry_get_overwrite_mode(GTK_ENTRY(m_pDelegate)); +#endif + } + + virtual void set_message_type(weld::EntryMessageType eType) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + if (!GTK_IS_ENTRY(m_pDelegate)) + { + ::set_widget_css_message_type(m_pDelegate, eType); + return; + } +#endif + ::set_entry_message_type(GTK_ENTRY(m_pDelegate), eType); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pDelegate, m_nActivateSignalId); + g_signal_handler_block(m_pEditable, m_nSelectionPosSignalId); + g_signal_handler_block(m_pEditable, m_nCursorPosSignalId); + g_signal_handler_block(m_pEditable, m_nInsertTextSignalId); + g_signal_handler_block(m_pEditable, m_nChangedSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pEditable, m_nChangedSignalId); + g_signal_handler_unblock(m_pEditable, m_nInsertTextSignalId); + g_signal_handler_unblock(m_pEditable, m_nCursorPosSignalId); + g_signal_handler_unblock(m_pEditable, m_nSelectionPosSignalId); + g_signal_handler_unblock(m_pDelegate, m_nActivateSignalId); + } + + virtual vcl::Font get_font() override + { + if (const vcl::Font* pFont = m_aCustomFont.get_custom_font()) + return *pFont; + return GtkInstanceWidget::get_font(); + } + + void set_font_color(const Color& rColor) override + { + PangoAttrList* pOrigList = get_attributes(); + if (rColor == COL_AUTO && !pOrigList) // nothing to do + return; + + PangoAttrType aFilterAttrs[] = {PANGO_ATTR_FOREGROUND, PANGO_ATTR_INVALID}; + + PangoAttrList* pAttrs = pOrigList ? pango_attr_list_copy(pOrigList) : pango_attr_list_new(); + PangoAttrList* pRemovedAttrs = pOrigList ? pango_attr_list_filter(pAttrs, filter_pango_attrs, &aFilterAttrs) : nullptr; + + if (rColor != COL_AUTO) + pango_attr_list_insert(pAttrs, pango_attr_foreground_new(rColor.GetRed()/255.0, rColor.GetGreen()/255.0, rColor.GetBlue()/255.0)); + + set_attributes(pAttrs); + pango_attr_list_unref(pAttrs); + pango_attr_list_unref(pRemovedAttrs); + } + + void fire_signal_changed() + { + signal_changed(); + } + + virtual void cut_clipboard() override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_activate_action(m_pDelegate, "cut.clipboard", nullptr); +#else + gtk_editable_cut_clipboard(m_pEditable); +#endif + } + + virtual void copy_clipboard() override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_activate_action(m_pDelegate, "copy.clipboard", nullptr); +#else + gtk_editable_copy_clipboard(m_pEditable); +#endif + } + + virtual void paste_clipboard() override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_activate_action(m_pDelegate, "paste.clipboard", nullptr); +#else + gtk_editable_paste_clipboard(m_pEditable); +#endif + } + + virtual void set_placeholder_text(const OUString& rText) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_text_set_placeholder_text(GTK_TEXT(m_pDelegate), rText.toUtf8().getStr()); +#else + gtk_entry_set_placeholder_text(GTK_ENTRY(m_pDelegate), rText.toUtf8().getStr()); +#endif + } + + virtual void grab_focus() override + { + if (has_focus()) + return; +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_text_grab_focus_without_selecting(GTK_TEXT(m_pDelegate)); +#else + gtk_entry_grab_focus_without_selecting(GTK_ENTRY(m_pDelegate)); +#endif + } + + virtual void set_alignment(TxtAlign eXAlign) override + { + gfloat xalign = 0; + switch (eXAlign) + { + case TxtAlign::Left: + xalign = 0.0; + break; + case TxtAlign::Center: + xalign = 0.5; + break; + case TxtAlign::Right: + xalign = 1.0; + break; + } +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_editable_set_alignment(m_pEditable, xalign); +#else + gtk_entry_set_alignment(GTK_ENTRY(m_pDelegate), xalign); +#endif + } + + virtual ~GtkInstanceEditable() override + { + g_signal_handler_disconnect(m_pDelegate, m_nActivateSignalId); + g_signal_handler_disconnect(m_pEditable, m_nSelectionPosSignalId); + g_signal_handler_disconnect(m_pEditable, m_nCursorPosSignalId); + g_signal_handler_disconnect(m_pEditable, m_nInsertTextSignalId); + g_signal_handler_disconnect(m_pEditable, m_nChangedSignalId); + } +}; + +class GtkInstanceEntry : public GtkInstanceEditable +{ +private: +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkEntry* m_pEntry; + GtkOverlay* m_pPlaceHolderReplacement; + GtkLabel* m_pPlaceHolderLabel; + gulong m_nEntryFocusInSignalId; + gulong m_nEntryFocusOutSignalId; + gulong m_nEntryTextLengthSignalId; + gulong m_nEntryScrollOffsetSignalId; + guint m_nUpdatePlaceholderReplacementIdle; + + static gboolean do_update_placeholder_replacement(gpointer widget) + { + GtkInstanceEntry* pThis = static_cast(widget); + pThis->update_placeholder_replacement(); + return false; + } + + void update_placeholder_replacement() + { + m_nUpdatePlaceholderReplacementIdle = 0; + + const char* placeholder_text = gtk_entry_get_placeholder_text(m_pEntry); + const bool bShow = placeholder_text && !gtk_entry_get_text_length(m_pEntry) && + gtk_widget_has_focus(GTK_WIDGET(m_pEntry)); + if (bShow) + { + GdkRectangle text_area; + gtk_entry_get_text_area(m_pEntry, &text_area); + gint x; + gtk_entry_get_layout_offsets(m_pEntry, &x, nullptr); + gtk_widget_set_margin_start(GTK_WIDGET(m_pPlaceHolderLabel), x); + gtk_widget_set_margin_end(GTK_WIDGET(m_pPlaceHolderLabel), x); + gtk_label_set_text(m_pPlaceHolderLabel, placeholder_text); + gtk_widget_show(GTK_WIDGET(m_pPlaceHolderLabel)); + } + else + gtk_widget_hide(GTK_WIDGET(m_pPlaceHolderLabel)); + } + + void launch_update_placeholder_replacement() + { + // do it in the next event cycle so the GtkEntry has done its layout + // and gtk_entry_get_layout_offsets returns the right results + if (m_nUpdatePlaceholderReplacementIdle) + return; + // G_PRIORITY_LOW so gtk's idles are run before this + m_nUpdatePlaceholderReplacementIdle = g_idle_add_full(G_PRIORITY_LOW, do_update_placeholder_replacement, this, nullptr); + } + + static gboolean signalEntryFocusIn(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceEntry* pThis = static_cast(widget); + pThis->launch_update_placeholder_replacement(); + return false; + } + + static gboolean signalEntryFocusOut(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceEntry* pThis = static_cast(widget); + pThis->launch_update_placeholder_replacement(); + return false; + } + + static void signalEntryTextLength(void*, GParamSpec*, gpointer widget) + { + GtkInstanceEntry* pThis = static_cast(widget); + pThis->launch_update_placeholder_replacement(); + } + + static void signalEntryScrollOffset(void*, GParamSpec*, gpointer widget) + { + // this property affects the x-position of the text area + GtkInstanceEntry* pThis = static_cast(widget); + pThis->launch_update_placeholder_replacement(); + } + +#endif + +public: + GtkInstanceEntry(GtkEntry* pEntry, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceEditable(GTK_WIDGET(pEntry), pBuilder, bTakeOwnership) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_pEntry(pEntry) + , m_pPlaceHolderReplacement(nullptr) + , m_pPlaceHolderLabel(nullptr) + , m_nEntryFocusInSignalId(0) + , m_nEntryFocusOutSignalId(0) + , m_nEntryTextLengthSignalId(0) + , m_nEntryScrollOffsetSignalId(0) + , m_nUpdatePlaceholderReplacementIdle(0) +#endif + { +#if !GTK_CHECK_VERSION(4, 0, 0) + // tdf#150810 fake getting placeholders visible even when GtkEntry has focus in gtk3. + // In gtk4 this works out of the box, for gtk3 fake it by having a GtkLabel in an + // overlay and show that label if the placeholder would be shown if there was + // no focus + const char* pPlaceHolderText = gtk_entry_get_placeholder_text(m_pEntry); + if (pPlaceHolderText ? strlen(pPlaceHolderText) : 0) + { + m_pPlaceHolderReplacement = GTK_OVERLAY(gtk_overlay_new()); + m_pPlaceHolderLabel = GTK_LABEL(gtk_label_new(nullptr)); + + GtkStyleContext *pStyleContext = gtk_widget_get_style_context(GTK_WIDGET(m_pEntry)); + GdkRGBA fg = { 0.5, 0.5, 0.5, 0.0 }; + gtk_style_context_lookup_color(pStyleContext, "placeholder_text_color", &fg); + + auto red = std::clamp(fg.red * 65535 + 0.5, 0.0, 65535.0); + auto green = std::clamp(fg.green * 65535 + 0.5, 0.0, 65535.0); + auto blue = std::clamp(fg.blue * 65535 + 0.5, 0.0, 65535.0); + + PangoAttribute *pAttr = pango_attr_foreground_new(red, green, blue); + pAttr->start_index = 0; + pAttr->end_index = G_MAXINT; + PangoAttrList* pAttrList = pango_attr_list_new(); + pango_attr_list_insert(pAttrList, pAttr); + gtk_label_set_attributes(m_pPlaceHolderLabel, pAttrList); + pango_attr_list_unref(pAttrList); + + // The GtkEntry will have the placeholder as the text to analyze here, assumes there is no initial text, just placeholder + const bool bRTL = PANGO_DIRECTION_RTL == pango_context_get_base_dir(pango_layout_get_context(gtk_entry_get_layout(m_pEntry))); + SAL_WARN_IF(gtk_entry_get_text_length(m_pEntry), "vcl.gtk", "don't have a placeholder set, but also initial text"); + gtk_label_set_xalign(m_pPlaceHolderLabel, bRTL ? 1.0 : 0.0); + + gtk_overlay_add_overlay(m_pPlaceHolderReplacement, GTK_WIDGET(m_pPlaceHolderLabel)); + insertAsParent(GTK_WIDGET(m_pEntry), GTK_WIDGET(m_pPlaceHolderReplacement)); + m_nEntryFocusInSignalId = g_signal_connect_after(m_pEntry, "focus-in-event", G_CALLBACK(signalEntryFocusIn), this); + m_nEntryFocusOutSignalId = g_signal_connect_after(m_pEntry, "focus-out-event", G_CALLBACK(signalEntryFocusOut), this); + m_nEntryTextLengthSignalId = g_signal_connect(m_pEntry, "notify::text-length", G_CALLBACK(signalEntryTextLength), this); + m_nEntryScrollOffsetSignalId = g_signal_connect(m_pEntry, "notify::scroll-offset", G_CALLBACK(signalEntryScrollOffset), this); + } +#endif + } + + virtual void set_font(const vcl::Font& rFont) override + { + m_aCustomFont.use_custom_font(&rFont, u"entry"); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + + virtual void show() override + { + GtkInstanceEditable::show(); + if (m_pPlaceHolderReplacement) + gtk_widget_show(GTK_WIDGET(m_pPlaceHolderReplacement)); + } + + virtual void hide() override + { + if (m_pPlaceHolderReplacement) + gtk_widget_hide(GTK_WIDGET(m_pPlaceHolderReplacement)); + GtkInstanceEditable::hide(); + } + + virtual ~GtkInstanceEntry() override + { + if (m_nUpdatePlaceholderReplacementIdle) + g_source_remove(m_nUpdatePlaceholderReplacementIdle); + if (m_nEntryFocusInSignalId) + g_signal_handler_disconnect(m_pEntry, m_nEntryFocusInSignalId); + if (m_nEntryFocusOutSignalId) + g_signal_handler_disconnect(m_pEntry, m_nEntryFocusOutSignalId); + if (m_nEntryTextLengthSignalId) + g_signal_handler_disconnect(m_pEntry, m_nEntryTextLengthSignalId); + if (m_nEntryScrollOffsetSignalId) + g_signal_handler_disconnect(m_pEntry, m_nEntryScrollOffsetSignalId); + } +#endif +}; + +} + +namespace +{ + + struct Search + { + OString str; + int index; + int col; + Search(std::u16string_view rText, int nCol) + : str(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)) + , index(-1) + , col(nCol) + { + } + }; + + gboolean foreach_find(GtkTreeModel* model, GtkTreePath* path, GtkTreeIter* iter, gpointer data) + { + Search* search = static_cast(data); + gchar *pStr = nullptr; + gtk_tree_model_get(model, iter, search->col, &pStr, -1); + bool found = strcmp(pStr, search->str.getStr()) == 0; + if (found) + { + gint depth; + gint* indices = gtk_tree_path_get_indices_with_depth(path, &depth); + search->index = indices[depth-1]; + } + g_free(pStr); + return found; + } + + void insert_row(GtkListStore* pListStore, GtkTreeIter& iter, int pos, const OUString* pId, std::u16string_view rText, const OUString* pIconName, const VirtualDevice* pDevice) + { + if (!pIconName && !pDevice) + { + gtk_list_store_insert_with_values(pListStore, &iter, pos, + 0, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr(), + 1, !pId ? nullptr : OUStringToOString(*pId, RTL_TEXTENCODING_UTF8).getStr(), + -1); + } + else + { + if (pIconName) + { + GdkPixbuf* pixbuf = getPixbuf(*pIconName); + + gtk_list_store_insert_with_values(pListStore, &iter, pos, + 0, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr(), + 1, !pId ? nullptr : OUStringToOString(*pId, RTL_TEXTENCODING_UTF8).getStr(), + 2, pixbuf, + -1); + + if (pixbuf) + g_object_unref(pixbuf); + } + else + { + cairo_surface_t* surface = get_underlying_cairo_surface(*pDevice); + + Size aSize(pDevice->GetOutputSizePixel()); + cairo_surface_t* target = cairo_surface_create_similar(surface, + cairo_surface_get_content(surface), + aSize.Width(), + aSize.Height()); + + cairo_t* cr = cairo_create(target); + cairo_set_source_surface(cr, surface, 0, 0); + cairo_paint(cr); + cairo_destroy(cr); + + gtk_list_store_insert_with_values(pListStore, &iter, pos, + 0, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr(), + 1, !pId ? nullptr : OUStringToOString(*pId, RTL_TEXTENCODING_UTF8).getStr(), + 3, target, + -1); + cairo_surface_destroy(target); + } + } + } +} + +namespace +{ + gint default_sort_func(GtkTreeModel* pModel, GtkTreeIter* a, GtkTreeIter* b, gpointer data) + { + comphelper::string::NaturalStringSorter* pSorter = static_cast(data); + gchar* pName1; + gchar* pName2; + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(pModel); + gint sort_column_id(0); + gtk_tree_sortable_get_sort_column_id(pSortable, &sort_column_id, nullptr); + gtk_tree_model_get(pModel, a, sort_column_id, &pName1, -1); + gtk_tree_model_get(pModel, b, sort_column_id, &pName2, -1); + gint ret = pSorter->compare(OUString(pName1, pName1 ? strlen(pName1) : 0, RTL_TEXTENCODING_UTF8), + OUString(pName2, pName2 ? strlen(pName2) : 0, RTL_TEXTENCODING_UTF8)); + g_free(pName1); + g_free(pName2); + return ret; + } + + int starts_with(GtkTreeModel* pTreeModel, const OUString& rStr, int col, int nStartRow, bool bCaseSensitive) + { + GtkTreeIter iter; + if (!gtk_tree_model_iter_nth_child(pTreeModel, &iter, nullptr, nStartRow)) + return -1; + + const vcl::I18nHelper& rI18nHelper = Application::GetSettings().GetUILocaleI18nHelper(); + int nRet = nStartRow; + do + { + gchar* pStr; + gtk_tree_model_get(pTreeModel, &iter, col, &pStr, -1); + OUString aStr(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + g_free(pStr); + const bool bMatch = !bCaseSensitive ? rI18nHelper.MatchString(rStr, aStr) : aStr.startsWith(rStr); + if (bMatch) + return nRet; + ++nRet; + } while (gtk_tree_model_iter_next(pTreeModel, &iter)); + + return -1; + } + + struct GtkInstanceTreeIter : public weld::TreeIter + { + GtkInstanceTreeIter(const GtkInstanceTreeIter* pOrig) + { + if (pOrig) + iter = pOrig->iter; + else + memset(&iter, 0, sizeof(iter)); + } + GtkInstanceTreeIter(const GtkTreeIter& rOrig) + { + memcpy(&iter, &rOrig, sizeof(iter)); + } + virtual bool equal(const TreeIter& rOther) const override + { + return memcmp(&iter, &static_cast(rOther).iter, sizeof(GtkTreeIter)) == 0; + } + GtkTreeIter iter; + }; + + class GtkInstanceTreeView; + +} + +static GtkInstanceTreeView* g_DragSource; + +namespace { + +struct CompareGtkTreePath +{ + bool operator()(const GtkTreePath* lhs, const GtkTreePath* rhs) const + { + return gtk_tree_path_compare(lhs, rhs) < 0; + } +}; + +int get_height_row(GtkTreeView* pTreeView, GList* pColumns) +{ + gint nMaxRowHeight = 0; + for (GList* pEntry = g_list_first(pColumns); pEntry; pEntry = g_list_next(pEntry)) + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + GList *pRenderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(pColumn)); + for (GList* pRenderer = g_list_first(pRenderers); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + gint nRowHeight; + gtk_cell_renderer_get_preferred_height(pCellRenderer, GTK_WIDGET(pTreeView), nullptr, &nRowHeight); + nMaxRowHeight = std::max(nMaxRowHeight, nRowHeight); + } + g_list_free(pRenderers); + } + return nMaxRowHeight; +} + +int get_height_row_separator(GtkTreeView* pTreeView) +{ + // gtk4: _TREE_VIEW_VERTICAL_SEPARATOR define in gtk/gtktreeview.c + gint nVerticalSeparator = 2; +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_style_get(GTK_WIDGET(pTreeView), "vertical-separator", &nVerticalSeparator, nullptr); +#else + (void)pTreeView; +#endif + return nVerticalSeparator; +} + +int get_height_rows(GtkTreeView* pTreeView, GList* pColumns, int nRows) +{ + gint nMaxRowHeight = get_height_row(pTreeView, pColumns); + gint nVerticalSeparator = get_height_row_separator(pTreeView); + return (nMaxRowHeight * nRows) + (nVerticalSeparator * nRows) / 2; +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +int get_height_rows(int nRowHeight, int nSeparatorHeight, int nRows) +{ + return (nRowHeight * nRows) + (nSeparatorHeight * (nRows + 1)); +} +#endif + +tools::Rectangle get_row_area(GtkTreeView* pTreeView, GList* pColumns, GtkTreePath* pPath) +{ + tools::Rectangle aRet; + + GdkRectangle aRect; + for (GList* pEntry = g_list_last(pColumns); pEntry; pEntry = g_list_previous(pEntry)) + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + gtk_tree_view_get_cell_area(pTreeView, pPath, pColumn, &aRect); + aRet.Union(tools::Rectangle(aRect.x, aRect.y, aRect.x + aRect.width, aRect.y + aRect.height)); + } + + return aRet; +} + +struct GtkTreeRowReferenceDeleter +{ + void operator()(GtkTreeRowReference* p) const + { + gtk_tree_row_reference_free(p); + } +}; + +bool separator_function(const GtkTreePath* path, const std::vector>& rSeparatorRows) +{ + bool bFound = false; + for (auto& a : rSeparatorRows) + { + GtkTreePath* seppath = gtk_tree_row_reference_get_path(a.get()); + if (seppath) + { + bFound = gtk_tree_path_compare(path, seppath) == 0; + gtk_tree_path_free(seppath); + } + if (bFound) + break; + } + return bFound; +} + +void tree_store_set(GtkTreeModel* pTreeModel, GtkTreeIter *pIter, ...) +{ + va_list args; + + va_start(args, pIter); + gtk_tree_store_set_valist(GTK_TREE_STORE(pTreeModel), pIter, args); + va_end(args); +} + +void list_store_set(GtkTreeModel* pTreeModel, GtkTreeIter *pIter, ...) +{ + va_list args; + + va_start(args, pIter); + gtk_list_store_set_valist(GTK_LIST_STORE(pTreeModel), pIter, args); + va_end(args); +} + +void tree_store_insert_with_values(GtkTreeModel* pTreeModel, GtkTreeIter *pIter, GtkTreeIter *pParent, gint nPos, + gint nTextCol, const gchar* pText, + gint nIdCol, const gchar* pId) +{ + gtk_tree_store_insert_with_values(GTK_TREE_STORE(pTreeModel), pIter, pParent, nPos, + nTextCol, pText, nIdCol, pId, -1); +} + +void list_store_insert_with_values(GtkTreeModel* pTreeModel, GtkTreeIter *pIter, GtkTreeIter *pParent, gint nPos, + gint nTextCol, const gchar* pText, + gint nIdCol, const gchar* pId) +{ + assert(!pParent); (void)pParent; + gtk_list_store_insert_with_values(GTK_LIST_STORE(pTreeModel), pIter, nPos, + nTextCol, pText, nIdCol, pId, -1); +} + +void tree_store_prepend(GtkTreeModel* pTreeModel, GtkTreeIter *pIter, GtkTreeIter *pParent) +{ + gtk_tree_store_prepend(GTK_TREE_STORE(pTreeModel), pIter, pParent); +} + +void list_store_prepend(GtkTreeModel* pTreeModel, GtkTreeIter *pIter, GtkTreeIter *pParent) +{ + assert(!pParent); (void)pParent; + gtk_list_store_prepend(GTK_LIST_STORE(pTreeModel), pIter); +} + +void tree_store_insert(GtkTreeModel* pTreeModel, GtkTreeIter *pIter, GtkTreeIter *pParent, gint nPosition) +{ + gtk_tree_store_insert(GTK_TREE_STORE(pTreeModel), pIter, pParent, nPosition); +} + +void list_store_insert(GtkTreeModel* pTreeModel, GtkTreeIter *pIter, GtkTreeIter *pParent, gint nPosition) +{ + assert(!pParent); (void)pParent; + gtk_list_store_insert(GTK_LIST_STORE(pTreeModel), pIter, nPosition); +} + +void tree_store_clear(GtkTreeModel* pTreeModel) +{ + gtk_tree_store_clear(GTK_TREE_STORE(pTreeModel)); +} + +void list_store_clear(GtkTreeModel* pTreeModel) +{ + gtk_list_store_clear(GTK_LIST_STORE(pTreeModel)); +} + +bool tree_store_remove(GtkTreeModel* pTreeModel, GtkTreeIter *pIter) +{ + return gtk_tree_store_remove(GTK_TREE_STORE(pTreeModel), pIter); +} + +bool list_store_remove(GtkTreeModel* pTreeModel, GtkTreeIter *pIter) +{ + return gtk_list_store_remove(GTK_LIST_STORE(pTreeModel), pIter); +} + +void tree_store_swap(GtkTreeModel* pTreeModel, GtkTreeIter* pIter1, GtkTreeIter* pIter2) +{ + gtk_tree_store_swap(GTK_TREE_STORE(pTreeModel), pIter1, pIter2); +} + +void list_store_swap(GtkTreeModel* pTreeModel, GtkTreeIter* pIter1, GtkTreeIter* pIter2) +{ + gtk_list_store_swap(GTK_LIST_STORE(pTreeModel), pIter1, pIter2); +} + +void tree_store_set_value(GtkTreeModel* pTreeModel, GtkTreeIter* pIter, gint nColumn, GValue* pValue) +{ + gtk_tree_store_set_value(GTK_TREE_STORE(pTreeModel), pIter, nColumn, pValue); +} + +void list_store_set_value(GtkTreeModel* pTreeModel, GtkTreeIter* pIter, gint nColumn, GValue* pValue) +{ + gtk_list_store_set_value(GTK_LIST_STORE(pTreeModel), pIter, nColumn, pValue); +} + +int promote_arg(bool bArg) +{ + return static_cast(bArg); +} + +class GtkInstanceTreeView : public GtkInstanceWidget, public virtual weld::TreeView +{ +private: + GtkTreeView* m_pTreeView; + GtkTreeModel* m_pTreeModel; + + typedef void(*setterFnc)(GtkTreeModel*, GtkTreeIter*, ...); + setterFnc m_Setter; + + typedef void(*insertWithValuesFnc)(GtkTreeModel*, GtkTreeIter*, GtkTreeIter*, gint, gint, const gchar*, gint, const gchar*); + insertWithValuesFnc m_InsertWithValues; + + typedef void(*insertFnc)(GtkTreeModel*, GtkTreeIter*, GtkTreeIter*, gint); + insertFnc m_Insert; + + typedef void(*prependFnc)(GtkTreeModel*, GtkTreeIter*, GtkTreeIter*); + prependFnc m_Prepend; + + typedef void(*clearFnc)(GtkTreeModel*); + clearFnc m_Clear; + + typedef bool(*removeFnc)(GtkTreeModel*, GtkTreeIter*); + removeFnc m_Remove; + + typedef void(*swapFnc)(GtkTreeModel*, GtkTreeIter*, GtkTreeIter*); + swapFnc m_Swap; + + typedef void(*setValueFnc)(GtkTreeModel*, GtkTreeIter*, gint, GValue*); + setValueFnc m_SetValue; + + std::unique_ptr m_xSorter; + GList *m_pColumns; + std::vector m_aColumnSignalIds; + // map from toggle column to toggle visibility column + std::map m_aToggleVisMap; + // map from toggle column to tristate column + std::map m_aToggleTriStateMap; + // map from text column to text weight column + std::map m_aWeightMap; + // map from text column to sensitive column + std::map m_aSensitiveMap; + // map from text column to indent column + std::map m_aIndentMap; + // map from text column to text align column + std::map m_aAlignMap; + // currently expanding parent that logically, but not currently physically, + // contain placeholders + o3tl::sorted_vector m_aExpandingPlaceHolderParents; + // which rows are separators (rare) + std::vector> m_aSeparatorRows; + std::vector m_aSavedSortTypes; + std::vector m_aSavedSortColumns; + bool m_bWorkAroundBadDragRegion; + bool m_bInDrag; + bool m_bChangedByMouse; + gint m_nTextCol; + gint m_nTextView; + gint m_nImageCol; + gint m_nExpanderToggleCol; + gint m_nExpanderImageCol; + gint m_nIdCol; + int m_nPendingVAdjustment; + gulong m_nChangedSignalId; + gulong m_nRowActivatedSignalId; + gulong m_nTestExpandRowSignalId; + gulong m_nTestCollapseRowSignalId; + gulong m_nVAdjustmentChangedSignalId; + gulong m_nRowDeletedSignalId; + gulong m_nRowInsertedSignalId; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong m_nPopupMenuSignalId; + gulong m_nKeyPressSignalId; + gulong m_nCrossingSignalid; +#endif + gulong m_nQueryTooltipSignalId; + GtkAdjustment* m_pVAdjustment; + ImplSVEvent* m_pChangeEvent; + + DECL_LINK(async_signal_changed, void*, void); + + void launch_signal_changed() + { + //tdf#117991 selection change is sent before the focus change, and focus change + //is what will cause a spinbutton that currently has the focus to set its contents + //as the spin button value. So any LibreOffice callbacks on + //signal-change would happen before the spinbutton value-change occurs. + //To avoid this, send the signal-change to LibreOffice to occur after focus-change + //has been processed + if (m_pChangeEvent) + Application::RemoveUserEvent(m_pChangeEvent); + +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkEvent *pEvent = gtk_get_current_event(); + m_bChangedByMouse = pEvent && categorizeEvent(pEvent) == VclInputFlags::MOUSE; +#else + //TODO maybe iterate over gtk_widget_observe_controllers looking for a motion controller +#endif + + m_pChangeEvent = Application::PostUserEvent(LINK(this, GtkInstanceTreeView, async_signal_changed)); + } + + static void signalChanged(GtkTreeView*, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast(widget); + pThis->launch_signal_changed(); + } + + void handle_row_activated() + { + if (signal_row_activated()) + return; + GtkInstanceTreeIter aIter(nullptr); + if (!get_cursor(&aIter)) + return; + if (gtk_tree_model_iter_has_child(m_pTreeModel, &aIter.iter)) + get_row_expanded(aIter) ? collapse_row(aIter) : expand_row(aIter); + } + + static void signalRowActivated(GtkTreeView*, GtkTreePath*, GtkTreeViewColumn*, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->handle_row_activated(); + } + + virtual bool signal_popup_menu(const CommandEvent& rCEvt) override + { + return m_aPopupMenuHdl.Call(rCEvt); + } + + void insert_row(GtkTreeIter& iter, const GtkTreeIter* parent, int pos, const OUString* pId, const OUString* pText, + const OUString* pIconName, const VirtualDevice* pDevice) + { + m_InsertWithValues(m_pTreeModel, &iter, const_cast(parent), pos, + m_nTextCol, !pText ? nullptr : OUStringToOString(*pText, RTL_TEXTENCODING_UTF8).getStr(), + m_nIdCol, !pId ? nullptr : OUStringToOString(*pId, RTL_TEXTENCODING_UTF8).getStr()); + + if (pIconName) + { + GdkPixbuf* pixbuf = getPixbuf(*pIconName); + m_Setter(m_pTreeModel, &iter, m_nImageCol, pixbuf, -1); + if (pixbuf) + g_object_unref(pixbuf); + } + else if (pDevice) + { + cairo_surface_t* surface = get_underlying_cairo_surface(*pDevice); + + Size aSize(pDevice->GetOutputSizePixel()); + cairo_surface_t* target = cairo_surface_create_similar(surface, + cairo_surface_get_content(surface), + aSize.Width(), + aSize.Height()); + + cairo_t* cr = cairo_create(target); + cairo_set_source_surface(cr, surface, 0, 0); + cairo_paint(cr); + cairo_destroy(cr); + + m_Setter(m_pTreeModel, &iter, m_nImageCol, target, -1); + cairo_surface_destroy(target); + } + } + + bool separator_function(const GtkTreePath* path) + { + return ::separator_function(path, m_aSeparatorRows); + } + + static gboolean separatorFunction(GtkTreeModel* pTreeModel, GtkTreeIter* pIter, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast(widget); + GtkTreePath* path = gtk_tree_model_get_path(pTreeModel, pIter); + bool bRet = pThis->separator_function(path); + gtk_tree_path_free(path); + return bRet; + } + + OUString get(const GtkTreeIter& iter, int col) const + { + gchar* pStr; + gtk_tree_model_get(m_pTreeModel, const_cast(&iter), col, &pStr, -1); + OUString sRet(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + g_free(pStr); + return sRet; + } + + OUString get(int pos, int col) const + { + OUString sRet; + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + sRet = get(iter, col); + return sRet; + } + + gint get_int(const GtkTreeIter& iter, int col) const + { + gint nRet(-1); + gtk_tree_model_get(m_pTreeModel, const_cast(&iter), col, &nRet, -1); + return nRet; + } + + gint get_int(int pos, int col) const + { + gint nRet(-1); + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + nRet = get_int(iter, col); + gtk_tree_model_get(m_pTreeModel, &iter, col, &nRet, -1); + return nRet; + } + + bool get_bool(const GtkTreeIter& iter, int col) const + { + gboolean bRet(false); + gtk_tree_model_get(m_pTreeModel, const_cast(&iter), col, &bRet, -1); + return bRet; + } + + bool get_bool(int pos, int col) const + { + bool bRet(false); + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + bRet = get_bool(iter, col); + return bRet; + } + + void set_toggle(const GtkTreeIter& iter, TriState eState, int col) + { + if (col == -1) + col = m_nExpanderToggleCol; + else + col = to_internal_model(col); + + if (eState == TRISTATE_INDET) + { + m_Setter(m_pTreeModel, const_cast(&iter), + m_aToggleVisMap[col], promote_arg(true), // checkbuttons are invisible until toggled on or off + m_aToggleTriStateMap[col], promote_arg(true), // tristate on + -1); + } + else + { + m_Setter(m_pTreeModel, const_cast(&iter), + m_aToggleVisMap[col], promote_arg(true), // checkbuttons are invisible until toggled on or off + m_aToggleTriStateMap[col], promote_arg(false), // tristate off + col, promote_arg(eState == TRISTATE_TRUE), // set toggle state + -1); + } + } + + void set(const GtkTreeIter& iter, int col, std::u16string_view rText) + { + OString aStr(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)); + m_Setter(m_pTreeModel, const_cast(&iter), col, aStr.getStr(), -1); + } + + void set(int pos, int col, std::u16string_view rText) + { + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + set(iter, col, rText); + } + + void set(const GtkTreeIter& iter, int col, bool bOn) + { + m_Setter(m_pTreeModel, const_cast(&iter), col, promote_arg(bOn), -1); + } + + void set(int pos, int col, bool bOn) + { + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + set(iter, col, bOn); + } + + void set(const GtkTreeIter& iter, int col, gint bInt) + { + m_Setter(m_pTreeModel, const_cast(&iter), col, bInt, -1); + } + + void set(int pos, int col, gint bInt) + { + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + set(iter, col, bInt); + } + + void set(const GtkTreeIter& iter, int col, double fValue) + { + m_Setter(m_pTreeModel, const_cast(&iter), col, fValue, -1); + } + + void set(int pos, int col, double fValue) + { + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + set(iter, col, fValue); + } + + static gboolean signalTestExpandRow(GtkTreeView*, GtkTreeIter* iter, GtkTreePath*, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast(widget); + return !pThis->signal_test_expand_row(*iter); + } + + static gboolean signalTestCollapseRow(GtkTreeView*, GtkTreeIter* iter, GtkTreePath*, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast(widget); + return !pThis->signal_test_collapse_row(*iter); + } + + bool child_is_placeholder(GtkInstanceTreeIter& rGtkIter) const + { + GtkTreePath* pPath = gtk_tree_model_get_path(m_pTreeModel, &rGtkIter.iter); + bool bExpanding = m_aExpandingPlaceHolderParents.count(pPath); + gtk_tree_path_free(pPath); + if (bExpanding) + return true; + + bool bPlaceHolder = false; + GtkTreeIter tmp; + if (gtk_tree_model_iter_children(m_pTreeModel, &tmp, &rGtkIter.iter)) + { + rGtkIter.iter = tmp; + if (get_text(rGtkIter, -1) == "") + { + bPlaceHolder = true; + } + } + return bPlaceHolder; + } + + bool signal_test_expand_row(GtkTreeIter& iter) + { + disable_notify_events(); + + // if there's a preexisting placeholder child, required to make this + // potentially expandable in the first place, now we remove it + GtkInstanceTreeIter aIter(iter); + GtkTreePath* pPlaceHolderPath = nullptr; + bool bPlaceHolder = child_is_placeholder(aIter); + if (bPlaceHolder) + { + m_Remove(m_pTreeModel, &aIter.iter); + + pPlaceHolderPath = gtk_tree_model_get_path(m_pTreeModel, &iter); + m_aExpandingPlaceHolderParents.insert(pPlaceHolderPath); + } + + aIter.iter = iter; + bool bRet = signal_expanding(aIter); + + if (bPlaceHolder) + { + //expand disallowed, restore placeholder + if (!bRet) + { + GtkTreeIter subiter; + OUString sDummy(""); + insert_row(subiter, &iter, -1, nullptr, &sDummy, nullptr, nullptr); + } + m_aExpandingPlaceHolderParents.erase(pPlaceHolderPath); + gtk_tree_path_free(pPlaceHolderPath); + } + + enable_notify_events(); + return bRet; + } + + bool signal_test_collapse_row(const GtkTreeIter& iter) + { + disable_notify_events(); + + GtkInstanceTreeIter aIter(iter); + bool bRet = signal_collapsing(aIter); + + enable_notify_events(); + return bRet; + } + + static void signalCellToggled(GtkCellRendererToggle* pCell, const gchar *path, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast(widget); + void* pData = g_object_get_data(G_OBJECT(pCell), "g-lo-CellIndex"); + pThis->signal_cell_toggled(path, reinterpret_cast(pData)); + } + + void signal_cell_toggled(const gchar *path, int nCol) + { + GtkTreePath *tree_path = gtk_tree_path_new_from_string(path); + + // additionally set the cursor into the row the toggled element is in + gtk_tree_view_set_cursor(m_pTreeView, tree_path, nullptr, false); + + GtkTreeIter iter; + gtk_tree_model_get_iter(m_pTreeModel, &iter, tree_path); + + gboolean bRet(false); + gtk_tree_model_get(m_pTreeModel, &iter, nCol, &bRet, -1); + bRet = !bRet; + m_Setter(m_pTreeModel, &iter, nCol, bRet, -1); + + set(iter, m_aToggleTriStateMap[nCol], false); + + signal_toggled(iter_col(GtkInstanceTreeIter(iter), to_external_model(nCol))); + + gtk_tree_path_free(tree_path); + } + + DECL_LINK(async_stop_cell_editing, void*, void); + + static void signalCellEditingStarted(GtkCellRenderer*, GtkCellEditable*, const gchar *path, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast(widget); + if (!pThis->signal_cell_editing_started(path)) + Application::PostUserEvent(LINK(pThis, GtkInstanceTreeView, async_stop_cell_editing)); + } + + bool signal_cell_editing_started(const gchar *path) + { + GtkTreePath *tree_path = gtk_tree_path_new_from_string(path); + + GtkInstanceTreeIter aGtkIter(nullptr); + gtk_tree_model_get_iter(m_pTreeModel, &aGtkIter.iter, tree_path); + gtk_tree_path_free(tree_path); + + return signal_editing_started(aGtkIter); + } + + static void signalCellEdited(GtkCellRendererText* pCell, const gchar *path, const gchar *pNewText, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast(widget); + pThis->signal_cell_edited(pCell, path, pNewText); + } + + static void restoreNonEditable(GObject* pCell) + { + if (g_object_get_data(pCell, "g-lo-RestoreNonEditable")) + { + g_object_set(pCell, "editable", false, "editable-set", false, nullptr); + g_object_set_data(pCell, "g-lo-RestoreNonEditable", reinterpret_cast(false)); + } + } + + void signal_cell_edited(GtkCellRendererText* pCell, const gchar *path, const gchar* pNewText) + { + GtkTreePath *tree_path = gtk_tree_path_new_from_string(path); + + GtkInstanceTreeIter aGtkIter(nullptr); + gtk_tree_model_get_iter(m_pTreeModel, &aGtkIter.iter, tree_path); + gtk_tree_path_free(tree_path); + + OUString sText(pNewText, pNewText ? strlen(pNewText) : 0, RTL_TEXTENCODING_UTF8); + if (signal_editing_done(iter_string(aGtkIter, sText))) + { + void* pData = g_object_get_data(G_OBJECT(pCell), "g-lo-CellIndex"); + set(aGtkIter.iter, reinterpret_cast(pData), sText); + } + + restoreNonEditable(G_OBJECT(pCell)); + } + + static void signalCellEditingCanceled(GtkCellRenderer* pCell, gpointer /*widget*/) + { + restoreNonEditable(G_OBJECT(pCell)); + } + + void signal_column_clicked(GtkTreeViewColumn* pClickedColumn) + { + int nIndex(0); + for (GList* pEntry = g_list_first(m_pColumns); pEntry; pEntry = g_list_next(pEntry)) + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + if (pColumn == pClickedColumn) + { + TreeView::signal_column_clicked(nIndex); + break; + } + ++nIndex; + } + } + + static void signalColumnClicked(GtkTreeViewColumn* pColumn, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast(widget); + pThis->signal_column_clicked(pColumn); + } + + static void signalVAdjustmentChanged(GtkAdjustment*, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast(widget); + pThis->signal_visible_range_changed(); + } + + // The outside concept of a column maps to a gtk CellRenderer, rather than + // a TreeViewColumn. If the first TreeViewColumn has a leading Toggle Renderer + // and/or a leading Image Renderer, those are considered special expander + // columns and precede index 0 and can be accessed via outside index -1 + int to_external_model(int modelcol) const + { + if (m_nExpanderToggleCol != -1) + --modelcol; + if (m_nExpanderImageCol != -1) + --modelcol; + return modelcol; + } + + int to_internal_model(int modelcol) const + { + if (m_nExpanderToggleCol != -1) + ++modelcol; + if (m_nExpanderImageCol != -1) + ++modelcol; + return modelcol; + } + + void set_column_editable(int nCol, bool bEditable) + { + nCol = to_internal_model(nCol); + + for (GList* pEntry = g_list_first(m_pColumns); pEntry; pEntry = g_list_next(pEntry)) + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + GList *pRenderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(pColumn)); + for (GList* pRenderer = g_list_first(pRenderers); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + void* pData = g_object_get_data(G_OBJECT(pCellRenderer), "g-lo-CellIndex"); + if (reinterpret_cast(pData) == nCol) + { + g_object_set(G_OBJECT(pCellRenderer), "editable", bEditable, "editable-set", true, nullptr); + break; + } + } + g_list_free(pRenderers); + } + } + + static void signalRowDeleted(GtkTreeModel*, GtkTreePath*, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast(widget); + pThis->signal_model_changed(); + } + + static void signalRowInserted(GtkTreeModel*, GtkTreePath*, GtkTreeIter*, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast(widget); + pThis->signal_model_changed(); + } + + static gint sortFunc(GtkTreeModel* pModel, GtkTreeIter* a, GtkTreeIter* b, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast(widget); + return pThis->sort_func(pModel, a, b); + } + + gint sort_func(GtkTreeModel* pModel, GtkTreeIter* a, GtkTreeIter* b) + { + if (m_aCustomSort) + return m_aCustomSort(GtkInstanceTreeIter(*a), GtkInstanceTreeIter(*b)); + return default_sort_func(pModel, a, b, m_xSorter.get()); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + bool signal_key_press(GdkEventKey* pEvent) + { + if (pEvent->keyval != GDK_KEY_Left && pEvent->keyval != GDK_KEY_Right) + return false; + + GtkInstanceTreeIter aIter(nullptr); + if (!get_cursor(&aIter)) + return false; + + bool bHasChild = gtk_tree_model_iter_has_child(m_pTreeModel, &aIter.iter); + + if (pEvent->keyval == GDK_KEY_Right) + { + if (bHasChild && !get_row_expanded(aIter)) + { + expand_row(aIter); + return true; + } + return false; + } + + if (bHasChild && get_row_expanded(aIter)) + { + collapse_row(aIter); + return true; + } + + if (iter_parent(aIter)) + { + unselect_all(); + set_cursor(aIter); + select(aIter); + return true; + } + + return false; + } + + static gboolean signalKeyPress(GtkWidget*, GdkEventKey* pEvent, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast(widget); + return pThis->signal_key_press(pEvent); + } +#endif + + static gboolean signalQueryTooltip(GtkWidget* /*pGtkWidget*/, gint x, gint y, + gboolean keyboard_tip, GtkTooltip *tooltip, + gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast(widget); + GtkTreeIter iter; + GtkTreeView *pTreeView = pThis->m_pTreeView; + GtkTreeModel *pModel = gtk_tree_view_get_model(pTreeView); + GtkTreePath *pPath = nullptr; +#if GTK_CHECK_VERSION(4, 0, 0) + if (!gtk_tree_view_get_tooltip_context(pTreeView, x, y, keyboard_tip, &pModel, &pPath, &iter)) + return false; +#else + if (!gtk_tree_view_get_tooltip_context(pTreeView, &x, &y, keyboard_tip, &pModel, &pPath, &iter)) + return false; +#endif + OUString aTooltip = pThis->signal_query_tooltip(GtkInstanceTreeIter(iter)); + if (!aTooltip.isEmpty()) + { + gtk_tooltip_set_text(tooltip, OUStringToOString(aTooltip, RTL_TEXTENCODING_UTF8).getStr()); + gtk_tree_view_set_tooltip_row(pTreeView, tooltip, pPath); + } + gtk_tree_path_free(pPath); + return !aTooltip.isEmpty(); + } + + void last_child(GtkTreeModel* pModel, GtkTreeIter* result, GtkTreeIter* pParent, int nChildren) const + { + gtk_tree_model_iter_nth_child(pModel, result, pParent, nChildren - 1); + nChildren = gtk_tree_model_iter_n_children(pModel, result); + if (nChildren) + { + GtkTreeIter newparent(*result); + last_child(pModel, result, &newparent, nChildren); + } + } + + GtkTreePath* get_path_of_last_entry(GtkTreeModel *pModel) + { + GtkTreePath *lastpath; + // find the last entry in the model for comparison + int nChildren = gtk_tree_model_iter_n_children(pModel, nullptr); + if (!nChildren) + lastpath = gtk_tree_path_new_from_indices(0, -1); + else + { + GtkTreeIter iter; + last_child(pModel, &iter, nullptr, nChildren); + lastpath = gtk_tree_model_get_path(pModel, &iter); + } + return lastpath; + } + + void set_font_color(const GtkTreeIter& iter, const Color& rColor) + { + if (rColor == COL_AUTO) + m_Setter(m_pTreeModel, const_cast(&iter), m_nIdCol + 1, nullptr, -1); + else + { + GdkRGBA aColor{rColor.GetRed()/255.0f, rColor.GetGreen()/255.0f, rColor.GetBlue()/255.0f, 0}; + m_Setter(m_pTreeModel, const_cast(&iter), m_nIdCol + 1, &aColor, -1); + } + } + + int get_expander_size() const + { + // gtk4: _TREE_VIEW_EXPANDER_SIZE define in gtk/gtktreeview.c + gint nExpanderSize = 16; + // gtk4: _TREE_VIEW_HORIZONTAL_SEPARATOR define in gtk/gtktreeview.c + gint nHorizontalSeparator = 4; + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_style_get(GTK_WIDGET(m_pTreeView), + "expander-size", &nExpanderSize, + "horizontal-separator", &nHorizontalSeparator, + nullptr); +#endif + + return nExpanderSize + (nHorizontalSeparator/ 2); + } + + void real_vadjustment_set_value(int value) + { + disable_notify_events(); + gtk_adjustment_set_value(m_pVAdjustment, value); + enable_notify_events(); + } + + static gboolean setAdjustmentCallback(GtkWidget*, GdkFrameClock*, gpointer widget) + { + GtkInstanceTreeView* pThis = static_cast(widget); + if (pThis->m_nPendingVAdjustment != -1) + { + pThis->real_vadjustment_set_value(pThis->m_nPendingVAdjustment); + pThis->m_nPendingVAdjustment = -1; + } + return false; + } + + bool iter_next(weld::TreeIter& rIter, bool bOnlyExpanded) const + { + GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + GtkTreeIter tmp; + GtkTreeIter iter = rGtkIter.iter; + + bool ret = gtk_tree_model_iter_children(m_pTreeModel, &tmp, &iter); + if (ret && bOnlyExpanded && !get_row_expanded(rGtkIter)) + ret = false; + rGtkIter.iter = tmp; + if (ret) + { + //on-demand dummy entry doesn't count + if (get_text(rGtkIter, -1) == "") + return iter_next(rGtkIter, bOnlyExpanded); + return true; + } + + tmp = iter; + if (gtk_tree_model_iter_next(m_pTreeModel, &tmp)) + { + rGtkIter.iter = tmp; + //on-demand dummy entry doesn't count + if (get_text(rGtkIter, -1) == "") + return iter_next(rGtkIter, bOnlyExpanded); + return true; + } + // Move up level(s) until we find the level where the next node exists. + while (gtk_tree_model_iter_parent(m_pTreeModel, &tmp, &iter)) + { + iter = tmp; + if (gtk_tree_model_iter_next(m_pTreeModel, &tmp)) + { + rGtkIter.iter = tmp; + //on-demand dummy entry doesn't count + if (get_text(rGtkIter, -1) == "") + return iter_next(rGtkIter, bOnlyExpanded); + return true; + } + } + return false; + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + // tdf#154565 ignore the crossing event if it was triggered ultimately by a + // key stroke which is likely from exiting the search box. This way we can + // avoid the problem that with hover-selection that after return is used in + // the search box, selecting a matching row, that during teardown of the + // widget the box is hidden, and the crossing notification triggers + // selection of a different row under the mouse. If needs be this could be + // refined further to only happen for a specific key or other details of + // the triggering event + static gboolean signalCrossing(GtkWidget*, GdkEventCrossing*, gpointer) + { + if (GdkEvent *pEvent = gtk_get_current_event()) + { + const bool bCrossingTriggeredByKeyStroke = gdk_event_get_event_type(pEvent) == GDK_KEY_PRESS; + gdk_event_free(pEvent); + return bCrossingTriggeredByKeyStroke; + } + + return false; + } +#endif + +public: + GtkInstanceTreeView(GtkTreeView* pTreeView, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pTreeView), pBuilder, bTakeOwnership) + , m_pTreeView(pTreeView) + , m_pTreeModel(gtk_tree_view_get_model(m_pTreeView)) + , m_bWorkAroundBadDragRegion(false) + , m_bInDrag(false) + , m_bChangedByMouse(false) + , m_nTextCol(-1) + , m_nTextView(-1) + , m_nImageCol(-1) + , m_nExpanderToggleCol(-1) + , m_nExpanderImageCol(-1) + , m_nPendingVAdjustment(-1) + , m_nChangedSignalId(g_signal_connect(gtk_tree_view_get_selection(pTreeView), "changed", + G_CALLBACK(signalChanged), this)) + , m_nRowActivatedSignalId(g_signal_connect(pTreeView, "row-activated", G_CALLBACK(signalRowActivated), this)) + , m_nTestExpandRowSignalId(g_signal_connect(pTreeView, "test-expand-row", G_CALLBACK(signalTestExpandRow), this)) + , m_nTestCollapseRowSignalId(g_signal_connect(pTreeView, "test-collapse-row", G_CALLBACK(signalTestCollapseRow), this)) + , m_nVAdjustmentChangedSignalId(0) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_nPopupMenuSignalId(g_signal_connect(pTreeView, "popup-menu", G_CALLBACK(signalPopupMenu), this)) + , m_nKeyPressSignalId(g_signal_connect(pTreeView, "key-press-event", G_CALLBACK(signalKeyPress), this)) + , m_nCrossingSignalid(g_signal_connect(pTreeView, "enter-notify-event", G_CALLBACK(signalCrossing), this)) +#endif + , m_nQueryTooltipSignalId(0) + , m_pVAdjustment(gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(pTreeView))) + , m_pChangeEvent(nullptr) + { + if (GTK_IS_TREE_STORE(m_pTreeModel)) + { + m_Setter = tree_store_set; + m_InsertWithValues = tree_store_insert_with_values; + m_Insert = tree_store_insert; + m_Prepend = tree_store_prepend; + m_Remove = tree_store_remove; + m_Swap = tree_store_swap; + m_SetValue = tree_store_set_value; + m_Clear = tree_store_clear; + } + else + { + /* + tdf#136559 see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2693 + If we only need a list and not a tree we can get a performance boost from using a ListStore + */ + assert(!gtk_tree_view_get_show_expanders(m_pTreeView) && "a liststore can only be used if no tree structure is needed"); + m_Setter = list_store_set; + m_InsertWithValues = list_store_insert_with_values; + m_Insert = list_store_insert; + m_Prepend = list_store_prepend; + m_Remove = list_store_remove; + m_Swap = list_store_swap; + m_SetValue = list_store_set_value; + m_Clear = list_store_clear; + } + + /* The outside concept of a column maps to a gtk CellRenderer, rather than + a TreeViewColumn. If the first TreeViewColumn has a leading Toggle Renderer + and/or a leading Image Renderer, those are considered special expander + columns and precede index 0 and can be accessed via outside index -1 + */ + m_pColumns = gtk_tree_view_get_columns(m_pTreeView); + int nIndex(0); + int nViewColumn(0); + for (GList* pEntry = g_list_first(m_pColumns); pEntry; pEntry = g_list_next(pEntry)) + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + m_aColumnSignalIds.push_back(g_signal_connect(pColumn, "clicked", G_CALLBACK(signalColumnClicked), this)); + GList *pRenderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(pColumn)); + for (GList* pRenderer = g_list_first(pRenderers); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + if (GTK_IS_CELL_RENDERER_TEXT(pCellRenderer)) + { + if (m_nTextCol == -1) + { + m_nTextCol = nIndex; + m_nTextView = nViewColumn; + } + m_aWeightMap[nIndex] = -1; + m_aSensitiveMap[nIndex] = -1; + m_aIndentMap[nIndex] = -1; + m_aAlignMap[nIndex] = -1; + g_signal_connect(G_OBJECT(pCellRenderer), "editing-started", G_CALLBACK(signalCellEditingStarted), this); + g_signal_connect(G_OBJECT(pCellRenderer), "editing-canceled", G_CALLBACK(signalCellEditingCanceled), this); + g_signal_connect(G_OBJECT(pCellRenderer), "edited", G_CALLBACK(signalCellEdited), this); + } + else if (GTK_IS_CELL_RENDERER_TOGGLE(pCellRenderer)) + { + const bool bExpander = nIndex == 0 || (nIndex == 1 && m_nExpanderImageCol == 0); + if (bExpander) + m_nExpanderToggleCol = nIndex; + g_signal_connect(G_OBJECT(pCellRenderer), "toggled", G_CALLBACK(signalCellToggled), this); + m_aToggleVisMap[nIndex] = -1; + m_aToggleTriStateMap[nIndex] = -1; + } + else if (GTK_IS_CELL_RENDERER_PIXBUF(pCellRenderer)) + { + const bool bExpander = g_list_next(pRenderer) != nullptr; + if (bExpander && m_nExpanderImageCol == -1) + m_nExpanderImageCol = nIndex; + else if (m_nImageCol == -1) + m_nImageCol = nIndex; + } + g_object_set_data(G_OBJECT(pCellRenderer), "g-lo-CellIndex", reinterpret_cast(nIndex)); + ++nIndex; + } + g_list_free(pRenderers); + ++nViewColumn; + } + + m_nIdCol = nIndex++; + + for (auto& a : m_aToggleVisMap) + a.second = nIndex++; + for (auto& a : m_aToggleTriStateMap) + a.second = nIndex++; + for (auto& a : m_aWeightMap) + a.second = nIndex++; + for (auto& a : m_aSensitiveMap) + a.second = nIndex++; + for (auto& a : m_aIndentMap) + a.second = nIndex++; + for (auto& a : m_aAlignMap) + a.second = nIndex++; + + ensure_drag_begin_end(); + + m_nRowDeletedSignalId = g_signal_connect(m_pTreeModel, "row-deleted", G_CALLBACK(signalRowDeleted), this); + m_nRowInsertedSignalId = g_signal_connect(m_pTreeModel, "row-inserted", G_CALLBACK(signalRowInserted), this); + } + + virtual void connect_query_tooltip(const Link& rLink) override + { + weld::TreeView::connect_query_tooltip(rLink); + m_nQueryTooltipSignalId = g_signal_connect(m_pTreeView, "query-tooltip", G_CALLBACK(signalQueryTooltip), this); + } + + virtual void columns_autosize() override + { + gtk_tree_view_columns_autosize(m_pTreeView); + } + + virtual void set_column_fixed_widths(const std::vector& rWidths) override + { + GList* pEntry = g_list_first(m_pColumns); + for (auto nWidth : rWidths) + { + assert(pEntry && "wrong count"); + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + gtk_tree_view_column_set_fixed_width(pColumn, nWidth); + pEntry = g_list_next(pEntry); + } + } + + virtual void set_column_editables(const std::vector& rEditables) override + { + size_t nTabCount = rEditables.size(); + for (size_t i = 0 ; i < nTabCount; ++i) + set_column_editable(i, rEditables[i]); + } + + virtual void set_centered_column(int nCol) override + { + for (GList* pEntry = g_list_first(m_pColumns); pEntry; pEntry = g_list_next(pEntry)) + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + GList *pRenderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(pColumn)); + for (GList* pRenderer = g_list_first(pRenderers); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + void* pData = g_object_get_data(G_OBJECT(pCellRenderer), "g-lo-CellIndex"); + if (reinterpret_cast(pData) == nCol) + { + g_object_set(G_OBJECT(pCellRenderer), "xalign", 0.5, nullptr); + break; + } + } + g_list_free(pRenderers); + } + } + + virtual int get_column_width(int nColumn) const override + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(g_list_nth_data(m_pColumns, nColumn)); + assert(pColumn && "wrong count"); + int nWidth = gtk_tree_view_column_get_width(pColumn); + // https://github.com/exaile/exaile/issues/580 + // after setting fixed_width on a column and requesting width before + // gtk has a chance to do its layout of the column means that the width + // request hasn't come into effect + if (!nWidth) + nWidth = gtk_tree_view_column_get_fixed_width(pColumn); + return nWidth; + } + + virtual OUString get_column_title(int nColumn) const override + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(g_list_nth_data(m_pColumns, nColumn)); + assert(pColumn && "wrong count"); + const gchar* pTitle = gtk_tree_view_column_get_title(pColumn); + OUString sRet(pTitle, pTitle ? strlen(pTitle) : 0, RTL_TEXTENCODING_UTF8); + return sRet; + } + + virtual void set_column_title(int nColumn, const OUString& rTitle) override + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(g_list_nth_data(m_pColumns, nColumn)); + assert(pColumn && "wrong count"); + gtk_tree_view_column_set_title(pColumn, OUStringToOString(rTitle, RTL_TEXTENCODING_UTF8).getStr()); + } + + virtual void set_column_custom_renderer(int nColumn, bool bEnable) override + { + assert(n_children() == 0 && "tree must be empty"); + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(g_list_nth_data(m_pColumns, nColumn)); + assert(pColumn && "wrong count"); + + GtkCellRenderer* pExpander = nullptr; + GtkCellRenderer* pToggle = nullptr; + + // migrate existing editable setting to the new renderer + gboolean is_editable(false); + void* pEditCellData(nullptr); + GList *pRenderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(pColumn)); + for (GList* pRenderer = g_list_first(pRenderers); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + + void* pData = g_object_get_data(G_OBJECT(pCellRenderer), "g-lo-CellIndex"); + auto nCellIndex = reinterpret_cast(pData); + + if (GTK_IS_CELL_RENDERER_TEXT(pCellRenderer)) + { + g_object_get(pCellRenderer, "editable", &is_editable, nullptr); + pEditCellData = pData; + break; + } + else if (GTK_IS_CELL_RENDERER_TOGGLE(pCellRenderer)) + { + if (nCellIndex == m_nExpanderToggleCol) + { + pToggle = pCellRenderer; + g_object_ref(pToggle); + } + } + else if (GTK_IS_CELL_RENDERER_PIXBUF(pCellRenderer)) + { + if (nCellIndex == m_nExpanderImageCol) + { + pExpander = pCellRenderer; + g_object_ref(pExpander); + } + } + + } + g_list_free(pRenderers); + + GtkCellRenderer* pRenderer; + + gtk_cell_layout_clear(GTK_CELL_LAYOUT(pColumn)); + if (pExpander) + { + gtk_tree_view_column_pack_start(pColumn, pExpander, false); + gtk_tree_view_column_add_attribute(pColumn, pExpander, "pixbuf", m_nExpanderImageCol); + g_object_unref(pExpander); + } + if (pToggle) + { + gtk_tree_view_column_pack_start(pColumn, pToggle, false); + gtk_tree_view_column_add_attribute(pColumn, pToggle, "active", m_nExpanderToggleCol); + gtk_tree_view_column_add_attribute(pColumn, pToggle, "active", m_nExpanderToggleCol); + gtk_tree_view_column_add_attribute(pColumn, pToggle, "visible", m_aToggleTriStateMap[m_nExpanderToggleCol]); + g_object_unref(pToggle); + } + + if (bEnable) + { + pRenderer = custom_cell_renderer_new(); + GValue value = G_VALUE_INIT; + g_value_init(&value, G_TYPE_POINTER); + g_value_set_pointer(&value, static_cast(this)); + g_object_set_property(G_OBJECT(pRenderer), "instance", &value); + gtk_tree_view_column_pack_start(pColumn, pRenderer, true); + gtk_tree_view_column_add_attribute(pColumn, pRenderer, "text", m_nTextCol); + gtk_tree_view_column_add_attribute(pColumn, pRenderer, "id", m_nIdCol); + } + else + { + pRenderer = gtk_cell_renderer_text_new(); + gtk_tree_view_column_pack_start(pColumn, pRenderer, true); + gtk_tree_view_column_add_attribute(pColumn, pRenderer, "text", m_nTextCol); + } + + if (is_editable) + { + g_object_set(pRenderer, "editable", true, "editable-set", true, nullptr); + g_object_set_data(G_OBJECT(pRenderer), "g-lo-CellIndex", pEditCellData); + g_signal_connect(pRenderer, "editing-started", G_CALLBACK(signalCellEditingStarted), this); + g_signal_connect(pRenderer, "editing-canceled", G_CALLBACK(signalCellEditingCanceled), this); + g_signal_connect(pRenderer, "edited", G_CALLBACK(signalCellEdited), this); + } + } + + virtual void queue_draw() override + { + gtk_widget_queue_draw(GTK_WIDGET(m_pTreeView)); + } + + virtual void insert(const weld::TreeIter* pParent, int pos, const OUString* pText, const OUString* pId, const OUString* pIconName, + VirtualDevice* pImageSurface, + bool bChildrenOnDemand, weld::TreeIter* pRet) override + { + disable_notify_events(); + GtkTreeIter iter; + const GtkInstanceTreeIter* pGtkIter = static_cast(pParent); + insert_row(iter, pGtkIter ? &pGtkIter->iter : nullptr, pos, pId, pText, pIconName, pImageSurface); + if (bChildrenOnDemand) + { + GtkTreeIter subiter; + OUString sDummy(""); + insert_row(subiter, &iter, -1, nullptr, &sDummy, nullptr, nullptr); + } + if (pRet) + { + GtkInstanceTreeIter* pGtkRetIter = static_cast(pRet); + pGtkRetIter->iter = iter; + } + enable_notify_events(); + } + + virtual void insert_separator(int pos, const OUString& rId) override + { + disable_notify_events(); + GtkTreeIter iter; + if (!gtk_tree_view_get_row_separator_func(m_pTreeView)) + gtk_tree_view_set_row_separator_func(m_pTreeView, separatorFunction, this, nullptr); + insert_row(iter, nullptr, pos, &rId, nullptr, nullptr, nullptr); + GtkTreePath* pPath = gtk_tree_model_get_path(m_pTreeModel, &iter); + m_aSeparatorRows.emplace_back(gtk_tree_row_reference_new(m_pTreeModel, pPath)); + gtk_tree_path_free(pPath); + enable_notify_events(); + } + + virtual void set_font_color(int pos, const Color& rColor) override + { + GtkTreeIter iter; + gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos); + set_font_color(iter, rColor); + } + + virtual void set_font_color(const weld::TreeIter& rIter, const Color& rColor) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + set_font_color(rGtkIter.iter, rColor); + } + + virtual void remove(int pos) override + { + disable_notify_events(); + GtkTreeIter iter; + gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos); + m_Remove(m_pTreeModel, &iter); + enable_notify_events(); + } + + virtual int find_text(const OUString& rText) const override + { + Search aSearch(rText, m_nTextCol); + gtk_tree_model_foreach(m_pTreeModel, foreach_find, &aSearch); + return aSearch.index; + } + + virtual int find_id(const OUString& rId) const override + { + Search aSearch(rId, m_nIdCol); + gtk_tree_model_foreach(m_pTreeModel, foreach_find, &aSearch); + return aSearch.index; + } + + virtual void bulk_insert_for_each(int nSourceCount, const std::function& func, + const weld::TreeIter* pParent, + const std::vector* pFixedWidths) override + { + GtkInstanceTreeIter* pGtkIter = const_cast(static_cast(pParent)); + + freeze(); + if (!pGtkIter) + clear(); + else + { + GtkTreeIter restore(pGtkIter->iter); + + if (iter_children(*pGtkIter)) + while (m_Remove(m_pTreeModel, &pGtkIter->iter)); + + pGtkIter->iter = restore; + } + GtkInstanceTreeIter aGtkIter(nullptr); + + if (pFixedWidths) + set_column_fixed_widths(*pFixedWidths); + + while (nSourceCount) + { + // tdf#125241 inserting backwards is massively faster + m_Prepend(m_pTreeModel, &aGtkIter.iter, pGtkIter ? &pGtkIter->iter : nullptr); + func(aGtkIter, --nSourceCount); + } + + thaw(); + } + + virtual void swap(int pos1, int pos2) override + { + disable_notify_events(); + + GtkTreeIter iter1; + gtk_tree_model_iter_nth_child(m_pTreeModel, &iter1, nullptr, pos1); + + GtkTreeIter iter2; + gtk_tree_model_iter_nth_child(m_pTreeModel, &iter2, nullptr, pos2); + + m_Swap(m_pTreeModel, &iter1, &iter2); + + enable_notify_events(); + } + + virtual void clear() override + { + disable_notify_events(); + gtk_tree_view_set_row_separator_func(m_pTreeView, nullptr, nullptr, nullptr); + m_aSeparatorRows.clear(); + m_Clear(m_pTreeModel); + enable_notify_events(); + } + + virtual void make_sorted() override + { + // thaw wants to restore sort state of freeze + assert(gtk_tree_view_get_model(m_pTreeView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + m_xSorter.reset(new comphelper::string::NaturalStringSorter( + ::comphelper::getProcessComponentContext(), + Application::GetSettings().GetUILanguageTag().getLocale())); + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_set_sort_func(pSortable, m_nTextCol, sortFunc, this, nullptr); + gtk_tree_sortable_set_sort_column_id(pSortable, m_nTextCol, GTK_SORT_ASCENDING); + } + + virtual void make_unsorted() override + { + m_xSorter.reset(); + int nSortColumn; + GtkSortType eSortType; + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_get_sort_column_id(pSortable, &nSortColumn, &eSortType); + gtk_tree_sortable_set_sort_column_id(pSortable, GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, eSortType); + } + + virtual void set_sort_order(bool bAscending) override + { + GtkSortType eSortType = bAscending ? GTK_SORT_ASCENDING : GTK_SORT_DESCENDING; + + gint sort_column_id(0); + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_get_sort_column_id(pSortable, &sort_column_id, nullptr); + gtk_tree_sortable_set_sort_column_id(pSortable, sort_column_id, eSortType); + } + + virtual bool get_sort_order() const override + { + int nSortColumn; + GtkSortType eSortType; + + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_get_sort_column_id(pSortable, &nSortColumn, &eSortType); + return nSortColumn != GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID && eSortType == GTK_SORT_ASCENDING; + } + + virtual void set_sort_indicator(TriState eState, int col) override + { + assert(col >= 0 && "cannot sort on expander column"); + + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(g_list_nth_data(m_pColumns, col)); + assert(pColumn && "wrong count"); + if (eState == TRISTATE_INDET) + gtk_tree_view_column_set_sort_indicator(pColumn, false); + else + { + gtk_tree_view_column_set_sort_indicator(pColumn, true); + GtkSortType eSortType = eState == TRISTATE_TRUE ? GTK_SORT_ASCENDING : GTK_SORT_DESCENDING; + gtk_tree_view_column_set_sort_order(pColumn, eSortType); + } + } + + virtual TriState get_sort_indicator(int col) const override + { + assert(col >= 0 && "cannot sort on expander column"); + + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(g_list_nth_data(m_pColumns, col)); + if (!gtk_tree_view_column_get_sort_indicator(pColumn)) + return TRISTATE_INDET; + return gtk_tree_view_column_get_sort_order(pColumn) == GTK_SORT_ASCENDING ? TRISTATE_TRUE : TRISTATE_FALSE; + } + + virtual int get_sort_column() const override + { + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gint sort_column_id(0); + if (!gtk_tree_sortable_get_sort_column_id(pSortable, &sort_column_id, nullptr)) + return -1; + return to_external_model(sort_column_id); + } + + virtual void set_sort_column(int nColumn) override + { + if (nColumn == -1) + { + make_unsorted(); + return; + } + GtkSortType eSortType; + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_get_sort_column_id(pSortable, nullptr, &eSortType); + int nSortCol = to_internal_model(nColumn); + gtk_tree_sortable_set_sort_func(pSortable, nSortCol, sortFunc, this, nullptr); + gtk_tree_sortable_set_sort_column_id(pSortable, nSortCol, eSortType); + } + + virtual void set_sort_func(const std::function& func) override + { + weld::TreeView::set_sort_func(func); + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_sort_column_changed(pSortable); + } + + virtual int n_children() const override + { + return gtk_tree_model_iter_n_children(m_pTreeModel, nullptr); + } + + virtual int iter_n_children(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + return gtk_tree_model_iter_n_children(m_pTreeModel, const_cast(&rGtkIter.iter)); + } + + virtual void select(int pos) override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + if (pos == -1 || (pos == 0 && n_children() == 0)) + { + gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(m_pTreeView)); + } + else + { + GtkTreePath* path = gtk_tree_path_new_from_indices(pos, -1); + gtk_tree_selection_select_path(gtk_tree_view_get_selection(m_pTreeView), path); + gtk_tree_view_scroll_to_cell(m_pTreeView, path, nullptr, false, 0, 0); + gtk_tree_path_free(path); + } + enable_notify_events(); + } + + virtual void set_cursor(int pos) override + { + disable_notify_events(); + GtkTreePath* path; + if (pos != -1) + { + path = gtk_tree_path_new_from_indices(pos, -1); + gtk_tree_view_scroll_to_cell(m_pTreeView, path, nullptr, false, 0, 0); + } + else + path = gtk_tree_path_new_from_indices(G_MAXINT, -1); + gtk_tree_view_set_cursor(m_pTreeView, path, nullptr, false); + gtk_tree_path_free(path); + enable_notify_events(); + } + + virtual void scroll_to_row(int pos) override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + GtkTreePath* path = gtk_tree_path_new_from_indices(pos, -1); + gtk_tree_view_expand_to_path(m_pTreeView, path); + gtk_tree_view_scroll_to_cell(m_pTreeView, path, nullptr, true, 0, 0); + gtk_tree_path_free(path); + enable_notify_events(); + } + + virtual bool is_selected(int pos) const override + { + GtkTreeIter iter; + gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos); + return gtk_tree_selection_iter_is_selected(gtk_tree_view_get_selection(m_pTreeView), &iter); + } + + virtual void unselect(int pos) override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + if (pos == -1 || (pos == 0 && n_children() == 0)) + { + gtk_tree_selection_select_all(gtk_tree_view_get_selection(m_pTreeView)); + } + else + { + GtkTreePath* path = gtk_tree_path_new_from_indices(pos, -1); + gtk_tree_selection_unselect_path(gtk_tree_view_get_selection(m_pTreeView), path); + gtk_tree_path_free(path); + } + enable_notify_events(); + } + + virtual std::vector get_selected_rows() const override + { + std::vector aRows; + + GList* pList = gtk_tree_selection_get_selected_rows(gtk_tree_view_get_selection(m_pTreeView), nullptr); + for (GList* pItem = g_list_first(pList); pItem; pItem = g_list_next(pItem)) + { + GtkTreePath* path = static_cast(pItem->data); + + gint depth; + gint* indices = gtk_tree_path_get_indices_with_depth(path, &depth); + int nRow = indices[depth-1]; + + aRows.push_back(nRow); + } + g_list_free_full(pList, reinterpret_cast(gtk_tree_path_free)); + + return aRows; + } + + virtual void all_foreach(const std::function& func) override + { + g_object_freeze_notify(G_OBJECT(m_pTreeModel)); + + GtkInstanceTreeIter aGtkIter(nullptr); + if (get_iter_first(aGtkIter)) + { + do + { + if (func(aGtkIter)) + break; + } while (iter_next(aGtkIter)); + } + + g_object_thaw_notify(G_OBJECT(m_pTreeModel)); + } + + virtual void selected_foreach(const std::function& func) override + { + g_object_freeze_notify(G_OBJECT(m_pTreeModel)); + + GtkInstanceTreeIter aGtkIter(nullptr); + + GtkTreeModel* pModel; + GList* pList = gtk_tree_selection_get_selected_rows(gtk_tree_view_get_selection(m_pTreeView), &pModel); + for (GList* pItem = g_list_first(pList); pItem; pItem = g_list_next(pItem)) + { + GtkTreePath* path = static_cast(pItem->data); + gtk_tree_model_get_iter(pModel, &aGtkIter.iter, path); + if (func(aGtkIter)) + break; + } + g_list_free_full(pList, reinterpret_cast(gtk_tree_path_free)); + + g_object_thaw_notify(G_OBJECT(m_pTreeModel)); + } + + virtual void visible_foreach(const std::function& func) override + { + g_object_freeze_notify(G_OBJECT(m_pTreeModel)); + + GtkTreePath* start_path; + GtkTreePath* end_path; + + if (!gtk_tree_view_get_visible_range(m_pTreeView, &start_path, &end_path)) + return; + + GtkInstanceTreeIter aGtkIter(nullptr); + gtk_tree_model_get_iter(m_pTreeModel, &aGtkIter.iter, start_path); + + do + { + if (func(aGtkIter)) + break; + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, &aGtkIter.iter); + bool bContinue = gtk_tree_path_compare(path, end_path) != 0; + gtk_tree_path_free(path); + if (!bContinue) + break; + if (!iter_next(aGtkIter)) + break; + } while(true); + + gtk_tree_path_free(start_path); + gtk_tree_path_free(end_path); + + g_object_thaw_notify(G_OBJECT(m_pTreeModel)); + } + + virtual void connect_visible_range_changed(const Link& rLink) override + { + weld::TreeView::connect_visible_range_changed(rLink); + if (!m_nVAdjustmentChangedSignalId) + { + GtkAdjustment* pVAdjustment = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(m_pTreeView)); + m_nVAdjustmentChangedSignalId = g_signal_connect(pVAdjustment, "value-changed", G_CALLBACK(signalVAdjustmentChanged), this); + } + } + + virtual bool is_selected(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + return gtk_tree_selection_iter_is_selected(gtk_tree_view_get_selection(m_pTreeView), const_cast(&rGtkIter.iter)); + } + + virtual OUString get_text(int pos, int col) const override + { + if (col == -1) + col = m_nTextCol; + else + col = to_internal_model(col); + return get(pos, col); + } + + virtual void set_text(int pos, const OUString& rText, int col) override + { + if (col == -1) + col = m_nTextCol; + else + col = to_internal_model(col); + set(pos, col, rText); + } + + virtual TriState get_toggle(int pos, int col) const override + { + if (col == -1) + col = m_nExpanderToggleCol; + else + col = to_internal_model(col); + + const auto iter = m_aToggleTriStateMap.find(col); + assert(iter != m_aToggleTriStateMap.end()); + if (get_bool(pos, iter->second)) + return TRISTATE_INDET; + return get_bool(pos, col) ? TRISTATE_TRUE : TRISTATE_FALSE; + } + + virtual TriState get_toggle(const weld::TreeIter& rIter, int col) const override + { + if (col == -1) + col = m_nExpanderToggleCol; + else + col = to_internal_model(col); + + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + const auto iter = m_aToggleTriStateMap.find(col); + assert(iter != m_aToggleTriStateMap.end()); + if (get_bool(rGtkIter.iter, iter->second)) + return TRISTATE_INDET; + return get_bool(rGtkIter.iter, col) ? TRISTATE_TRUE : TRISTATE_FALSE; + } + + virtual void set_toggle(const weld::TreeIter& rIter, TriState eState, int col) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + set_toggle(rGtkIter.iter, eState, col); + } + + virtual void set_toggle(int pos, TriState eState, int col) override + { + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + set_toggle(iter, eState, col); + } + + virtual void enable_toggle_buttons(weld::ColumnToggleType eType) override + { + for (GList* pEntry = g_list_first(m_pColumns); pEntry; pEntry = g_list_next(pEntry)) + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + GList *pRenderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(pColumn)); + for (GList* pRenderer = g_list_first(pRenderers); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + if (!GTK_IS_CELL_RENDERER_TOGGLE(pCellRenderer)) + continue; + GtkCellRendererToggle* pToggle = GTK_CELL_RENDERER_TOGGLE(pCellRenderer); + gtk_cell_renderer_toggle_set_radio(pToggle, eType == weld::ColumnToggleType::Radio); + } + g_list_free(pRenderers); + } + } + + virtual void set_clicks_to_toggle(int /*nToggleBehavior*/) override + { + } + + virtual void set_extra_row_indent(const weld::TreeIter& rIter, int nIndentLevel) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + set(rGtkIter.iter, m_aIndentMap[m_nTextCol], nIndentLevel * get_expander_size()); + } + + virtual void set_text_emphasis(const weld::TreeIter& rIter, bool bOn, int col) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + auto weight = bOn ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL; + if (col == -1) + { + for (const auto& elem : m_aWeightMap) + set(rGtkIter.iter, elem.second, weight); + return; + } + col = to_internal_model(col); + set(rGtkIter.iter, m_aWeightMap[col], weight); + } + + virtual void set_text_emphasis(int pos, bool bOn, int col) override + { + auto weight = bOn ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL; + if (col == -1) + { + for (const auto& elem : m_aWeightMap) + set(pos, elem.second, weight); + return; + } + col = to_internal_model(col); + set(pos, m_aWeightMap[col], weight); + } + + virtual bool get_text_emphasis(const weld::TreeIter& rIter, int col) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + col = to_internal_model(col); + const auto iter = m_aWeightMap.find(col); + assert(iter != m_aWeightMap.end()); + return get_int(rGtkIter.iter, iter->second) == PANGO_WEIGHT_BOLD; + } + + virtual bool get_text_emphasis(int pos, int col) const override + { + col = to_internal_model(col); + const auto iter = m_aWeightMap.find(col); + assert(iter != m_aWeightMap.end()); + return get_int(pos, iter->second) == PANGO_WEIGHT_BOLD; + } + + virtual void set_text_align(const weld::TreeIter& rIter, double fAlign, int col) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + col = to_internal_model(col); + set(rGtkIter.iter, m_aAlignMap[col], fAlign); + } + + virtual void set_text_align(int pos, double fAlign, int col) override + { + col = to_internal_model(col); + set(pos, m_aAlignMap[col], fAlign); + } + + using GtkInstanceWidget::set_sensitive; + using GtkInstanceWidget::get_sensitive; + + virtual void set_sensitive(int pos, bool bSensitive, int col) override + { + if (col == -1) + { + for (const auto& elem : m_aSensitiveMap) + set(pos, elem.second, bSensitive); + } + else + { + col = to_internal_model(col); + set(pos, m_aSensitiveMap[col], bSensitive); + } + } + + virtual bool get_sensitive(int pos, int col) const override + { + col = to_internal_model(col); + const auto iter = m_aSensitiveMap.find(col); + assert(iter != m_aSensitiveMap.end()); + return get_bool(pos, iter->second); + } + + virtual void set_sensitive(const weld::TreeIter& rIter, bool bSensitive, int col) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + if (col == -1) + { + for (const auto& elem : m_aSensitiveMap) + set(rGtkIter.iter, elem.second, bSensitive); + } + else + { + col = to_internal_model(col); + set(rGtkIter.iter, m_aSensitiveMap[col], bSensitive); + } + } + + virtual bool get_sensitive(const weld::TreeIter& rIter, int col) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + col = to_internal_model(col); + const auto iter = m_aSensitiveMap.find(col); + assert(iter != m_aSensitiveMap.end()); + return get_bool(rGtkIter.iter, iter->second); + } + + void set_image(const GtkTreeIter& iter, int col, GdkPixbuf* pixbuf) + { + if (col == -1) + col = m_nExpanderImageCol; + else + col = to_internal_model(col); + m_Setter(m_pTreeModel, const_cast(&iter), col, pixbuf, -1); + if (pixbuf) + g_object_unref(pixbuf); + } + + void set_image(int pos, GdkPixbuf* pixbuf, int col) + { + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + { + set_image(iter, col, pixbuf); + } + } + + virtual void set_image(int pos, const css::uno::Reference& rImage, int col) override + { + set_image(pos, getPixbuf(rImage), col); + } + + virtual void set_image(int pos, const OUString& rImage, int col) override + { + set_image(pos, getPixbuf(rImage), col); + } + + virtual void set_image(int pos, VirtualDevice& rImage, int col) override + { + set_image(pos, getPixbuf(rImage), col); + } + + virtual void set_image(const weld::TreeIter& rIter, const css::uno::Reference& rImage, int col) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + set_image(rGtkIter.iter, col, getPixbuf(rImage)); + } + + virtual void set_image(const weld::TreeIter& rIter, const OUString& rImage, int col) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + set_image(rGtkIter.iter, col, getPixbuf(rImage)); + } + + virtual void set_image(const weld::TreeIter& rIter, VirtualDevice& rImage, int col) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + set_image(rGtkIter.iter, col, getPixbuf(rImage)); + } + + virtual OUString get_id(int pos) const override + { + return get(pos, m_nIdCol); + } + + virtual void set_id(int pos, const OUString& rId) override + { + return set(pos, m_nIdCol, rId); + } + + virtual int get_iter_index_in_parent(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, const_cast(&rGtkIter.iter)); + + gint depth; + gint* indices = gtk_tree_path_get_indices_with_depth(path, &depth); + int nRet = indices[depth-1]; + + gtk_tree_path_free(path); + + return nRet; + } + + virtual int iter_compare(const weld::TreeIter& a, const weld::TreeIter& b) const override + { + const GtkInstanceTreeIter& rGtkIterA = static_cast(a); + const GtkInstanceTreeIter& rGtkIterB = static_cast(b); + + GtkTreePath* pathA = gtk_tree_model_get_path(m_pTreeModel, const_cast(&rGtkIterA.iter)); + GtkTreePath* pathB = gtk_tree_model_get_path(m_pTreeModel, const_cast(&rGtkIterB.iter)); + + int nRet = gtk_tree_path_compare(pathA, pathB); + + gtk_tree_path_free(pathB); + gtk_tree_path_free(pathA); + + return nRet; + } + + // by copy and delete of old copy + void move_subtree(GtkTreeIter& rFromIter, GtkTreeIter* pGtkParentIter, int nIndexInNewParent) + { + int nCols = gtk_tree_model_get_n_columns(m_pTreeModel); + GValue value; + + GtkTreeIter toiter; + m_Insert(m_pTreeModel, &toiter, pGtkParentIter, nIndexInNewParent); + + for (int i = 0; i < nCols; ++i) + { + memset(&value, 0, sizeof(GValue)); + gtk_tree_model_get_value(m_pTreeModel, &rFromIter, i, &value); + m_SetValue(m_pTreeModel, &toiter, i, &value); + g_value_unset(&value); + } + + GtkTreeIter tmpfromiter; + if (gtk_tree_model_iter_children(m_pTreeModel, &tmpfromiter, &rFromIter)) + { + int j = 0; + do + { + move_subtree(tmpfromiter, &toiter, j++); + } while (gtk_tree_model_iter_next(m_pTreeModel, &tmpfromiter)); + } + + m_Remove(m_pTreeModel, &rFromIter); + } + + virtual void move_subtree(weld::TreeIter& rNode, const weld::TreeIter* pNewParent, int nIndexInNewParent) override + { + GtkInstanceTreeIter& rGtkIter = static_cast(rNode); + const GtkInstanceTreeIter* pGtkParentIter = static_cast(pNewParent); + move_subtree(rGtkIter.iter, pGtkParentIter ? const_cast(&pGtkParentIter->iter) : nullptr, nIndexInNewParent); + } + + virtual int get_selected_index() const override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't request selection when frozen"); + int nRet = -1; + GtkTreeSelection *selection = gtk_tree_view_get_selection(m_pTreeView); + if (gtk_tree_selection_get_mode(selection) != GTK_SELECTION_MULTIPLE) + { + GtkTreeIter iter; + GtkTreeModel* pModel; + if (gtk_tree_selection_get_selected(gtk_tree_view_get_selection(m_pTreeView), &pModel, &iter)) + { + GtkTreePath* path = gtk_tree_model_get_path(pModel, &iter); + + gint depth; + gint* indices = gtk_tree_path_get_indices_with_depth(path, &depth); + nRet = indices[depth-1]; + + gtk_tree_path_free(path); + } + } + else + { + auto vec = get_selected_rows(); + return vec.empty() ? -1 : vec[0]; + } + return nRet; + } + + bool get_selected_iterator(GtkTreeIter* pIter) const + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't request selection when frozen"); + bool bRet = false; + GtkTreeSelection *selection = gtk_tree_view_get_selection(m_pTreeView); + if (gtk_tree_selection_get_mode(selection) != GTK_SELECTION_MULTIPLE) + bRet = gtk_tree_selection_get_selected(gtk_tree_view_get_selection(m_pTreeView), nullptr, pIter); + else + { + GtkTreeModel* pModel; + GList* pList = gtk_tree_selection_get_selected_rows(gtk_tree_view_get_selection(m_pTreeView), &pModel); + for (GList* pItem = g_list_first(pList); pItem; pItem = g_list_next(pItem)) + { + if (pIter) + { + GtkTreePath* path = static_cast(pItem->data); + gtk_tree_model_get_iter(pModel, pIter, path); + } + bRet = true; + break; + } + g_list_free_full(pList, reinterpret_cast(gtk_tree_path_free)); + } + return bRet; + } + + virtual OUString get_selected_text() const override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't request selection when frozen"); + GtkTreeIter iter; + if (get_selected_iterator(&iter)) + return get(iter, m_nTextCol); + return OUString(); + } + + virtual OUString get_selected_id() const override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't request selection when frozen"); + GtkTreeIter iter; + if (get_selected_iterator(&iter)) + return get(iter, m_nIdCol); + return OUString(); + } + + virtual std::unique_ptr make_iterator(const weld::TreeIter* pOrig) const override + { + return std::unique_ptr(new GtkInstanceTreeIter(static_cast(pOrig))); + } + + virtual void copy_iterator(const weld::TreeIter& rSource, weld::TreeIter& rDest) const override + { + const GtkInstanceTreeIter& rGtkSource(static_cast(rSource)); + GtkInstanceTreeIter& rGtkDest(static_cast(rDest)); + rGtkDest.iter = rGtkSource.iter; + } + + virtual bool get_selected(weld::TreeIter* pIter) const override + { + GtkInstanceTreeIter* pGtkIter = static_cast(pIter); + return get_selected_iterator(pGtkIter ? &pGtkIter->iter : nullptr); + } + + virtual bool get_cursor(weld::TreeIter* pIter) const override + { + GtkInstanceTreeIter* pGtkIter = static_cast(pIter); + GtkTreePath* path; + gtk_tree_view_get_cursor(m_pTreeView, &path, nullptr); + if (pGtkIter && path) + { + gtk_tree_model_get_iter(m_pTreeModel, &pGtkIter->iter, path); + } + if (!path) + return false; + gtk_tree_path_free(path); + return true; + } + + virtual int get_cursor_index() const override + { + int nRet = -1; + + GtkTreePath* path; + gtk_tree_view_get_cursor(m_pTreeView, &path, nullptr); + if (path) + { + gint depth; + gint* indices = gtk_tree_path_get_indices_with_depth(path, &depth); + nRet = indices[depth-1]; + gtk_tree_path_free(path); + } + + return nRet; + } + + virtual void set_cursor(const weld::TreeIter& rIter) override + { + disable_notify_events(); + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + GtkTreeIter Iter; + if (gtk_tree_model_iter_parent(m_pTreeModel, &Iter, const_cast(&rGtkIter.iter))) + { + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, &Iter); + if (!gtk_tree_view_row_expanded(m_pTreeView, path)) + gtk_tree_view_expand_to_path(m_pTreeView, path); + gtk_tree_path_free(path); + } + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, const_cast(&rGtkIter.iter)); + gtk_tree_view_scroll_to_cell(m_pTreeView, path, nullptr, false, 0, 0); + gtk_tree_view_set_cursor(m_pTreeView, path, nullptr, false); + gtk_tree_path_free(path); + enable_notify_events(); + } + + virtual bool get_iter_first(weld::TreeIter& rIter) const override + { + GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + return gtk_tree_model_get_iter_first(m_pTreeModel, &rGtkIter.iter); + } + + virtual bool iter_next_sibling(weld::TreeIter& rIter) const override + { + GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + return gtk_tree_model_iter_next(m_pTreeModel, &rGtkIter.iter); + } + + virtual bool iter_previous_sibling(weld::TreeIter& rIter) const override + { + GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + return gtk_tree_model_iter_previous(m_pTreeModel, &rGtkIter.iter); + } + + virtual bool iter_next(weld::TreeIter& rIter) const override + { + return iter_next(rIter, false); + } + + virtual bool iter_previous(weld::TreeIter& rIter) const override + { + bool ret = false; + GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + GtkTreeIter iter = rGtkIter.iter; + GtkTreeIter tmp = iter; + if (gtk_tree_model_iter_previous(m_pTreeModel, &tmp)) + { + // Move down level(s) until we find the level where the last node exists. + int nChildren = gtk_tree_model_iter_n_children(m_pTreeModel, &tmp); + if (!nChildren) + rGtkIter.iter = tmp; + else + last_child(m_pTreeModel, &rGtkIter.iter, &tmp, nChildren); + ret = true; + } + else + { + // Move up level + if (gtk_tree_model_iter_parent(m_pTreeModel, &tmp, &iter)) + { + rGtkIter.iter = tmp; + ret = true; + } + } + + if (ret) + { + //on-demand dummy entry doesn't count + if (get_text(rGtkIter, -1) == "") + return iter_previous(rGtkIter); + return true; + } + + return false; + } + + virtual bool iter_children(weld::TreeIter& rIter) const override + { + GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + GtkTreeIter tmp; + bool ret = gtk_tree_model_iter_children(m_pTreeModel, &tmp, &rGtkIter.iter); + rGtkIter.iter = tmp; + if (ret) + { + //on-demand dummy entry doesn't count + return get_text(rGtkIter, -1) != ""; + } + return ret; + } + + virtual bool iter_parent(weld::TreeIter& rIter) const override + { + GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + GtkTreeIter tmp; + bool ret = gtk_tree_model_iter_parent(m_pTreeModel, &tmp, &rGtkIter.iter); + rGtkIter.iter = tmp; + return ret; + } + + virtual void remove(const weld::TreeIter& rIter) override + { + disable_notify_events(); + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + m_Remove(m_pTreeModel, const_cast(&rGtkIter.iter)); + enable_notify_events(); + } + + virtual void remove_selection() override + { + disable_notify_events(); + + std::vector aIters; + GtkTreeModel* pModel; + GList* pList = gtk_tree_selection_get_selected_rows(gtk_tree_view_get_selection(m_pTreeView), &pModel); + for (GList* pItem = g_list_first(pList); pItem; pItem = g_list_next(pItem)) + { + GtkTreePath* path = static_cast(pItem->data); + aIters.emplace_back(); + gtk_tree_model_get_iter(pModel, &aIters.back(), path); + } + g_list_free_full(pList, reinterpret_cast(gtk_tree_path_free)); + + for (auto& iter : aIters) + m_Remove(m_pTreeModel, &iter); + + enable_notify_events(); + } + + virtual void select(const weld::TreeIter& rIter) override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + gtk_tree_selection_select_iter(gtk_tree_view_get_selection(m_pTreeView), const_cast(&rGtkIter.iter)); + enable_notify_events(); + } + + virtual void scroll_to_row(const weld::TreeIter& rIter) override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, const_cast(&rGtkIter.iter)); + gtk_tree_view_expand_to_path(m_pTreeView, path); + gtk_tree_view_scroll_to_cell(m_pTreeView, path, nullptr, true, 0, 0); + gtk_tree_path_free(path); + enable_notify_events(); + } + + virtual void unselect(const weld::TreeIter& rIter) override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + gtk_tree_selection_unselect_iter(gtk_tree_view_get_selection(m_pTreeView), const_cast(&rGtkIter.iter)); + enable_notify_events(); + } + + virtual int get_iter_depth(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, const_cast(&rGtkIter.iter)); + int ret = gtk_tree_path_get_depth(path) - 1; + gtk_tree_path_free(path); + return ret; + } + + virtual bool iter_has_child(const weld::TreeIter& rIter) const override + { + GtkInstanceTreeIter aTempCopy(static_cast(&rIter)); + return iter_children(aTempCopy); + } + + virtual bool get_row_expanded(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, const_cast(&rGtkIter.iter)); + bool ret = gtk_tree_view_row_expanded(m_pTreeView, path); + gtk_tree_path_free(path); + return ret; + } + + virtual bool get_children_on_demand(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + GtkInstanceTreeIter aIter(&rGtkIter); + return child_is_placeholder(aIter); + } + + virtual void set_children_on_demand(const weld::TreeIter& rIter, bool bChildrenOnDemand) override + { + disable_notify_events(); + + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + GtkInstanceTreeIter aPlaceHolderIter(&rGtkIter); + + bool bPlaceHolder = child_is_placeholder(aPlaceHolderIter); + + if (bChildrenOnDemand && !bPlaceHolder) + { + GtkTreeIter subiter; + OUString sDummy(""); + insert_row(subiter, &rGtkIter.iter, -1, nullptr, &sDummy, nullptr, nullptr); + } + else if (!bChildrenOnDemand && bPlaceHolder) + remove(aPlaceHolderIter); + + enable_notify_events(); + } + + virtual void expand_row(const weld::TreeIter& rIter) override + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't expand when frozen"); + + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, const_cast(&rGtkIter.iter)); + if (!gtk_tree_view_row_expanded(m_pTreeView, path)) + gtk_tree_view_expand_to_path(m_pTreeView, path); + gtk_tree_path_free(path); + } + + virtual void collapse_row(const weld::TreeIter& rIter) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, const_cast(&rGtkIter.iter)); + if (gtk_tree_view_row_expanded(m_pTreeView, path)) + gtk_tree_view_collapse_row(m_pTreeView, path); + gtk_tree_path_free(path); + } + + virtual OUString get_text(const weld::TreeIter& rIter, int col) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + if (col == -1) + col = m_nTextCol; + else + col = to_internal_model(col); + return get(rGtkIter.iter, col); + } + + virtual void set_text(const weld::TreeIter& rIter, const OUString& rText, int col) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + if (col == -1) + col = m_nTextCol; + else + col = to_internal_model(col); + set(rGtkIter.iter, col, rText); + } + + virtual OUString get_id(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + return get(rGtkIter.iter, m_nIdCol); + } + + virtual void set_id(const weld::TreeIter& rIter, const OUString& rId) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + set(rGtkIter.iter, m_nIdCol, rId); + } + + virtual void freeze() override + { + disable_notify_events(); + bool bIsFirstFreeze = IsFirstFreeze(); + GtkInstanceWidget::freeze(); + if (bIsFirstFreeze) + { + g_object_ref(m_pTreeModel); + gtk_tree_view_set_model(m_pTreeView, nullptr); + g_object_freeze_notify(G_OBJECT(m_pTreeModel)); + if (m_xSorter) + { + int nSortColumn; + GtkSortType eSortType; + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_get_sort_column_id(pSortable, &nSortColumn, &eSortType); + gtk_tree_sortable_set_sort_column_id(pSortable, GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, eSortType); + + m_aSavedSortColumns.push_back(nSortColumn); + m_aSavedSortTypes.push_back(eSortType); + } + } + enable_notify_events(); + } + + virtual void thaw() override + { + disable_notify_events(); + if (IsLastThaw()) + { + if (m_xSorter) + { + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_set_sort_column_id(pSortable, m_aSavedSortColumns.back(), m_aSavedSortTypes.back()); + m_aSavedSortTypes.pop_back(); + m_aSavedSortColumns.pop_back(); + } + g_object_thaw_notify(G_OBJECT(m_pTreeModel)); + gtk_tree_view_set_model(m_pTreeView, GTK_TREE_MODEL(m_pTreeModel)); + g_object_unref(m_pTreeModel); + } + GtkInstanceWidget::thaw(); + enable_notify_events(); + } + + virtual int get_height_rows(int nRows) const override + { + return ::get_height_rows(m_pTreeView, m_pColumns, nRows); + } + + virtual Size get_size_request() const override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + { + return Size(gtk_scrolled_window_get_min_content_width(GTK_SCROLLED_WINDOW(pParent)), + gtk_scrolled_window_get_min_content_height(GTK_SCROLLED_WINDOW(pParent))); + } + int nWidth, nHeight; + gtk_widget_get_size_request(m_pWidget, &nWidth, &nHeight); + return Size(nWidth, nHeight); + } + + virtual Size get_preferred_size() const override + { + Size aRet(-1, -1); + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + { + aRet = Size(gtk_scrolled_window_get_min_content_width(GTK_SCROLLED_WINDOW(pParent)), + gtk_scrolled_window_get_min_content_height(GTK_SCROLLED_WINDOW(pParent))); + } + GtkRequisition size; +#if !GTK_CHECK_VERSION(4, 0, 0) + // sometimes gtk gives a bad outcome for gtk_widget_get_preferred_size if GtkTreeView's + // do_validate_rows hasn't been run before querying the preferred size, if we call + // gtk_widget_get_preferred_width first, we can guarantee do_validate_rows gets called + gtk_widget_get_preferred_width(m_pWidget, nullptr, &size.width); +#endif + gtk_widget_get_preferred_size(m_pWidget, nullptr, &size); + if (aRet.Width() == -1) + aRet.setWidth(size.width); + if (aRet.Height() == -1) + aRet.setHeight(size.height); + return aRet; + } + + virtual void show() override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + gtk_widget_show(pParent); + gtk_widget_show(m_pWidget); + } + + virtual void hide() override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + gtk_widget_hide(pParent); + gtk_widget_hide(m_pWidget); + } + + virtual void enable_drag_source(rtl::Reference& rHelper, sal_uInt8 eDNDConstants) override + { + do_enable_drag_source(rHelper, eDNDConstants); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + virtual void drag_source_set(const std::vector& rGtkTargets, GdkDragAction eDragAction) override + { + if (rGtkTargets.empty() && !eDragAction) + gtk_tree_view_unset_rows_drag_source(m_pTreeView); + else + gtk_tree_view_enable_model_drag_source(m_pTreeView, GDK_BUTTON1_MASK, rGtkTargets.data(), rGtkTargets.size(), eDragAction); + } +#endif + + virtual void set_selection_mode(SelectionMode eMode) override + { + disable_notify_events(); + gtk_tree_selection_set_mode(gtk_tree_view_get_selection(m_pTreeView), VclToGtk(eMode)); + enable_notify_events(); + } + + virtual int count_selected_rows() const override + { + return gtk_tree_selection_count_selected_rows(gtk_tree_view_get_selection(m_pTreeView)); + } + + int starts_with(const OUString& rStr, int nStartRow, bool bCaseSensitive) + { + return ::starts_with(m_pTreeModel, rStr, m_nTextCol, nStartRow, bCaseSensitive); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(gtk_tree_view_get_selection(m_pTreeView), m_nChangedSignalId); + g_signal_handler_block(m_pTreeView, m_nRowActivatedSignalId); + + g_signal_handler_block(m_pTreeModel, m_nRowDeletedSignalId); + g_signal_handler_block(m_pTreeModel, m_nRowInsertedSignalId); + + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + + g_signal_handler_unblock(m_pTreeModel, m_nRowDeletedSignalId); + g_signal_handler_unblock(m_pTreeModel, m_nRowInsertedSignalId); + + g_signal_handler_unblock(m_pTreeView, m_nRowActivatedSignalId); + g_signal_handler_unblock(gtk_tree_view_get_selection(m_pTreeView), m_nChangedSignalId); + } + + virtual void connect_popup_menu(const Link& rLink) override + { + ensureButtonPressSignal(); + weld::TreeView::connect_popup_menu(rLink); + } + + virtual bool get_dest_row_at_pos(const Point &rPos, weld::TreeIter* pResult, bool bDnDMode, bool bAutoScroll) override + { + if (rPos.X() < 0 || rPos.Y() < 0) + { + // short-circuit to avoid "gtk_tree_view_get_dest_row_at_pos: assertion 'drag_x >= 0'" g_assert + return false; + } + + const bool bAsTree = gtk_tree_view_get_enable_tree_lines(m_pTreeView); + + // to keep it simple we'll default to always drop before the current row + // except for the special edge cases + GtkTreeViewDropPosition pos = bAsTree ? GTK_TREE_VIEW_DROP_INTO_OR_BEFORE : GTK_TREE_VIEW_DROP_BEFORE; + + // unhighlight current highlighted row + gtk_tree_view_set_drag_dest_row(m_pTreeView, nullptr, pos); + + if (m_bWorkAroundBadDragRegion) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_unset_state_flags(GTK_WIDGET(m_pTreeView), GTK_STATE_FLAG_DROP_ACTIVE); +#else + gtk_drag_unhighlight(GTK_WIDGET(m_pTreeView)); +#endif + } + + GtkTreePath *path = nullptr; + GtkTreeViewDropPosition gtkpos = bAsTree ? GTK_TREE_VIEW_DROP_INTO_OR_BEFORE : GTK_TREE_VIEW_DROP_BEFORE; + bool ret = gtk_tree_view_get_dest_row_at_pos(m_pTreeView, rPos.X(), rPos.Y(), + &path, >kpos); + + // find the last entry in the model for comparison + GtkTreePath *lastpath = get_path_of_last_entry(m_pTreeModel); + + if (!ret) + { + // empty space, draw an indicator at the last entry + assert(!path); + path = gtk_tree_path_copy(lastpath); + pos = GTK_TREE_VIEW_DROP_AFTER; + } + else if (bDnDMode && gtk_tree_path_compare(path, lastpath) == 0) + { + // if we're on the last entry, see if gtk thinks + // the drop should be before or after it, and if + // its after, treat it like a drop into empty + // space, i.e. append it + if (gtkpos == GTK_TREE_VIEW_DROP_AFTER || + gtkpos == GTK_TREE_VIEW_DROP_INTO_OR_AFTER) + { + ret = false; + pos = bAsTree ? gtkpos : GTK_TREE_VIEW_DROP_AFTER; + } + } + + if (ret && pResult) + { + GtkInstanceTreeIter& rGtkIter = static_cast(*pResult); + gtk_tree_model_get_iter(m_pTreeModel, &rGtkIter.iter, path); + } + + if (m_bInDrag && bDnDMode) + { + // highlight the row + gtk_tree_view_set_drag_dest_row(m_pTreeView, path, pos); + } + + assert(path); + gtk_tree_path_free(path); + gtk_tree_path_free(lastpath); + + if (bAutoScroll) + { + // auto scroll if we're close to the edges + GtkAdjustment* pVAdjustment = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(m_pTreeView)); + double fStep = gtk_adjustment_get_step_increment(pVAdjustment); + if (rPos.Y() < fStep) + { + double fValue = gtk_adjustment_get_value(pVAdjustment) - fStep; + if (fValue < 0) + fValue = 0.0; + gtk_adjustment_set_value(pVAdjustment, fValue); + } + else + { + GdkRectangle aRect; + gtk_tree_view_get_visible_rect(m_pTreeView, &aRect); + if (rPos.Y() > aRect.height - fStep) + { + double fValue = gtk_adjustment_get_value(pVAdjustment) + fStep; + double fMax = gtk_adjustment_get_upper(pVAdjustment); + if (fValue > fMax) + fValue = fMax; + gtk_adjustment_set_value(pVAdjustment, fValue); + } + } + } + + return ret; + } + + virtual void unset_drag_dest_row() override + { + gtk_tree_view_set_drag_dest_row(m_pTreeView, nullptr, GTK_TREE_VIEW_DROP_BEFORE); + } + + virtual tools::Rectangle get_row_area(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + GtkTreePath* pPath = gtk_tree_model_get_path(m_pTreeModel, const_cast(&rGtkIter.iter)); + tools::Rectangle aRet = ::get_row_area(m_pTreeView, m_pColumns, pPath); + gtk_tree_path_free(pPath); + return aRet; + } + + virtual void start_editing(const weld::TreeIter& rIter) override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + GtkTreePath* path = gtk_tree_model_get_path(m_pTreeModel, const_cast(&rGtkIter.iter)); + + GtkTreeViewColumn* pColumn = nullptr; + + for (GList* pEntry = g_list_first(m_pColumns); pEntry; pEntry = g_list_next(pEntry)) + { + GtkTreeViewColumn* pTestColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + + // see if this column is editable + gboolean is_editable(false); + GList *pRenderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(pTestColumn)); + for (GList* pRenderer = g_list_first(pRenderers); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + if (GTK_IS_CELL_RENDERER_TEXT(pCellRenderer)) + { + g_object_get(pCellRenderer, "editable", &is_editable, nullptr); + if (is_editable) + { + pColumn = pTestColumn; + break; + } + } + } + g_list_free(pRenderers); + + if (is_editable) + break; + } + + // if nothing explicit editable, allow editing of cells which are not + // usually editable, so we can have double click do its usual + // row-activate but if we explicitly want to edit (remote files dialog) + // we can still do that + if (!pColumn) + { + pColumn = GTK_TREE_VIEW_COLUMN(g_list_nth_data(m_pColumns, m_nTextView)); + assert(pColumn && "wrong column"); + + GList *pRenderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(pColumn)); + for (GList* pRenderer = g_list_first(pRenderers); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + if (GTK_IS_CELL_RENDERER_TEXT(pCellRenderer)) + { + g_object_set(pCellRenderer, "editable", true, "editable-set", true, nullptr); + g_object_set_data(G_OBJECT(pCellRenderer), "g-lo-RestoreNonEditable", reinterpret_cast(true)); + break; + } + } + g_list_free(pRenderers); + } + + gtk_tree_view_scroll_to_cell(m_pTreeView, path, pColumn, false, 0, 0); + gtk_tree_view_set_cursor(m_pTreeView, path, pColumn, true); + + gtk_tree_path_free(path); + } + + virtual void end_editing() override + { + GtkTreeViewColumn *focus_column = nullptr; + gtk_tree_view_get_cursor(m_pTreeView, nullptr, &focus_column); + if (focus_column) + gtk_cell_area_stop_editing(gtk_cell_layout_get_area(GTK_CELL_LAYOUT(focus_column)), true); + } + + virtual TreeView* get_drag_source() const override + { + return g_DragSource; + } + + virtual bool do_signal_drag_begin(bool& rUnsetDragIcon) override + { + if (m_aDragBeginHdl.Call(rUnsetDragIcon)) + return true; + g_DragSource = this; + return false; + } + +#if GTK_CHECK_VERSION(4, 0, 0) + virtual void drag_set_icon(GtkDragSource*) override + { + } +#else + virtual void drag_set_icon(GdkDragContext* context) override + { + GtkTreeSelection *selection = gtk_tree_view_get_selection(m_pTreeView); + if (gtk_tree_selection_get_mode(selection) == GTK_SELECTION_MULTIPLE) + { + int nWidth = 0; + int nHeight = 0; + + GList* pList = gtk_tree_selection_get_selected_rows(gtk_tree_view_get_selection(m_pTreeView), nullptr); + std::vector surfaces; + std::vector heights; + for (GList* pItem = g_list_first(pList); pItem; pItem = g_list_next(pItem)) + { + GtkTreePath* pPath = static_cast(pItem->data); + + surfaces.push_back(gtk_tree_view_create_row_drag_icon(m_pTreeView, pPath)); + + double x1, x2, y1, y2; + cairo_t* cr = cairo_create(surfaces.back()); + cairo_clip_extents(cr, &x1, &y1, &x2, &y2); + cairo_destroy(cr); + + heights.push_back(y2 - y1); + + nWidth = std::max(nWidth, static_cast(x2 - x1)); + nHeight += heights.back(); + } + g_list_free_full(pList, reinterpret_cast(gtk_tree_path_free)); + + // if it's just one, then don't do anything and leave the default dnd icon as-is + if (surfaces.size() > 1) + { + cairo_surface_t* target = cairo_surface_create_similar(surfaces[0], + cairo_surface_get_content(surfaces[0]), + nWidth, + nHeight); + + cairo_t* cr = cairo_create(target); + + double y_pos = 0; + for (size_t i = 0; i < surfaces.size(); ++i) + { + cairo_set_source_surface(cr, surfaces[i], 2, y_pos + 2); + cairo_rectangle(cr, 0, y_pos, nWidth, heights[i]); + cairo_fill(cr); + y_pos += heights[i]; + } + + cairo_destroy(cr); + + double fXScale, fYScale; + dl_cairo_surface_get_device_scale(target, &fXScale, &fYScale); + cairo_surface_set_device_offset(target, + - m_nPressStartX * fXScale, + 0); + + gtk_drag_set_icon_surface(context, target); + cairo_surface_destroy(target); + } + + for (auto surface : surfaces) + cairo_surface_destroy(surface); + } + } +#endif + + virtual void do_signal_drag_end() override + { + g_DragSource = nullptr; + } + + // Under gtk 3.24.8 dragging into the TreeView is not highlighting + // entire TreeView widget, just the rectangle which has no entries + // in it, so as a workaround highlight the parent container + // on drag start, and undo it on drag end, and trigger removal + // of the treeview's highlight effort + virtual void drag_started() override + { + m_bInDrag = true; + GtkWidget* pWidget = GTK_WIDGET(m_pTreeView); + GtkWidget* pParent = gtk_widget_get_parent(pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_unset_state_flags(pWidget, GTK_STATE_FLAG_DROP_ACTIVE); + gtk_widget_set_state_flags(pParent, GTK_STATE_FLAG_DROP_ACTIVE, false); +#else + gtk_drag_unhighlight(pWidget); + gtk_drag_highlight(pParent); +#endif + m_bWorkAroundBadDragRegion = true; + } + } + + virtual void drag_ended() override + { + m_bInDrag = false; + if (m_bWorkAroundBadDragRegion) + { + GtkWidget* pWidget = GTK_WIDGET(m_pTreeView); + GtkWidget* pParent = gtk_widget_get_parent(pWidget); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_unset_state_flags(pParent, GTK_STATE_FLAG_DROP_ACTIVE); +#else + gtk_drag_unhighlight(pParent); +#endif + m_bWorkAroundBadDragRegion = false; + } + // unhighlight the row + gtk_tree_view_set_drag_dest_row(m_pTreeView, nullptr, GTK_TREE_VIEW_DROP_BEFORE); + } + + virtual int vadjustment_get_value() const override + { + if (m_nPendingVAdjustment != -1) + return m_nPendingVAdjustment; + return gtk_adjustment_get_value(m_pVAdjustment); + } + + virtual void vadjustment_set_value(int value) override + { + disable_notify_events(); + + /* This rube goldberg device is to remove flicker from setting the + scroll position of a GtkTreeView directly after clearing it and + filling it. As a specific example the writer navigator with ~100 + tables, scroll to the end, right click on an entry near the end + and rename it, the tree is cleared and refilled and an attempt + made to set the scroll position of the freshly refilled tree to + the same point as before the clear. + */ + + // This forces the tree to recalculate now its preferred size + // after being cleared + GtkRequisition size; + gtk_widget_get_preferred_size(GTK_WIDGET(m_pTreeView), nullptr, &size); + + m_nPendingVAdjustment = value; + + // The value set here just has to be different to the final value + // set later so that isn't a no-op + gtk_adjustment_set_value(m_pVAdjustment, value - 0.0001); + + // This will set the desired m_nPendingVAdjustment value right + // before the tree gets drawn + gtk_widget_add_tick_callback(GTK_WIDGET(m_pTreeView), setAdjustmentCallback, this, nullptr); + + enable_notify_events(); + } + + void call_signal_custom_render(VirtualDevice& rOutput, const tools::Rectangle& rRect, bool bSelected, const OUString& rId) + { + signal_custom_render(rOutput, rRect, bSelected, rId); + } + + Size call_signal_custom_get_size(VirtualDevice& rOutput, const OUString& rId) + { + return signal_custom_get_size(rOutput, rId); + } + + virtual void set_show_expanders(bool bShow) override + { + gtk_tree_view_set_show_expanders(m_pTreeView, bShow); + } + + virtual bool changed_by_hover() const override + { + return m_bChangedByMouse; + } + + virtual ~GtkInstanceTreeView() override + { + if (m_pChangeEvent) + Application::RemoveUserEvent(m_pChangeEvent); + if (m_nQueryTooltipSignalId) + g_signal_handler_disconnect(m_pTreeView, m_nQueryTooltipSignalId); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pTreeView, m_nCrossingSignalid); + g_signal_handler_disconnect(m_pTreeView, m_nKeyPressSignalId); + g_signal_handler_disconnect(m_pTreeView, m_nPopupMenuSignalId); +#endif + g_signal_handler_disconnect(m_pTreeModel, m_nRowDeletedSignalId); + g_signal_handler_disconnect(m_pTreeModel, m_nRowInsertedSignalId); + + if (m_nVAdjustmentChangedSignalId) + { + GtkAdjustment* pVAdjustment = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(m_pTreeView)); + g_signal_handler_disconnect(pVAdjustment, m_nVAdjustmentChangedSignalId); + } + + g_signal_handler_disconnect(m_pTreeView, m_nTestCollapseRowSignalId); + g_signal_handler_disconnect(m_pTreeView, m_nTestExpandRowSignalId); + g_signal_handler_disconnect(m_pTreeView, m_nRowActivatedSignalId); + g_signal_handler_disconnect(gtk_tree_view_get_selection(m_pTreeView), m_nChangedSignalId); + + if (g_DragSource == this) + g_DragSource = nullptr; + + GValue value = G_VALUE_INIT; + g_value_init(&value, G_TYPE_POINTER); + g_value_set_pointer(&value, static_cast(nullptr)); + + for (GList* pEntry = g_list_last(m_pColumns); pEntry; pEntry = g_list_previous(pEntry)) + { + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pEntry->data); + g_signal_handler_disconnect(pColumn, m_aColumnSignalIds.back()); + m_aColumnSignalIds.pop_back(); + + // unset "instance" to avoid dangling "instance" points in any CustomCellRenderers + GList *pRenderers = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(pColumn)); + for (GList* pRenderer = g_list_first(pRenderers); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + if (!CUSTOM_IS_CELL_RENDERER(pCellRenderer)) + continue; + g_object_set_property(G_OBJECT(pCellRenderer), "instance", &value); + } + g_list_free(pRenderers); + } + g_list_free(m_pColumns); + } +}; + +} + +IMPL_LINK_NOARG(GtkInstanceTreeView, async_signal_changed, void*, void) +{ + m_pChangeEvent = nullptr; + signal_changed(); + m_bChangedByMouse = false; +} + +IMPL_LINK_NOARG(GtkInstanceTreeView, async_stop_cell_editing, void*, void) +{ + end_editing(); +} + +namespace { + +class GtkInstanceIconView : public GtkInstanceWidget, public virtual weld::IconView +{ +private: + GtkIconView* m_pIconView; + GtkTreeStore* m_pTreeStore; + gint m_nTextCol; + gint m_nImageCol; + gint m_nIdCol; + gulong m_nSelectionChangedSignalId; + gulong m_nItemActivatedSignalId; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong m_nPopupMenu; +#endif + gulong m_nQueryTooltipSignalId = 0; + ImplSVEvent* m_pSelectionChangeEvent; + + DECL_LINK(async_signal_selection_changed, void*, void); + + bool signal_command(const CommandEvent& rCEvt) + { + return m_aCommandHdl.Call(rCEvt); + } + + virtual bool signal_popup_menu(const CommandEvent& rCEvt) override + { + return signal_command(rCEvt); + } + + void launch_signal_selection_changed() + { + //tdf#117991 selection change is sent before the focus change, and focus change + //is what will cause a spinbutton that currently has the focus to set its contents + //as the spin button value. So any LibreOffice callbacks on + //signal-change would happen before the spinbutton value-change occurs. + //To avoid this, send the signal-change to LibreOffice to occur after focus-change + //has been processed + if (m_pSelectionChangeEvent) + Application::RemoveUserEvent(m_pSelectionChangeEvent); + m_pSelectionChangeEvent = Application::PostUserEvent(LINK(this, GtkInstanceIconView, async_signal_selection_changed)); + } + + static void signalSelectionChanged(GtkIconView*, gpointer widget) + { + GtkInstanceIconView* pThis = static_cast(widget); + pThis->launch_signal_selection_changed(); + } + + void handle_item_activated() + { + if (signal_item_activated()) + return; + } + + static void signalItemActivated(GtkIconView*, GtkTreePath*, gpointer widget) + { + GtkInstanceIconView* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->handle_item_activated(); + } + + static gboolean signalQueryTooltip(GtkWidget* /*pGtkWidget*/, gint x, gint y, + gboolean keyboard_tip, GtkTooltip* tooltip, + gpointer widget) + { + GtkInstanceIconView* pThis = static_cast(widget); + GtkTreeIter iter; + GtkIconView* pIconView = pThis->m_pIconView; + GtkTreeModel* pModel = gtk_icon_view_get_model(pIconView); + GtkTreePath* pPath = nullptr; +#if GTK_CHECK_VERSION(4, 0, 0) + if (!gtk_icon_view_get_tooltip_context(pIconView, x, y, keyboard_tip, &pModel, &pPath, &iter)) + return false; +#else + if (!gtk_icon_view_get_tooltip_context(pIconView, &x, &y, keyboard_tip, &pModel, &pPath, &iter)) + return false; +#endif + OUString aTooltip = pThis->signal_query_tooltip(GtkInstanceTreeIter(iter)); + if (!aTooltip.isEmpty()) + { + gtk_tooltip_set_text(tooltip, OUStringToOString(aTooltip, RTL_TEXTENCODING_UTF8).getStr()); + gtk_icon_view_set_tooltip_item(pIconView, tooltip, pPath); + } + gtk_tree_path_free(pPath); + return !aTooltip.isEmpty(); + } + + /* Set the item's tooltip text as its accessible description as well. */ + void set_item_accessible_description_from_tooltip(GtkTreeIter& iter) + { +#if GTK_CHECK_VERSION(4, 0, 0) + (void)iter; +#else + AtkObject* pAtkObject = gtk_widget_get_accessible(GTK_WIDGET(m_pIconView)); + assert(pAtkObject); + GtkTreePath* pPath = gtk_tree_model_get_path(GTK_TREE_MODEL(m_pTreeStore), &iter); + assert(gtk_tree_path_get_depth(pPath) == 1); + int* indices = gtk_tree_path_get_indices(pPath); + const int nIndex = indices[0]; + assert(nIndex < atk_object_get_n_accessible_children(pAtkObject) + && "item index too high for ItemView's accessible child count"); + + const OUString sTooltipText = signal_query_tooltip(GtkInstanceTreeIter(iter)); + AtkObject* pChild = atk_object_ref_accessible_child(pAtkObject, nIndex); + atk_object_set_description(pChild, + OUStringToOString(sTooltipText, RTL_TEXTENCODING_UTF8).getStr()); + g_object_unref(pChild); +#endif + } + + void insert_item(GtkTreeIter& iter, int pos, const OUString* pId, const OUString* pText, const OUString* pIconName) + { + // m_nTextCol may be -1, so pass it last, to not terminate the sequence before the Id value + gtk_tree_store_insert_with_values(m_pTreeStore, &iter, nullptr, pos, + m_nIdCol, !pId ? nullptr : OUStringToOString(*pId, RTL_TEXTENCODING_UTF8).getStr(), + m_nTextCol, !pText ? nullptr : OUStringToOString(*pText, RTL_TEXTENCODING_UTF8).getStr(), + -1); + if (pIconName) + { + GdkPixbuf* pixbuf = getPixbuf(*pIconName); + gtk_tree_store_set(m_pTreeStore, &iter, m_nImageCol, pixbuf, -1); + if (pixbuf) + g_object_unref(pixbuf); + } + + set_item_accessible_description_from_tooltip(iter); + } + + void insert_item(GtkTreeIter& iter, int pos, const OUString* pId, const OUString* pText, const VirtualDevice* pIcon) + { + // m_nTextCol may be -1, so pass it last, to not terminate the sequence before the Id value + gtk_tree_store_insert_with_values(m_pTreeStore, &iter, nullptr, pos, + m_nIdCol, !pId ? nullptr : OUStringToOString(*pId, RTL_TEXTENCODING_UTF8).getStr(), + m_nTextCol, !pText ? nullptr : OUStringToOString(*pText, RTL_TEXTENCODING_UTF8).getStr(), + -1); + if (pIcon) + { + GdkPixbuf* pixbuf = getPixbuf(*pIcon); + gtk_tree_store_set(m_pTreeStore, &iter, m_nImageCol, pixbuf, -1); + if (pixbuf) + g_object_unref(pixbuf); + } + + set_item_accessible_description_from_tooltip(iter); + } + + OUString get(const GtkTreeIter& iter, int col) const + { + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + gchar* pStr; + gtk_tree_model_get(pModel, const_cast(&iter), col, &pStr, -1); + OUString sRet(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + g_free(pStr); + return sRet; + } + + bool get_selected_iterator(GtkTreeIter* pIter) const + { + assert(gtk_icon_view_get_model(m_pIconView) && "don't request selection when frozen"); + bool bRet = false; + { + GtkTreeModel* pModel = GTK_TREE_MODEL(m_pTreeStore); + GList* pList = gtk_icon_view_get_selected_items(m_pIconView); + for (GList* pItem = g_list_first(pList); pItem; pItem = g_list_next(pItem)) + { + if (pIter) + { + GtkTreePath* path = static_cast(pItem->data); + gtk_tree_model_get_iter(pModel, pIter, path); + } + bRet = true; + break; + } + g_list_free_full(pList, reinterpret_cast(gtk_tree_path_free)); + } + return bRet; + } + +public: + GtkInstanceIconView(GtkIconView* pIconView, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pIconView), pBuilder, bTakeOwnership) + , m_pIconView(pIconView) + , m_pTreeStore(GTK_TREE_STORE(gtk_icon_view_get_model(m_pIconView))) + , m_nTextCol(gtk_icon_view_get_text_column(m_pIconView)) // May be -1 + , m_nImageCol(gtk_icon_view_get_pixbuf_column(m_pIconView)) + , m_nSelectionChangedSignalId(g_signal_connect(pIconView, "selection-changed", + G_CALLBACK(signalSelectionChanged), this)) + , m_nItemActivatedSignalId(g_signal_connect(pIconView, "item-activated", G_CALLBACK(signalItemActivated), this)) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_nPopupMenu(g_signal_connect(pIconView, "popup-menu", G_CALLBACK(signalPopupMenu), this)) +#endif + , m_pSelectionChangeEvent(nullptr) + { + m_nIdCol = std::max(m_nTextCol, m_nImageCol) + 1; + } + + virtual int get_item_width() const override + { + return gtk_icon_view_get_item_width(m_pIconView); + } + + virtual void set_item_width(int width) override + { + gtk_icon_view_set_item_width(m_pIconView, width); + } + + virtual void insert(int pos, const OUString* pText, const OUString* pId, const OUString* pIconName, weld::TreeIter* pRet) override + { + disable_notify_events(); + GtkTreeIter iter; + insert_item(iter, pos, pId, pText, pIconName); + if (pRet) + { + GtkInstanceTreeIter* pGtkRetIter = static_cast(pRet); + pGtkRetIter->iter = iter; + } + enable_notify_events(); + } + + virtual void insert(int pos, const OUString* pText, const OUString* pId, const VirtualDevice* pIcon, weld::TreeIter* pRet) override + { + disable_notify_events(); + GtkTreeIter iter; + insert_item(iter, pos, pId, pText, pIcon); + if (pRet) + { + GtkInstanceTreeIter* pGtkRetIter = static_cast(pRet); + pGtkRetIter->iter = iter; + } + enable_notify_events(); + } + + virtual void insert_separator(int /* pos */, const OUString* /* pId */) override + { + // TODO: can't just copy from GtkInstanceTreeView, since there's + // no IconView analog for gtk_tree_view_get_row_separator_func + } + + virtual void connect_query_tooltip(const Link& rLink) override + { + weld::IconView::connect_query_tooltip(rLink); + m_nQueryTooltipSignalId = g_signal_connect(m_pIconView, "query-tooltip", G_CALLBACK(signalQueryTooltip), this); + gtk_widget_set_has_tooltip(GTK_WIDGET(m_pIconView), true); + } + + virtual void connect_get_property_tree_elem(const Link& /*rLink*/) override + { + //not implemented for the gtk variant + } + + virtual OUString get_selected_id() const override + { + assert(gtk_icon_view_get_model(m_pIconView) && "don't request selection when frozen"); + GtkTreeIter iter; + if (get_selected_iterator(&iter)) + return get(iter, m_nIdCol); + return OUString(); + } + + virtual void clear() override + { + disable_notify_events(); + gtk_tree_store_clear(m_pTreeStore); + enable_notify_events(); + } + + virtual void freeze() override + { + disable_notify_events(); + bool bIsFirstFreeze = IsFirstFreeze(); + GtkInstanceWidget::freeze(); + if (bIsFirstFreeze) + g_object_freeze_notify(G_OBJECT(m_pTreeStore)); + enable_notify_events(); + } + + virtual void thaw() override + { + disable_notify_events(); + if (IsLastThaw()) + g_object_thaw_notify(G_OBJECT(m_pTreeStore)); + GtkInstanceWidget::thaw(); + enable_notify_events(); + } + + virtual Size get_size_request() const override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + { + return Size(gtk_scrolled_window_get_min_content_width(GTK_SCROLLED_WINDOW(pParent)), + gtk_scrolled_window_get_min_content_height(GTK_SCROLLED_WINDOW(pParent))); + } + int nWidth, nHeight; + gtk_widget_get_size_request(m_pWidget, &nWidth, &nHeight); + return Size(nWidth, nHeight); + } + + virtual Size get_preferred_size() const override + { + Size aRet(-1, -1); + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + { + aRet = Size(gtk_scrolled_window_get_min_content_width(GTK_SCROLLED_WINDOW(pParent)), + gtk_scrolled_window_get_min_content_height(GTK_SCROLLED_WINDOW(pParent))); + } + GtkRequisition size; + gtk_widget_get_preferred_size(m_pWidget, nullptr, &size); + if (aRet.Width() == -1) + aRet.setWidth(size.width); + if (aRet.Height() == -1) + aRet.setHeight(size.height); + return aRet; + } + + virtual void show() override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + gtk_widget_show(pParent); + gtk_widget_show(m_pWidget); + } + + virtual void hide() override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + gtk_widget_hide(pParent); + gtk_widget_hide(m_pWidget); + } + + virtual OUString get_selected_text() const override + { + assert(gtk_icon_view_get_model(m_pIconView) && "don't request selection when frozen"); + GtkTreeIter iter; + if (get_selected_iterator(&iter)) + return get(iter, m_nTextCol); + return OUString(); + } + + virtual int count_selected_items() const override + { + GList* pList = gtk_icon_view_get_selected_items(m_pIconView); + int nRet = g_list_length(pList); + g_list_free_full(pList, reinterpret_cast(gtk_tree_path_free)); + return nRet; + } + + virtual void select(int pos) override + { + assert(gtk_icon_view_get_model(m_pIconView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + if (pos == -1 || (pos == 0 && n_children() == 0)) + { + gtk_icon_view_unselect_all(m_pIconView); + } + else + { + GtkTreePath* path = gtk_tree_path_new_from_indices(pos, -1); + gtk_icon_view_select_path(m_pIconView, path); + gtk_icon_view_scroll_to_path(m_pIconView, path, false, 0, 0); + gtk_tree_path_free(path); + } + enable_notify_events(); + } + + virtual void unselect(int pos) override + { + assert(gtk_icon_view_get_model(m_pIconView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + if (pos == -1 || (pos == 0 && n_children() == 0)) + { + gtk_icon_view_select_all(m_pIconView); + } + else + { + GtkTreePath* path = gtk_tree_path_new_from_indices(pos, -1); + gtk_icon_view_select_path(m_pIconView, path); + gtk_tree_path_free(path); + } + enable_notify_events(); + } + + virtual bool get_selected(weld::TreeIter* pIter) const override + { + GtkInstanceTreeIter* pGtkIter = static_cast(pIter); + return get_selected_iterator(pGtkIter ? &pGtkIter->iter : nullptr); + } + + virtual bool get_cursor(weld::TreeIter* pIter) const override + { + GtkInstanceTreeIter* pGtkIter = static_cast(pIter); + GtkTreePath* path; + gtk_icon_view_get_cursor(m_pIconView, &path, nullptr); + if (pGtkIter && path) + { + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + gtk_tree_model_get_iter(pModel, &pGtkIter->iter, path); + } + return path != nullptr; + } + + virtual void set_cursor(const weld::TreeIter& rIter) override + { + disable_notify_events(); + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreePath* path = gtk_tree_model_get_path(pModel, const_cast(&rGtkIter.iter)); + gtk_icon_view_set_cursor(m_pIconView, path, nullptr, false); + gtk_tree_path_free(path); + enable_notify_events(); + } + + virtual bool get_iter_first(weld::TreeIter& rIter) const override + { + GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + return gtk_tree_model_get_iter_first(pModel, &rGtkIter.iter); + } + + virtual void scroll_to_item(const weld::TreeIter& rIter) override + { + assert(gtk_icon_view_get_model(m_pIconView) && "don't select when frozen, select after thaw. Note selection doesn't survive a freeze"); + disable_notify_events(); + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GtkTreePath* path = gtk_tree_model_get_path(pModel, const_cast(&rGtkIter.iter)); + gtk_icon_view_scroll_to_path(m_pIconView, path, false, 0, 0); + gtk_tree_path_free(path); + enable_notify_events(); + } + + virtual std::unique_ptr make_iterator(const weld::TreeIter* pOrig) const override + { + return std::unique_ptr(new GtkInstanceTreeIter(static_cast(pOrig))); + } + + virtual void selected_foreach(const std::function& func) override + { + GtkInstanceTreeIter aGtkIter(nullptr); + + GtkTreeModel *pModel = GTK_TREE_MODEL(m_pTreeStore); + GList* pList = gtk_icon_view_get_selected_items(m_pIconView); + for (GList* pItem = g_list_first(pList); pItem; pItem = g_list_next(pItem)) + { + GtkTreePath* path = static_cast(pItem->data); + gtk_tree_model_get_iter(pModel, &aGtkIter.iter, path); + if (func(aGtkIter)) + break; + } + g_list_free_full(pList, reinterpret_cast(gtk_tree_path_free)); + } + + virtual int n_children() const override + { + return gtk_tree_model_iter_n_children(GTK_TREE_MODEL(m_pTreeStore), nullptr); + } + + virtual OUString get_id(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + return get(rGtkIter.iter, m_nIdCol); + } + + virtual OUString get_text(const weld::TreeIter& rIter) const override + { + const GtkInstanceTreeIter& rGtkIter = static_cast(rIter); + return get(rGtkIter.iter, m_nTextCol); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pIconView, m_nSelectionChangedSignalId); + g_signal_handler_block(m_pIconView, m_nItemActivatedSignalId); + + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + + g_signal_handler_unblock(m_pIconView, m_nItemActivatedSignalId); + g_signal_handler_unblock(m_pIconView, m_nSelectionChangedSignalId); + } + + virtual ~GtkInstanceIconView() override + { + if (m_pSelectionChangeEvent) + Application::RemoveUserEvent(m_pSelectionChangeEvent); + + if (m_nQueryTooltipSignalId) + g_signal_handler_disconnect(m_pIconView, m_nQueryTooltipSignalId); + + g_signal_handler_disconnect(m_pIconView, m_nItemActivatedSignalId); + g_signal_handler_disconnect(m_pIconView, m_nSelectionChangedSignalId); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pIconView, m_nPopupMenu); +#endif + } +}; + +} + +IMPL_LINK_NOARG(GtkInstanceIconView, async_signal_selection_changed, void*, void) +{ + m_pSelectionChangeEvent = nullptr; + signal_selection_changed(); +} + +namespace { + +void signalDestroyFlag(GtkWidget*, gpointer destroyed) +{ + bool* pDestroyed = static_cast(destroyed); + *pDestroyed = true; +} + +class GtkInstanceSpinButton : public GtkInstanceEditable, public virtual weld::SpinButton +{ +private: + GtkSpinButton* m_pButton; + gulong m_nValueChangedSignalId; + gulong m_nOutputSignalId; + gulong m_nInputSignalId; + bool m_bFormatting; + bool m_bBlockOutput; + bool m_bBlank; + + static void signalValueChanged(GtkSpinButton*, gpointer widget) + { + GtkInstanceSpinButton* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->m_bBlank = false; + pThis->signal_value_changed(); + } + + bool guarded_signal_output() + { + if (m_bBlockOutput) + return true; + m_bFormatting = true; + bool bRet = signal_output(); + m_bFormatting = false; + return bRet; + } + + static gboolean signalOutput(GtkSpinButton*, gpointer widget) + { + GtkInstanceSpinButton* pThis = static_cast(widget); + SolarMutexGuard aGuard; + return pThis->guarded_signal_output(); + } + + static gint signalInput(GtkSpinButton*, gdouble* new_value, gpointer widget) + { + GtkInstanceSpinButton* pThis = static_cast(widget); + SolarMutexGuard aGuard; + int result; + TriState eHandled = pThis->signal_input(&result); + if (eHandled == TRISTATE_INDET) + return 0; + if (eHandled == TRISTATE_TRUE) + { + *new_value = pThis->toGtk(result); + return 1; + } + return GTK_INPUT_ERROR; + } + + virtual void signal_activate() override + { + bool bActivateDestroy(false); + gulong nDestroySignalId = g_signal_connect(m_pButton, "destroy", G_CALLBACK(signalDestroyFlag), &bActivateDestroy); + gtk_spin_button_update(m_pButton); + if (bActivateDestroy) + return; + g_signal_handler_disconnect(m_pButton, nDestroySignalId); + GtkInstanceEditable::signal_activate(); + } + + double toGtk(sal_Int64 nValue) const + { + return static_cast(nValue) / Power10(get_digits()); + } + + sal_Int64 fromGtk(double fValue) const + { + return FRound(fValue * Power10(get_digits())); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static gboolean signalScroll(GtkWidget* pWidget, GdkEventScroll* /*pEvent*/, gpointer /*widget*/) + { + // tdf#149823 follow WheelBehavior setting, so if we don't have focus + // we don't react to the scroll-event. + MouseWheelBehaviour nWheelBehavior(Application::GetSettings().GetMouseSettings().GetWheelBehavior()); + switch (nWheelBehavior) + { + case MouseWheelBehaviour::ALWAYS: + break; + case MouseWheelBehaviour::Disable: + g_signal_stop_emission_by_name(pWidget, "scroll-event"); + break; + case MouseWheelBehaviour::FocusOnly: + if (!gtk_widget_has_focus(pWidget)) + g_signal_stop_emission_by_name(pWidget, "scroll-event"); + break; + } + return false; + } +#endif + +public: + GtkInstanceSpinButton(GtkSpinButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceEditable(GTK_WIDGET(pButton), pBuilder, bTakeOwnership) + , m_pButton(pButton) + , m_nValueChangedSignalId(g_signal_connect(pButton, "value-changed", G_CALLBACK(signalValueChanged), this)) + , m_nOutputSignalId(g_signal_connect(pButton, "output", G_CALLBACK(signalOutput), this)) + , m_nInputSignalId(g_signal_connect(pButton, "input", G_CALLBACK(signalInput), this)) + , m_bFormatting(false) + , m_bBlockOutput(false) + , m_bBlank(false) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_text_set_activates_default(GTK_TEXT(m_pDelegate), true); +#endif +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_connect(pButton, "scroll-event", G_CALLBACK(signalScroll), this); +#endif + } + + virtual sal_Int64 get_value() const override + { + return fromGtk(gtk_spin_button_get_value(m_pButton)); + } + + virtual void set_value(sal_Int64 value) override + { + disable_notify_events(); + m_bBlank = false; + gtk_spin_button_set_value(m_pButton, toGtk(value)); + enable_notify_events(); + } + + virtual void set_text(const OUString& rText) override + { + disable_notify_events(); + // tdf#122786 if we're just formatting a value, then we're done, + // however if set_text has been called directly we want to update our + // value from this new text, but don't want to reformat with that value + if (!m_bFormatting) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_editable_set_text(m_pEditable, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); +#else + gtk_entry_set_text(GTK_ENTRY(m_pButton), OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); +#endif + + m_bBlockOutput = true; + gtk_spin_button_update(m_pButton); + m_bBlank = rText.isEmpty(); + m_bBlockOutput = false; + } + else + { + bool bKeepBlank = m_bBlank && get_value() == 0; + if (!bKeepBlank) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_editable_set_text(m_pEditable, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); +#else + gtk_entry_set_text(GTK_ENTRY(m_pButton), OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); +#endif + m_bBlank = false; + } + } + enable_notify_events(); + } + + virtual void set_range(sal_Int64 min, sal_Int64 max) override + { + disable_notify_events(); + gtk_spin_button_set_range(m_pButton, toGtk(min), toGtk(max)); + enable_notify_events(); + } + + virtual void get_range(sal_Int64& min, sal_Int64& max) const override + { + double gtkmin, gtkmax; + gtk_spin_button_get_range(m_pButton, >kmin, >kmax); + min = fromGtk(gtkmin); + max = fromGtk(gtkmax); + } + + virtual void set_increments(int step, int page) override + { + disable_notify_events(); + gtk_spin_button_set_increments(m_pButton, toGtk(step), toGtk(page)); + enable_notify_events(); + } + + virtual void get_increments(int& step, int& page) const override + { + double gtkstep, gtkpage; + gtk_spin_button_get_increments(m_pButton, >kstep, >kpage); + step = fromGtk(gtkstep); + page = fromGtk(gtkpage); + } + + virtual void set_digits(unsigned int digits) override + { + disable_notify_events(); + gtk_spin_button_set_digits(m_pButton, digits); + enable_notify_events(); + } + + virtual unsigned int get_digits() const override + { + return gtk_spin_button_get_digits(m_pButton); + } + + virtual void set_font(const vcl::Font& rFont) override + { + m_aCustomFont.use_custom_font(&rFont, u"spinbutton"); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pButton, m_nValueChangedSignalId); + GtkInstanceEditable::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceEditable::enable_notify_events(); + g_signal_handler_unblock(m_pButton, m_nValueChangedSignalId); + } + + virtual ~GtkInstanceSpinButton() override + { + g_signal_handler_disconnect(m_pButton, m_nInputSignalId); + g_signal_handler_disconnect(m_pButton, m_nOutputSignalId); + g_signal_handler_disconnect(m_pButton, m_nValueChangedSignalId); + } +}; + +} + +namespace { + +class GtkInstanceFormattedSpinButton : public GtkInstanceEditable, public virtual weld::FormattedSpinButton +{ +private: + GtkSpinButton* m_pButton; + std::unique_ptr m_xOwnFormatter; + weld::EntryFormatter* m_pFormatter; + gulong m_nValueChangedSignalId; + gulong m_nOutputSignalId; + gulong m_nInputSignalId; + bool m_bEmptyField; + bool m_bSyncingValue; + double m_dValueWhenEmpty; + + bool signal_output() + { + double fValue = gtk_spin_button_get_value(m_pButton); + m_bEmptyField &= fValue == m_dValueWhenEmpty; + if (!m_bEmptyField) + GetFormatter().SetValue(fValue); + return true; + } + + static gboolean signalOutput(GtkSpinButton*, gpointer widget) + { + GtkInstanceFormattedSpinButton* pThis = static_cast(widget); + SolarMutexGuard aGuard; + return pThis->signal_output(); + } + + gint signal_input(double* value) + { + Formatter& rFormatter = GetFormatter(); + rFormatter.Modify(); + // if the blank-mode is enabled then if the input is empty don't parse + // the input but keep the value as it is. store what the value the + // blank is associated with and until the value is changed, or the text + // is updated from the outside, don't output that value + m_bEmptyField = rFormatter.IsEmptyFieldEnabled() && get_text().isEmpty(); + if (m_bEmptyField) + { + m_dValueWhenEmpty = gtk_spin_button_get_value(m_pButton); + *value = m_dValueWhenEmpty; + } + else + *value = rFormatter.GetValue(); + return 1; + } + + static gint signalInput(GtkSpinButton*, gdouble* new_value, gpointer widget) + { + GtkInstanceFormattedSpinButton* pThis = static_cast(widget); + SolarMutexGuard aGuard; + return pThis->signal_input(new_value); + } + + static void signalValueChanged(GtkSpinButton*, gpointer widget) + { + GtkInstanceFormattedSpinButton* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_value_changed(); + } + +public: + GtkInstanceFormattedSpinButton(GtkSpinButton* pButton, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceEditable(GTK_WIDGET(pButton), pBuilder, bTakeOwnership) + , m_pButton(pButton) + , m_pFormatter(nullptr) + , m_nValueChangedSignalId(g_signal_connect(pButton, "value-changed", G_CALLBACK(signalValueChanged), this)) + , m_nOutputSignalId(g_signal_connect(pButton, "output", G_CALLBACK(signalOutput), this)) + , m_nInputSignalId(g_signal_connect(pButton, "input", G_CALLBACK(signalInput), this)) + , m_bEmptyField(false) + , m_bSyncingValue(false) + , m_dValueWhenEmpty(0.0) + { + } + + virtual void set_text(const OUString& rText) override + { + GtkInstanceEditable::set_text(rText); + Formatter& rFormatter = GetFormatter(); + m_bEmptyField = rFormatter.IsEmptyFieldEnabled() && rText.isEmpty(); + if (m_bEmptyField) + m_dValueWhenEmpty = gtk_spin_button_get_value(m_pButton); + } + + virtual void connect_changed(const Link& rLink) override + { + if (!m_pFormatter) // once a formatter is set, it takes over "changed" + { + GtkInstanceEditable::connect_changed(rLink); + return; + } + m_pFormatter->connect_changed(rLink); + } + + virtual void connect_focus_out(const Link& rLink) override + { + if (!m_pFormatter) // once a formatter is set, it takes over "focus-out" + { + GtkInstanceEditable::connect_focus_out(rLink); + return; + } + m_pFormatter->connect_focus_out(rLink); + } + + virtual void SetFormatter(weld::EntryFormatter* pFormatter) override + { + m_xOwnFormatter.reset(); + m_pFormatter = pFormatter; + sync_range_from_formatter(); + sync_value_from_formatter(); + sync_increments_from_formatter(); + } + + virtual weld::EntryFormatter& GetFormatter() override + { + if (!m_pFormatter) + { + auto aFocusOutHdl = m_aFocusOutHdl; + m_aFocusOutHdl = Link(); + auto aChangeHdl = m_aChangeHdl; + m_aChangeHdl = Link(); + + double fValue = gtk_spin_button_get_value(m_pButton); + double fMin, fMax; + gtk_spin_button_get_range(m_pButton, &fMin, &fMax); + double fStep; + gtk_spin_button_get_increments(m_pButton, &fStep, nullptr); + m_xOwnFormatter.reset(new weld::EntryFormatter(*this)); + m_xOwnFormatter->SetMinValue(fMin); + m_xOwnFormatter->SetMaxValue(fMax); + m_xOwnFormatter->SetSpinSize(fStep); + m_xOwnFormatter->SetValue(fValue); + + m_xOwnFormatter->connect_focus_out(aFocusOutHdl); + m_xOwnFormatter->connect_changed(aChangeHdl); + + m_pFormatter = m_xOwnFormatter.get(); + } + return *m_pFormatter; + } + + virtual void sync_value_from_formatter() override + { + if (!m_pFormatter) + return; + // tdf#135317 avoid reenterence + if (m_bSyncingValue) + return; + m_bSyncingValue = true; + disable_notify_events(); + // tdf#138519 use gtk_adjustment_set_value instead of gtk_spin_button_set_value because the + // latter doesn't change the value if the new value is less than an EPSILON diff of 1e-10 + // from the old value + gtk_adjustment_set_value(gtk_spin_button_get_adjustment(m_pButton), m_pFormatter->GetValue()); + enable_notify_events(); + m_bSyncingValue = false; + } + + virtual void sync_range_from_formatter() override + { + if (!m_pFormatter) + return; + disable_notify_events(); + double fMin = m_pFormatter->HasMinValue() ? m_pFormatter->GetMinValue() : std::numeric_limits::lowest(); + double fMax = m_pFormatter->HasMaxValue() ? m_pFormatter->GetMaxValue() : std::numeric_limits::max(); + gtk_spin_button_set_range(m_pButton, fMin, fMax); + enable_notify_events(); + } + + virtual void sync_increments_from_formatter() override + { + if (!m_pFormatter) + return; + disable_notify_events(); + double fSpinSize = m_pFormatter->GetSpinSize(); + gtk_spin_button_set_increments(m_pButton, fSpinSize, fSpinSize * 10); + enable_notify_events(); + } + + virtual void set_font(const vcl::Font& rFont) override + { + m_aCustomFont.use_custom_font(&rFont, u"spinbutton"); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pButton, m_nValueChangedSignalId); + GtkInstanceEditable::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceEditable::enable_notify_events(); + g_signal_handler_unblock(m_pButton, m_nValueChangedSignalId); + } + + virtual ~GtkInstanceFormattedSpinButton() override + { + g_signal_handler_disconnect(m_pButton, m_nInputSignalId); + g_signal_handler_disconnect(m_pButton, m_nOutputSignalId); + g_signal_handler_disconnect(m_pButton, m_nValueChangedSignalId); + + m_pFormatter = nullptr; + m_xOwnFormatter.reset(); + } +}; + +} + +namespace { + +class GtkInstanceLabel : public GtkInstanceWidget, public virtual weld::Label +{ +private: + GtkLabel* m_pLabel; + + void set_text_background_color(const Color& rColor) + { + guint16 nRed = rColor.GetRed() << 8; + guint16 nGreen = rColor.GetGreen() << 8; + guint16 nBlue = rColor.GetBlue() << 8; + + PangoAttrType aFilterAttrs[] = {PANGO_ATTR_BACKGROUND, PANGO_ATTR_INVALID}; + + PangoAttrList* pOrigList = gtk_label_get_attributes(m_pLabel); + PangoAttrList* pAttrs = pOrigList ? pango_attr_list_copy(pOrigList) : pango_attr_list_new(); + PangoAttrList* pRemovedAttrs = pOrigList ? pango_attr_list_filter(pAttrs, filter_pango_attrs, &aFilterAttrs) : nullptr; + pango_attr_list_insert(pAttrs, pango_attr_background_new(nRed, nGreen, nBlue)); + gtk_label_set_attributes(m_pLabel, pAttrs); + pango_attr_list_unref(pAttrs); + pango_attr_list_unref(pRemovedAttrs); + } + + void set_text_foreground_color(const Color& rColor, bool bSetBold) + { + guint16 nRed = rColor.GetRed() << 8; + guint16 nGreen = rColor.GetGreen() << 8; + guint16 nBlue = rColor.GetBlue() << 8; + + PangoAttrType aFilterAttrs[] = {PANGO_ATTR_FOREGROUND, PANGO_ATTR_WEIGHT, PANGO_ATTR_INVALID}; + + if (!bSetBold) + aFilterAttrs[1] = PANGO_ATTR_INVALID; + + PangoAttrList* pOrigList = gtk_label_get_attributes(m_pLabel); + PangoAttrList* pAttrs = pOrigList ? pango_attr_list_copy(pOrigList) : pango_attr_list_new(); + PangoAttrList* pRemovedAttrs = pOrigList ? pango_attr_list_filter(pAttrs, filter_pango_attrs, &aFilterAttrs) : nullptr; + if (rColor != COL_AUTO) + pango_attr_list_insert(pAttrs, pango_attr_foreground_new(nRed, nGreen, nBlue)); + if (bSetBold) + pango_attr_list_insert(pAttrs, pango_attr_weight_new(PANGO_WEIGHT_BOLD)); + gtk_label_set_attributes(m_pLabel, pAttrs); + pango_attr_list_unref(pAttrs); + pango_attr_list_unref(pRemovedAttrs); + } + +public: + GtkInstanceLabel(GtkLabel* pLabel, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pLabel), pBuilder, bTakeOwnership) + , m_pLabel(pLabel) + { + } + + virtual void set_label(const OUString& rText) override + { + ::set_label(m_pLabel, rText); + } + + virtual OUString get_label() const override + { + return ::get_label(m_pLabel); + } + + virtual void set_mnemonic_widget(Widget* pTarget) override + { + assert(!gtk_label_get_selectable(m_pLabel) && "don't use set_mnemonic_widget on selectable labels, for consistency with gen backend"); + GtkInstanceWidget* pTargetWidget = dynamic_cast(pTarget); + gtk_label_set_mnemonic_widget(m_pLabel, pTargetWidget ? pTargetWidget->getWidget() : nullptr); + } + + virtual void set_label_type(weld::LabelType eType) override + { + switch (eType) + { + case weld::LabelType::Normal: + gtk_label_set_attributes(m_pLabel, nullptr); + break; + case weld::LabelType::Warning: + set_text_background_color(Application::GetSettings().GetStyleSettings().GetWarningColor()); + break; + case weld::LabelType::Error: + set_text_background_color(Application::GetSettings().GetStyleSettings().GetHighlightColor()); + break; + case weld::LabelType::Title: + set_text_foreground_color(Application::GetSettings().GetStyleSettings().GetLightColor(), true); + break; + } + } + + virtual void set_font(const vcl::Font& rFont) override + { + ::set_font(m_pLabel, rFont); + } + + virtual void set_font_color(const Color& rColor) override + { + set_text_foreground_color(rColor, false); + } +}; + +} + +std::unique_ptr GtkInstanceFrame::weld_label_widget() const +{ + GtkWidget* pLabel = gtk_frame_get_label_widget(m_pFrame); + if (!pLabel || !GTK_IS_LABEL(pLabel)) + return nullptr; + return std::make_unique(GTK_LABEL(pLabel), m_pBuilder, false); +} + +namespace { + +GdkClipboard* widget_get_clipboard(GtkWidget* pWidget) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_widget_get_clipboard(pWidget); +#else + return gtk_widget_get_clipboard(pWidget, GDK_SELECTION_CLIPBOARD); +#endif +} + +class GtkInstanceTextView : public GtkInstanceWidget, public virtual weld::TextView +{ +private: + GtkTextView* m_pTextView; + GtkTextBuffer* m_pTextBuffer; + GtkAdjustment* m_pVAdjustment; + GtkCssProvider* m_pFgCssProvider; + WidgetFont m_aCustomFont; + int m_nMaxTextLength; + gulong m_nChangedSignalId; // we don't disable/enable this one, it's to implement max-length + gulong m_nInsertTextSignalId; + gulong m_nCursorPosSignalId; + gulong m_nHasSelectionSignalId; // we don't disable/enable this one, it's to implement + // auto-scroll to cursor on losing selection + gulong m_nVAdjustChangedSignalId; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong m_nButtonPressEvent; // we don't disable/enable this one, it's to block mouse + // click down from getting to (potential) toplevel + // GtkSalFrame parent, which grabs focus away + + static gboolean signalButtonPressEvent(GtkWidget*, GdkEventButton*, gpointer) + { + // e.g. on clicking on the help TextView in OTableDesignHelpBar the currently displayed text shouldn't disappear + return true; + } +#endif + + static void signalChanged(GtkTextBuffer*, gpointer widget) + { + GtkInstanceTextView* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_changed(); + } + + static void signalInserText(GtkTextBuffer *pBuffer, GtkTextIter *pLocation, gchar* /*pText*/, gint /*nLen*/, gpointer widget) + { + GtkInstanceTextView* pThis = static_cast(widget); + pThis->insert_text(pBuffer, pLocation); + } + + void insert_text(GtkTextBuffer *pBuffer, GtkTextIter *pLocation) + { + if (m_nMaxTextLength) + { + gint nCount = gtk_text_buffer_get_char_count(pBuffer); + if (nCount > m_nMaxTextLength) + { + GtkTextIter nStart, nEnd; + gtk_text_buffer_get_iter_at_offset(m_pTextBuffer, &nStart, m_nMaxTextLength); + gtk_text_buffer_get_end_iter(m_pTextBuffer, &nEnd); + gtk_text_buffer_delete(m_pTextBuffer, &nStart, &nEnd); + gtk_text_iter_assign(pLocation, &nStart); + } + } + } + + static void signalCursorPosition(GtkTextBuffer*, GParamSpec*, gpointer widget) + { + GtkInstanceTextView* pThis = static_cast(widget); + pThis->signal_cursor_position(); + } + + static void signalHasSelection(GtkTextBuffer*, GParamSpec*, gpointer widget) + { + GtkInstanceTextView* pThis = static_cast(widget); + pThis->signal_has_selection(); + } + + void signal_has_selection() + { + /* + in the data browser (Data Sources, shift+ctrl+f4), entering a + multiline cell selects all, on cursoring to the right, the selection + is lost and the cursor is at the end but gtk doesn't auto-scroll to + the cursor so if the text needs scrolling to see the cursor it is off + screen, another cursor makes gtk auto-scroll as wanted. So on losing + selection help gtk out and do the initial scroll ourselves here + */ + if (!gtk_text_buffer_get_has_selection(m_pTextBuffer)) + { + GtkTextMark* pMark = gtk_text_buffer_get_insert(m_pTextBuffer); + gtk_text_view_scroll_mark_onscreen(m_pTextView, pMark); + } + } + + static void signalVAdjustValueChanged(GtkAdjustment*, gpointer widget) + { + GtkInstanceTextView* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_vadjustment_changed(); + } + +public: + GtkInstanceTextView(GtkTextView* pTextView, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pTextView), pBuilder, bTakeOwnership) + , m_pTextView(pTextView) + , m_pTextBuffer(gtk_text_view_get_buffer(pTextView)) + , m_pVAdjustment(gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(pTextView))) + , m_pFgCssProvider(nullptr) + , m_aCustomFont(m_pWidget) + , m_nMaxTextLength(0) + , m_nChangedSignalId(g_signal_connect(m_pTextBuffer, "changed", G_CALLBACK(signalChanged), this)) + , m_nInsertTextSignalId(g_signal_connect_after(m_pTextBuffer, "insert-text", G_CALLBACK(signalInserText), this)) + , m_nCursorPosSignalId(g_signal_connect(m_pTextBuffer, "notify::cursor-position", G_CALLBACK(signalCursorPosition), this)) + , m_nHasSelectionSignalId(g_signal_connect(m_pTextBuffer, "notify::has-selection", G_CALLBACK(signalHasSelection), this)) + , m_nVAdjustChangedSignalId(g_signal_connect(m_pVAdjustment, "value-changed", G_CALLBACK(signalVAdjustValueChanged), this)) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_nButtonPressEvent(g_signal_connect_after(m_pTextView, "button-press-event", G_CALLBACK(signalButtonPressEvent), this)) +#endif + { + } + + virtual void set_size_request(int nWidth, int nHeight) override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + { + gtk_scrolled_window_set_min_content_width(GTK_SCROLLED_WINDOW(pParent), nWidth); + gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(pParent), nHeight); + return; + } + gtk_widget_set_size_request(m_pWidget, nWidth, nHeight); + } + + virtual void set_text(const OUString& rText) override + { + disable_notify_events(); + OString sText(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)); + gtk_text_buffer_set_text(m_pTextBuffer, sText.getStr(), sText.getLength()); + enable_notify_events(); + } + + virtual OUString get_text() const override + { + GtkTextIter start, end; + gtk_text_buffer_get_bounds(m_pTextBuffer, &start, &end); + char* pStr = gtk_text_buffer_get_text(m_pTextBuffer, &start, &end, true); + OUString sRet(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + g_free(pStr); + return sRet; + } + + virtual void replace_selection(const OUString& rText) override + { + disable_notify_events(); + gtk_text_buffer_delete_selection(m_pTextBuffer, false, gtk_text_view_get_editable(m_pTextView)); + OString sText(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)); + gtk_text_buffer_insert_at_cursor(m_pTextBuffer, sText.getStr(), sText.getLength()); + enable_notify_events(); + } + + virtual bool get_selection_bounds(int& rStartPos, int& rEndPos) override + { + GtkTextIter start, end; + gtk_text_buffer_get_selection_bounds(m_pTextBuffer, &start, &end); + rStartPos = gtk_text_iter_get_offset(&start); + rEndPos = gtk_text_iter_get_offset(&end); + return rStartPos != rEndPos; + } + + virtual void select_region(int nStartPos, int nEndPos) override + { + disable_notify_events(); + GtkTextIter start, end; + gtk_text_buffer_get_iter_at_offset(m_pTextBuffer, &start, nStartPos); + gtk_text_buffer_get_iter_at_offset(m_pTextBuffer, &end, nEndPos); + gtk_text_buffer_select_range(m_pTextBuffer, &start, &end); + GtkTextMark* mark = gtk_text_buffer_create_mark(m_pTextBuffer, "scroll", &end, true); + gtk_text_view_scroll_mark_onscreen(m_pTextView, mark); + enable_notify_events(); + } + + virtual void set_editable(bool bEditable) override + { + gtk_text_view_set_editable(m_pTextView, bEditable); + } + + virtual bool get_editable() const override + { + return gtk_text_view_get_editable(m_pTextView); + } + + virtual void set_max_length(int nChars) override + { + m_nMaxTextLength = nChars; + } + + virtual void set_monospace(bool bMonospace) override + { + gtk_text_view_set_monospace(m_pTextView, bMonospace); + } + + virtual void set_font_color(const Color& rColor) override + { + const bool bRemoveColor = rColor == COL_AUTO; + if (bRemoveColor && !m_pFgCssProvider) + return; + GtkStyleContext *pWidgetContext = gtk_widget_get_style_context(GTK_WIDGET(m_pTextView)); + if (m_pFgCssProvider) + { + gtk_style_context_remove_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pFgCssProvider)); + m_pFgCssProvider = nullptr; + } + if (bRemoveColor) + return; + OUString sColor = rColor.AsRGBHexString(); + m_pFgCssProvider = gtk_css_provider_new(); + OUString aBuffer = "textview text { color: #" + sColor + "; }"; + OString aResult = OUStringToOString(aBuffer, RTL_TEXTENCODING_UTF8); + css_provider_load_from_data(m_pFgCssProvider, aResult.getStr(), aResult.getLength()); + gtk_style_context_add_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pFgCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + + virtual void set_font(const vcl::Font& rFont) override + { + m_aCustomFont.use_custom_font(&rFont, u"textview"); + } + + virtual vcl::Font get_font() override + { + if (const vcl::Font* pFont = m_aCustomFont.get_custom_font()) + return *pFont; + return GtkInstanceWidget::get_font(); + } + + virtual void disable_notify_events() override + { + g_signal_handler_block(m_pVAdjustment, m_nVAdjustChangedSignalId); + g_signal_handler_block(m_pTextBuffer, m_nCursorPosSignalId); + g_signal_handler_block(m_pTextBuffer, m_nChangedSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pTextBuffer, m_nChangedSignalId); + g_signal_handler_unblock(m_pTextBuffer, m_nCursorPosSignalId); + g_signal_handler_unblock(m_pVAdjustment, m_nVAdjustChangedSignalId); + } + + // in gtk, 'up' when on the first line, will jump to the start of the line + // if not there already + virtual bool can_move_cursor_with_up() const override + { + GtkTextIter start, end; + gtk_text_buffer_get_selection_bounds(m_pTextBuffer, &start, &end); + return !gtk_text_iter_equal(&start, &end) || !gtk_text_iter_is_start(&start); + } + + // in gtk, 'down' when on the first line, will jump to the end of the line + // if not there already + virtual bool can_move_cursor_with_down() const override + { + GtkTextIter start, end; + gtk_text_buffer_get_selection_bounds(m_pTextBuffer, &start, &end); + return !gtk_text_iter_equal(&start, &end) || !gtk_text_iter_is_end(&end); + } + + virtual void cut_clipboard() override + { + GdkClipboard *pClipboard = widget_get_clipboard(GTK_WIDGET(m_pTextView)); + gtk_text_buffer_cut_clipboard(m_pTextBuffer, pClipboard, get_editable()); + } + + virtual void copy_clipboard() override + { + GdkClipboard *pClipboard = widget_get_clipboard(GTK_WIDGET(m_pTextView)); + gtk_text_buffer_copy_clipboard(m_pTextBuffer, pClipboard); + } + + virtual void paste_clipboard() override + { + GdkClipboard *pClipboard = widget_get_clipboard(GTK_WIDGET(m_pTextView)); + gtk_text_buffer_paste_clipboard(m_pTextBuffer, pClipboard, nullptr, get_editable()); + } + + virtual void set_alignment(TxtAlign eXAlign) override + { + GtkJustification eJust = GTK_JUSTIFY_LEFT; + switch (eXAlign) + { + case TxtAlign::Left: + eJust = GTK_JUSTIFY_LEFT; + break; + case TxtAlign::Center: + eJust = GTK_JUSTIFY_CENTER; + break; + case TxtAlign::Right: + eJust = GTK_JUSTIFY_RIGHT; + break; + } + gtk_text_view_set_justification(m_pTextView, eJust); + } + + virtual int vadjustment_get_value() const override + { + return gtk_adjustment_get_value(m_pVAdjustment); + } + + virtual void vadjustment_set_value(int value) override + { + disable_notify_events(); + gtk_adjustment_set_value(m_pVAdjustment, value); + enable_notify_events(); + } + + virtual int vadjustment_get_upper() const override + { + return gtk_adjustment_get_upper(m_pVAdjustment); + } + + virtual int vadjustment_get_lower() const override + { + return gtk_adjustment_get_lower(m_pVAdjustment); + } + + virtual int vadjustment_get_page_size() const override + { + return gtk_adjustment_get_page_size(m_pVAdjustment); + } + + virtual void show() override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + gtk_widget_show(pParent); + gtk_widget_show(m_pWidget); + } + + virtual void hide() override + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + if (GTK_IS_SCROLLED_WINDOW(pParent)) + gtk_widget_hide(pParent); + gtk_widget_hide(m_pWidget); + } + + virtual ~GtkInstanceTextView() override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pTextView, m_nButtonPressEvent); +#endif + g_signal_handler_disconnect(m_pVAdjustment, m_nVAdjustChangedSignalId); + g_signal_handler_disconnect(m_pTextBuffer, m_nInsertTextSignalId); + g_signal_handler_disconnect(m_pTextBuffer, m_nChangedSignalId); + g_signal_handler_disconnect(m_pTextBuffer, m_nCursorPosSignalId); + g_signal_handler_disconnect(m_pTextBuffer, m_nHasSelectionSignalId); + } +}; + +} + +namespace { + +class GtkInstanceDrawingArea; + +// IMHandler +class IMHandler +{ +private: + GtkInstanceDrawingArea* m_pArea; +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController* m_pFocusController; +#endif + GtkIMContext* m_pIMContext; + OUString m_sPreeditText; + gulong m_nFocusInSignalId; + gulong m_nFocusOutSignalId; + bool m_bExtTextInput; + +public: + IMHandler(GtkInstanceDrawingArea* pArea); + + void signalFocus(bool bIn); + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalFocusIn(GtkEventControllerFocus*, gpointer im_handler); +#else + static gboolean signalFocusIn(GtkWidget*, GdkEvent*, gpointer im_handler); +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalFocusOut(GtkEventControllerFocus*, gpointer im_handler); +#else + static gboolean signalFocusOut(GtkWidget*, GdkEvent*, gpointer im_handler); +#endif + + ~IMHandler(); + + void updateIMSpotLocation(); + + void set_cursor_location(const tools::Rectangle& rRect); + + static void signalIMCommit(GtkIMContext* /*pContext*/, gchar* pText, gpointer im_handler); + + static void signalIMPreeditChanged(GtkIMContext* pIMContext, gpointer im_handler); + + static gboolean signalIMRetrieveSurrounding(GtkIMContext* pContext, gpointer im_handler); + + static gboolean signalIMDeleteSurrounding(GtkIMContext*, gint nOffset, gint nChars, + gpointer im_handler); + + void StartExtTextInput(); + + static void signalIMPreeditStart(GtkIMContext*, gpointer im_handler); + + void EndExtTextInput(); + + static void signalIMPreeditEnd(GtkIMContext*, gpointer im_handler); + +#if !GTK_CHECK_VERSION(4, 0, 0) + bool im_context_filter_keypress(const GdkEventKey* pEvent); +#endif +}; + +#if !GTK_CHECK_VERSION(4, 0, 0) +AtkObject* (*default_drawing_area_get_accessible)(GtkWidget *widget); +#endif + +class GtkInstanceDrawingArea : public GtkInstanceWidget, public virtual weld::DrawingArea +{ +private: + GtkDrawingArea* m_pDrawingArea; + a11yref m_xAccessible; +#if !GTK_CHECK_VERSION(4, 0, 0) + AtkObject *m_pAccessible; +#endif + ScopedVclPtrInstance m_xDevice; + std::unique_ptr m_xIMHandler; + cairo_surface_t* m_pSurface; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong m_nDrawSignalId; +#endif + gulong m_nQueryTooltip; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong m_nPopupMenu; + gulong m_nScrollEvent; +#endif + GtkGesture *m_pZoomGesture; + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalDraw(GtkDrawingArea*, cairo_t *cr, int /*width*/, int /*height*/, gpointer widget) +#else + static gboolean signalDraw(GtkWidget*, cairo_t* cr, gpointer widget) +#endif + { + GtkInstanceDrawingArea* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_draw(cr); +#if !GTK_CHECK_VERSION(4, 0, 0) + return false; +#endif + } + void signal_draw(cairo_t* cr) + { + if (!m_pSurface) + return; + + GdkRectangle rect; +#if GTK_CHECK_VERSION(4, 0, 0) + double clip_x1, clip_x2, clip_y1, clip_y2; + cairo_clip_extents(cr, &clip_x1, &clip_y1, &clip_x2, &clip_y2); + rect.x = clip_x1; + rect.y = clip_y1; + rect.width = clip_x2 - clip_x1; + rect.height = clip_y2 - clip_y1; + if (rect.width <= 0 || rect.height <= 0) + return; +#else + if (!gdk_cairo_get_clip_rectangle(cr, &rect)) + return; +#endif + + tools::Rectangle aRect(Point(rect.x, rect.y), Size(rect.width, rect.height)); + aRect = m_xDevice->PixelToLogic(aRect); + m_xDevice->Erase(aRect); + m_aDrawHdl.Call(std::pair(*m_xDevice, aRect)); + cairo_surface_mark_dirty(m_pSurface); + + cairo_set_source_surface(cr, m_pSurface, 0, 0); + cairo_paint(cr); + + tools::Rectangle aFocusRect(m_aGetFocusRectHdl.Call(*this)); + if (!aFocusRect.IsEmpty()) + { + gtk_render_focus(gtk_widget_get_style_context(GTK_WIDGET(m_pDrawingArea)), cr, + aFocusRect.Left(), aFocusRect.Top(), aFocusRect.GetWidth(), aFocusRect.GetHeight()); + } + } + virtual void signal_size_allocate(guint nWidth, guint nHeight) override + { + Size aNewSize(nWidth, nHeight); + if (m_pSurface && aNewSize == m_xDevice->GetOutputSizePixel()) + { + // unchanged + return; + } + m_xDevice->SetOutputSizePixel(Size(nWidth, nHeight)); + m_pSurface = get_underlying_cairo_surface(*m_xDevice); + GtkInstanceWidget::signal_size_allocate(nWidth, nHeight); + } + static gboolean signalQueryTooltip(GtkWidget* pGtkWidget, gint x, gint y, + gboolean /*keyboard_mode*/, GtkTooltip *tooltip, + gpointer widget) + { + GtkInstanceDrawingArea* pThis = static_cast(widget); + tools::Rectangle aHelpArea(x, y); + OUString aTooltip = pThis->signal_query_tooltip(aHelpArea); + if (aTooltip.isEmpty()) + return false; + gtk_tooltip_set_text(tooltip, OUStringToOString(aTooltip, RTL_TEXTENCODING_UTF8).getStr()); + GdkRectangle aGdkHelpArea; + aGdkHelpArea.x = aHelpArea.Left(); + aGdkHelpArea.y = aHelpArea.Top(); + aGdkHelpArea.width = aHelpArea.GetWidth(); + aGdkHelpArea.height = aHelpArea.GetHeight(); + if (pThis->SwapForRTL()) + aGdkHelpArea.x = gtk_widget_get_allocated_width(pGtkWidget) - aGdkHelpArea.width - 1 - aGdkHelpArea.x; + gtk_tooltip_set_tip_area(tooltip, &aGdkHelpArea); + return true; + } + virtual bool signal_popup_menu(const CommandEvent& rCEvt) override + { + return signal_command(rCEvt); + } +#if !GTK_CHECK_VERSION(4, 0, 0) + bool signal_scroll(const GdkEventScroll* pEvent) + { + SalWheelMouseEvent aEvt(GtkSalFrame::GetWheelEvent(*pEvent)); + + if (SwapForRTL()) + aEvt.mnX = gtk_widget_get_allocated_width(m_pWidget) - 1 - aEvt.mnX; + + CommandWheelMode nMode; + sal_uInt16 nCode = aEvt.mnCode; + bool bHorz = aEvt.mbHorz; + if (nCode & KEY_MOD1) + nMode = CommandWheelMode::ZOOM; + else if (nCode & KEY_MOD2) + nMode = CommandWheelMode::DATAZOOM; + else + { + nMode = CommandWheelMode::SCROLL; + // #i85450# interpret shift-wheel as horizontal wheel action + if( (nCode & (KEY_SHIFT | KEY_MOD1 | KEY_MOD2 | KEY_MOD3)) == KEY_SHIFT ) + bHorz = true; + } + + CommandWheelData aWheelData(aEvt.mnDelta, aEvt.mnNotchDelta, aEvt.mnScrollLines, + nMode, nCode, bHorz, aEvt.mbDeltaIsPixel); + CommandEvent aCEvt(Point(aEvt.mnX, aEvt.mnY), CommandEventId::Wheel, true, &aWheelData); + return m_aCommandHdl.Call(aCEvt); + } + static gboolean signalScroll(GtkWidget*, GdkEventScroll* pEvent, gpointer widget) + { + GtkInstanceDrawingArea* pThis = static_cast(widget); + return pThis->signal_scroll(pEvent); + } +#endif + + bool handleSignalZoom(GtkGesture* gesture, GdkEventSequence* sequence, + GestureEventZoomType eEventType) + { + gdouble x = 0; + gdouble y = 0; + gtk_gesture_get_point(gesture, sequence, &x, &y); + + double fScaleDelta = gtk_gesture_zoom_get_scale_delta(GTK_GESTURE_ZOOM(gesture)); + + CommandGestureZoomData aGestureData(x, y, eEventType, fScaleDelta); + CommandEvent aCEvt(Point(x, y), CommandEventId::GestureZoom, true, &aGestureData); + return m_aCommandHdl.Call(aCEvt); + } + + static bool signalZoomBegin(GtkGesture* gesture, GdkEventSequence* sequence, gpointer widget) + { + GtkInstanceDrawingArea* pThis = static_cast(widget); + return pThis->handleSignalZoom(gesture, sequence, GestureEventZoomType::Begin); + } + + static bool signalZoomUpdate(GtkGesture* gesture, GdkEventSequence* sequence, gpointer widget) + { + GtkInstanceDrawingArea* pThis = static_cast(widget); + return pThis->handleSignalZoom(gesture, sequence, GestureEventZoomType::Update); + } + + static bool signalZoomEnd(GtkGesture* gesture, GdkEventSequence* sequence, gpointer widget) + { + GtkInstanceDrawingArea* pThis = static_cast(widget); + return pThis->handleSignalZoom(gesture, sequence, GestureEventZoomType::End); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalResize(GtkDrawingArea*, int nWidth, int nHeight, gpointer widget) + { + GtkInstanceWidget* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_size_allocate(nWidth, nHeight); + } +#endif + +public: + GtkInstanceDrawingArea(GtkDrawingArea* pDrawingArea, GtkInstanceBuilder* pBuilder, a11yref xA11y, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pDrawingArea), pBuilder, bTakeOwnership) + , m_pDrawingArea(pDrawingArea) + , m_xAccessible(std::move(xA11y)) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_pAccessible(nullptr) +#endif + , m_xDevice(DeviceFormat::WITHOUT_ALPHA) + , m_pSurface(nullptr) + , m_nQueryTooltip(g_signal_connect(m_pDrawingArea, "query-tooltip", G_CALLBACK(signalQueryTooltip), this)) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_nPopupMenu(g_signal_connect(m_pDrawingArea, "popup-menu", G_CALLBACK(signalPopupMenu), this)) + , m_nScrollEvent(g_signal_connect(m_pDrawingArea, "scroll-event", G_CALLBACK(signalScroll), this)) +#endif + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_drawing_area_set_draw_func(m_pDrawingArea, signalDraw, this, nullptr); +#else + m_nDrawSignalId = g_signal_connect(m_pDrawingArea, "draw", G_CALLBACK(signalDraw), this); + gtk_widget_add_events(GTK_WIDGET(pDrawingArea), GDK_TOUCHPAD_GESTURE_MASK); +#endif + + ensureMouseEventWidget(); +#if GTK_CHECK_VERSION(4,0,0) + m_pZoomGesture = gtk_gesture_zoom_new(); + gtk_widget_add_controller(m_pMouseEventBox, GTK_EVENT_CONTROLLER(m_pZoomGesture)); +#else + m_pZoomGesture = gtk_gesture_zoom_new(m_pMouseEventBox); +#endif + gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(m_pZoomGesture), + GTK_PHASE_TARGET); + // Note that the default zoom gesture signal handler needs to run first to setup correct + // scale delta. Otherwise the first "begin" event will always contain scale delta of infinity. + g_signal_connect_after(m_pZoomGesture, "begin", G_CALLBACK(signalZoomBegin), this); + g_signal_connect_after(m_pZoomGesture, "update", G_CALLBACK(signalZoomUpdate), this); + g_signal_connect_after(m_pZoomGesture, "end", G_CALLBACK(signalZoomEnd), this); + + gtk_widget_set_has_tooltip(m_pWidget, true); + g_object_set_data(G_OBJECT(m_pDrawingArea), "g-lo-GtkInstanceDrawingArea", this); + m_xDevice->EnableRTL(get_direction()); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + AtkObject* GetAtkObject(AtkObject* pDefaultAccessible) + { + if (!m_pAccessible && m_xAccessible.is()) + { + GtkWidget* pParent = gtk_widget_get_parent(m_pWidget); + m_pAccessible = atk_object_wrapper_new(m_xAccessible, gtk_widget_get_accessible(pParent), pDefaultAccessible); + if (m_pAccessible) + g_object_ref(m_pAccessible); + } + return m_pAccessible; + } +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + virtual void connect_size_allocate(const Link& rLink) override + { + m_nSizeAllocateSignalId = g_signal_connect(m_pWidget, "resize", G_CALLBACK(signalResize), this); + weld::Widget::connect_size_allocate(rLink); + } +#endif + + virtual void connect_mouse_press(const Link& rLink) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!(gtk_widget_get_events(m_pWidget) & GDK_BUTTON_PRESS_MASK)) + gtk_widget_add_events(m_pWidget, GDK_BUTTON_PRESS_MASK); +#endif + GtkInstanceWidget::connect_mouse_press(rLink); + } + + virtual void connect_mouse_release(const Link& rLink) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + if (!(gtk_widget_get_events(m_pWidget) & GDK_BUTTON_RELEASE_MASK)) + gtk_widget_add_events(m_pWidget, GDK_BUTTON_RELEASE_MASK); +#endif + GtkInstanceWidget::connect_mouse_release(rLink); + } + + virtual void set_direction(bool bRTL) override + { + GtkInstanceWidget::set_direction(bRTL); + m_xDevice->EnableRTL(bRTL); + } + + virtual void set_cursor(PointerStyle ePointerStyle) override + { + GdkCursor *pCursor = GtkSalFrame::getDisplay()->getCursor(ePointerStyle); + if (!gtk_widget_get_realized(GTK_WIDGET(m_pDrawingArea))) + gtk_widget_realize(GTK_WIDGET(m_pDrawingArea)); + widget_set_cursor(GTK_WIDGET(m_pDrawingArea), pCursor); + } + + virtual Point get_pointer_position() const override + { + GdkDisplay *pDisplay = gtk_widget_get_display(m_pWidget); + GdkSeat* pSeat = gdk_display_get_default_seat(pDisplay); + GdkDevice* pPointer = gdk_seat_get_pointer(pSeat); + double x(-1), y(-1); + GdkSurface* pWin = widget_get_surface(m_pWidget); + surface_get_device_position(pWin, pPointer, x, y, nullptr); + return Point(x, y); + } + + virtual void set_input_context(const InputContext& rInputContext) override; + + virtual void im_context_set_cursor_location(const tools::Rectangle& rCursorRect, int nExtTextInputWidth) override; + + int im_context_get_surrounding(OUString& rSurroundingText) + { + return signal_im_context_get_surrounding(rSurroundingText); + } + + bool im_context_delete_surrounding(const Selection& rRange) + { + return signal_im_context_delete_surrounding(rRange); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + virtual bool do_signal_key_press(const GdkEventKey* pEvent) override; + virtual bool do_signal_key_release(const GdkEventKey* pEvent) override; +#endif + + virtual void queue_draw() override + { + gtk_widget_queue_draw(GTK_WIDGET(m_pDrawingArea)); + } + + virtual void queue_draw_area(int x, int y, int width, int height) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + tools::Rectangle aRect(Point(x, y), Size(width, height)); + aRect = m_xDevice->LogicToPixel(aRect); + gtk_widget_queue_draw_area(GTK_WIDGET(m_pDrawingArea), aRect.Left(), aRect.Top(), aRect.GetWidth(), aRect.GetHeight()); +#else + (void)x; (void)y; (void)width; (void)height; + queue_draw(); +#endif + } + + virtual a11yref get_accessible_parent() override + { + //get_accessible_parent should only be needed for the vcl implementation, + //in the gtk impl the native AtkObject parent set via + //atk_object_wrapper_new(m_xAccessible, gtk_widget_get_accessible(pParent)); + //should negate the need. + assert(false && "get_accessible_parent should only be called on a vcl impl"); + return uno::Reference(); + } + + virtual a11yrelationset get_accessible_relation_set() override + { + //get_accessible_relation_set should only be needed for the vcl implementation, + //in the gtk impl the native equivalent should negate the need. + assert(false && "get_accessible_relation_set should only be called on a vcl impl"); + return uno::Reference(); + } + + virtual AbsoluteScreenPixelPoint get_accessible_location_on_screen() override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + AtkObject* pAtkObject = default_drawing_area_get_accessible(m_pWidget); +#endif + gint x(0), y(0); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (pAtkObject && ATK_IS_COMPONENT(pAtkObject)) + atk_component_get_extents(ATK_COMPONENT(pAtkObject), &x, &y, nullptr, nullptr, ATK_XY_SCREEN); +#endif + return AbsoluteScreenPixelPoint(x, y); + } + + virtual void set_accessible_name(const OUString& rName) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + AtkObject* pAtkObject = default_drawing_area_get_accessible(m_pWidget); + if (!pAtkObject) + return; + atk_object_set_name(pAtkObject, OUStringToOString(rName, RTL_TEXTENCODING_UTF8).getStr()); +#else + (void)rName; +#endif + } + + virtual OUString get_accessible_name() const override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + AtkObject* pAtkObject = default_drawing_area_get_accessible(m_pWidget); + const char* pStr = pAtkObject ? atk_object_get_name(pAtkObject) : nullptr; + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); +#else + return OUString(); +#endif + } + + virtual OUString get_accessible_description() const override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + AtkObject* pAtkObject = default_drawing_area_get_accessible(m_pWidget); + const char* pStr = pAtkObject ? atk_object_get_description(pAtkObject) : nullptr; + return OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); +#else + return OUString(); +#endif + } + + virtual void enable_drag_source(rtl::Reference& rHelper, sal_uInt8 eDNDConstants) override + { + do_enable_drag_source(rHelper, eDNDConstants); + } + + virtual bool do_signal_drag_begin(bool& rUnsetDragIcon) override + { + rUnsetDragIcon = false; + if (m_aDragBeginHdl.Call(*this)) + return true; + return false; + } + + virtual ~GtkInstanceDrawingArea() override + { +#if GTK_CHECK_VERSION(4,0,0) + gtk_widget_remove_controller(m_pMouseEventBox, GTK_EVENT_CONTROLLER(m_pZoomGesture)); +#else + g_clear_object(&m_pZoomGesture); +#endif + + g_object_steal_data(G_OBJECT(m_pDrawingArea), "g-lo-GtkInstanceDrawingArea"); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (m_pAccessible) + g_object_unref(m_pAccessible); +#endif + css::uno::Reference xComp(m_xAccessible, css::uno::UNO_QUERY); + if (xComp.is()) + xComp->dispose(); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pDrawingArea, m_nScrollEvent); +#endif +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pDrawingArea, m_nPopupMenu); +#endif + g_signal_handler_disconnect(m_pDrawingArea, m_nQueryTooltip); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_drawing_area_set_draw_func(m_pDrawingArea, nullptr, nullptr, nullptr); +#else + g_signal_handler_disconnect(m_pDrawingArea, m_nDrawSignalId); +#endif + } + + virtual OutputDevice& get_ref_device() override + { + return *m_xDevice; + } + + bool signal_command(const CommandEvent& rCEvt) + { + return m_aCommandHdl.Call(rCEvt); + } + + virtual void click(const Point& rPos) override + { + MouseEvent aEvent(rPos); + m_aMousePressHdl.Call(aEvent); + m_aMouseReleaseHdl.Call(aEvent); + } +}; + +IMHandler::IMHandler(GtkInstanceDrawingArea* pArea) + : m_pArea(pArea) + , m_pIMContext(gtk_im_multicontext_new()) + , m_bExtTextInput(false) +{ + GtkWidget* pWidget = m_pArea->getWidget(); + +#if GTK_CHECK_VERSION(4, 0, 0) + m_pFocusController = gtk_event_controller_focus_new(); + gtk_widget_add_controller(pWidget, m_pFocusController); + + m_nFocusInSignalId = g_signal_connect(m_pFocusController, "enter", G_CALLBACK(signalFocusIn), this); + m_nFocusOutSignalId = g_signal_connect(m_pFocusController, "leave", G_CALLBACK(signalFocusOut), this); +#else + m_nFocusInSignalId = g_signal_connect(pWidget, "focus-in-event", G_CALLBACK(signalFocusIn), this); + m_nFocusOutSignalId = g_signal_connect(pWidget, "focus-out-event", G_CALLBACK(signalFocusOut), this); +#endif + + g_signal_connect(m_pIMContext, "preedit-start", G_CALLBACK(signalIMPreeditStart), this); + g_signal_connect(m_pIMContext, "preedit-end", G_CALLBACK(signalIMPreeditEnd), this); + g_signal_connect(m_pIMContext, "commit", G_CALLBACK(signalIMCommit), this); + g_signal_connect(m_pIMContext, "preedit-changed", G_CALLBACK(signalIMPreeditChanged), this); + g_signal_connect(m_pIMContext, "retrieve-surrounding", G_CALLBACK(signalIMRetrieveSurrounding), this); + g_signal_connect(m_pIMContext, "delete-surrounding", G_CALLBACK(signalIMDeleteSurrounding), this); + + if (!gtk_widget_get_realized(pWidget)) + gtk_widget_realize(pWidget); + im_context_set_client_widget(m_pIMContext, pWidget); + if (gtk_widget_has_focus(m_pArea->getWidget())) + gtk_im_context_focus_in(m_pIMContext); +} + +void IMHandler::signalFocus(bool bIn) +{ + if (bIn) + gtk_im_context_focus_in(m_pIMContext); + else + gtk_im_context_focus_out(m_pIMContext); +} + +#if GTK_CHECK_VERSION(4, 0, 0) +void IMHandler::signalFocusIn(GtkEventControllerFocus*, gpointer im_handler) +#else +gboolean IMHandler::signalFocusIn(GtkWidget*, GdkEvent*, gpointer im_handler) +#endif +{ + IMHandler* pThis = static_cast(im_handler); + pThis->signalFocus(true); +#if !GTK_CHECK_VERSION(4, 0, 0) + return false; +#endif +} + +#if GTK_CHECK_VERSION(4, 0, 0) +void IMHandler::signalFocusOut(GtkEventControllerFocus*, gpointer im_handler) +#else +gboolean IMHandler::signalFocusOut(GtkWidget*, GdkEvent*, gpointer im_handler) +#endif +{ + IMHandler* pThis = static_cast(im_handler); + pThis->signalFocus(false); +#if !GTK_CHECK_VERSION(4, 0, 0) + return false; +#endif +} + +IMHandler::~IMHandler() +{ + EndExtTextInput(); + +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pFocusController, m_nFocusOutSignalId); + g_signal_handler_disconnect(m_pFocusController, m_nFocusInSignalId); +#else + g_signal_handler_disconnect(m_pArea->getWidget(), m_nFocusOutSignalId); + g_signal_handler_disconnect(m_pArea->getWidget(), m_nFocusInSignalId); +#endif + + if (gtk_widget_has_focus(m_pArea->getWidget())) + gtk_im_context_focus_out(m_pIMContext); + + // first give IC a chance to deinitialize + im_context_set_client_widget(m_pIMContext, nullptr); + // destroy old IC + g_object_unref(m_pIMContext); +} + +void IMHandler::updateIMSpotLocation() +{ + CommandEvent aCEvt(Point(), CommandEventId::CursorPos); + // we expect set_cursor_location to get triggered by this + m_pArea->signal_command(aCEvt); +} + +void IMHandler::set_cursor_location(const tools::Rectangle& rRect) +{ + GdkRectangle aArea{static_cast(rRect.Left()), static_cast(rRect.Top()), + static_cast(rRect.GetWidth()), static_cast(rRect.GetHeight())}; + gtk_im_context_set_cursor_location(m_pIMContext, &aArea); +} + +void IMHandler::signalIMCommit(GtkIMContext* /*pContext*/, gchar* pText, gpointer im_handler) +{ + IMHandler* pThis = static_cast(im_handler); + + SolarMutexGuard aGuard; + + // at least editeng expects to have seen a start before accepting a commit + pThis->StartExtTextInput(); + + OUString sText(pText, strlen(pText), RTL_TEXTENCODING_UTF8); + CommandExtTextInputData aData(sText, nullptr, sText.getLength(), 0, false); + CommandEvent aCEvt(Point(), CommandEventId::ExtTextInput, false, &aData); + pThis->m_pArea->signal_command(aCEvt); + + pThis->updateIMSpotLocation(); + + pThis->EndExtTextInput(); + + pThis->m_sPreeditText.clear(); +} + +void IMHandler::signalIMPreeditChanged(GtkIMContext* pIMContext, gpointer im_handler) +{ + IMHandler* pThis = static_cast(im_handler); + + SolarMutexGuard aGuard; + + sal_Int32 nCursorPos(0); + sal_uInt8 nCursorFlags(0); + std::vector aInputFlags; + OUString sText = GtkSalFrame::GetPreeditDetails(pIMContext, aInputFlags, nCursorPos, nCursorFlags); + + // change from nothing to nothing -> do not start preedit e.g. this + // will activate input into a calc cell without user input + if (sText.isEmpty() && pThis->m_sPreeditText.isEmpty()) + return; + + pThis->m_sPreeditText = sText; + + CommandExtTextInputData aData(sText, aInputFlags.data(), nCursorPos, nCursorFlags, false); + CommandEvent aCEvt(Point(), CommandEventId::ExtTextInput, false, &aData); + pThis->m_pArea->signal_command(aCEvt); + + pThis->updateIMSpotLocation(); +} + +gboolean IMHandler::signalIMRetrieveSurrounding(GtkIMContext* pContext, gpointer im_handler) +{ + IMHandler* pThis = static_cast(im_handler); + + SolarMutexGuard aGuard; + + OUString sSurroundingText; + int nCursorIndex = pThis->m_pArea->im_context_get_surrounding(sSurroundingText); + + if (nCursorIndex != -1) + { + OString sUTF = OUStringToOString(sSurroundingText, RTL_TEXTENCODING_UTF8); + std::u16string_view sCursorText(sSurroundingText.subView(0, nCursorIndex)); + gtk_im_context_set_surrounding(pContext, sUTF.getStr(), sUTF.getLength(), + OUStringToOString(sCursorText, RTL_TEXTENCODING_UTF8).getLength()); + } + + return true; +} + +gboolean IMHandler::signalIMDeleteSurrounding(GtkIMContext*, gint nOffset, gint nChars, + gpointer im_handler) +{ + bool bRet = false; + + IMHandler* pThis = static_cast(im_handler); + + SolarMutexGuard aGuard; + + OUString sSurroundingText; + sal_Int32 nCursorIndex = pThis->m_pArea->im_context_get_surrounding(sSurroundingText); + + Selection aSelection = SalFrame::CalcDeleteSurroundingSelection(sSurroundingText, nCursorIndex, nOffset, nChars); + if (aSelection != Selection(SAL_MAX_UINT32, SAL_MAX_UINT32)) + bRet = pThis->m_pArea->im_context_delete_surrounding(aSelection); + return bRet; +} + +void IMHandler::StartExtTextInput() +{ + if (m_bExtTextInput) + return; + CommandEvent aCEvt(Point(), CommandEventId::StartExtTextInput); + m_pArea->signal_command(aCEvt); + m_bExtTextInput = true; +} + +void IMHandler::signalIMPreeditStart(GtkIMContext*, gpointer im_handler) +{ + IMHandler* pThis = static_cast(im_handler); + SolarMutexGuard aGuard; + pThis->StartExtTextInput(); + pThis->updateIMSpotLocation(); +} + +void IMHandler::EndExtTextInput() +{ + if (!m_bExtTextInput) + return; + CommandEvent aCEvt(Point(), CommandEventId::EndExtTextInput); + m_pArea->signal_command(aCEvt); + m_bExtTextInput = false; +} + +void IMHandler::signalIMPreeditEnd(GtkIMContext*, gpointer im_handler) +{ + IMHandler* pThis = static_cast(im_handler); + SolarMutexGuard aGuard; + pThis->updateIMSpotLocation(); + pThis->EndExtTextInput(); +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +bool IMHandler::im_context_filter_keypress(const GdkEventKey* pEvent) +{ + return gtk_im_context_filter_keypress(m_pIMContext, const_cast(pEvent)); +} +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) +bool GtkInstanceDrawingArea::do_signal_key_press(const GdkEventKey* pEvent) +{ + if (m_xIMHandler && m_xIMHandler->im_context_filter_keypress(pEvent)) + return true; + return GtkInstanceWidget::do_signal_key_press(pEvent); +} + +bool GtkInstanceDrawingArea::do_signal_key_release(const GdkEventKey* pEvent) +{ + if (m_xIMHandler && m_xIMHandler->im_context_filter_keypress(pEvent)) + return true; + return GtkInstanceWidget::do_signal_key_release(pEvent); +} +#endif + +void GtkInstanceDrawingArea::set_input_context(const InputContext& rInputContext) +{ + bool bUseIm(rInputContext.GetOptions() & InputContextFlags::Text); + if (!bUseIm) + { + m_xIMHandler.reset(); + return; + } + // create a new im context + if (!m_xIMHandler) + m_xIMHandler.reset(new IMHandler(this)); +} + +void GtkInstanceDrawingArea::im_context_set_cursor_location(const tools::Rectangle& rCursorRect, int /*nExtTextInputWidth*/) +{ + if (!m_xIMHandler) + return; + m_xIMHandler->set_cursor_location(rCursorRect); +} + +} + +#if !GTK_CHECK_VERSION(4, 0, 0) + +static void InsertSpecialChar(GtkEntry *pEntry) +{ + if (auto pImplFncGetSpecialChars = vcl::GetGetSpecialCharsFunction()) + { + weld::Window* pDialogParent = nullptr; + + GtkWidget* pTopLevel = widget_get_toplevel(GTK_WIDGET(pEntry)); + if (GtkSalFrame* pFrame = pTopLevel ? GtkSalFrame::getFromWindow(pTopLevel) : nullptr) + pDialogParent = pFrame->GetFrameWeld(); + + std::unique_ptr xFrameWeld; + if (!pDialogParent && pTopLevel) + { + xFrameWeld.reset(new GtkInstanceWindow(GTK_WINDOW(pTopLevel), nullptr, false)); + pDialogParent = xFrameWeld.get(); + } + + OUString aChars = pImplFncGetSpecialChars(pDialogParent, ::get_font(GTK_WIDGET(pEntry))); + if (!aChars.isEmpty()) + { + gtk_editable_delete_selection(GTK_EDITABLE(pEntry)); + gint position = gtk_editable_get_position(GTK_EDITABLE(pEntry)); + OString sText(OUStringToOString(aChars, RTL_TEXTENCODING_UTF8)); + gtk_editable_insert_text(GTK_EDITABLE(pEntry), sText.getStr(), sText.getLength(), + &position); + gtk_editable_set_position(GTK_EDITABLE(pEntry), position); + } + } +} + +static gboolean signalEntryInsertSpecialCharKeyPress(GtkEntry* pEntry, GdkEventKey* pEvent, gpointer) +{ + if ((pEvent->keyval == GDK_KEY_S || pEvent->keyval == GDK_KEY_s) && + (pEvent->state & GDK_MODIFIER_MASK) == static_cast(GDK_SHIFT_MASK|GDK_CONTROL_MASK)) + { + InsertSpecialChar(pEntry); + return true; + } + return false; +} + +static void signalActivateEntryInsertSpecialChar(GtkEntry *pEntry) +{ + InsertSpecialChar(pEntry); +} + +static void signalEntryPopulatePopup(GtkEntry* pEntry, GtkWidget* pMenu, gpointer) +{ + if (!GTK_IS_MENU(pMenu)) + return; + + if (!vcl::GetGetSpecialCharsFunction()) + return; + + GtkWidget *item = gtk_menu_item_new_with_mnemonic(MapToGtkAccelerator(VclResId(STR_SPECIAL_CHARACTER_MENU_ENTRY)).getStr()); + gtk_widget_show(item); + g_signal_connect_swapped(item, "activate", G_CALLBACK(signalActivateEntryInsertSpecialChar), pEntry); + gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), item); +} + +#endif + +namespace { + +GtkBuilder* makeMenuToggleButtonBuilder() +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + OUString aUri(AllSettings::GetUIRootDir() + "vcl/ui/menutogglebutton3.ui"); +#else + OUString aUri(AllSettings::GetUIRootDir() + "vcl/ui/menutogglebutton4.ui"); +#endif + OUString aPath; + osl::FileBase::getSystemPathFromFileURL(aUri, aPath); + return gtk_builder_new_from_file(OUStringToOString(aPath, RTL_TEXTENCODING_UTF8).getStr()); +} + +#if !GTK_CHECK_VERSION(4, 0, 0) + +GtkBuilder* makeComboBoxBuilder() +{ + OUString aUri(AllSettings::GetUIRootDir() + "vcl/ui/combobox.ui"); + OUString aPath; + osl::FileBase::getSystemPathFromFileURL(aUri, aPath); + return gtk_builder_new_from_file(OUStringToOString(aPath, RTL_TEXTENCODING_UTF8).getStr()); +} + +// pop down the toplevel combobox menu when something is activated from a custom +// submenu, i.e. wysiwyg style menu +class CustomRenderMenuButtonHelper : public MenuHelper +{ +private: + GtkToggleButton* m_pComboBox; +public: + CustomRenderMenuButtonHelper(GtkMenu* pMenu, GtkToggleButton* pComboBox) + : MenuHelper(pMenu, false) + , m_pComboBox(pComboBox) + { + } + virtual void signal_item_activate(const OUString& /*rIdent*/) override + { + gtk_toggle_button_set_active(m_pComboBox, false); + } +}; + +#endif + +gboolean signalTooltipQuery(GtkWidget* pWidget, gint /*x*/, gint /*y*/, + gboolean /*keyboard_mode*/, GtkTooltip *tooltip) +{ + const ImplSVHelpData& aHelpData = ImplGetSVHelpData(); + if (aHelpData.mbBalloonHelp) // extended tips + { +#if !GTK_CHECK_VERSION(4, 0, 0) + // by default use accessible description + AtkObject* pAtkObject = gtk_widget_get_accessible(pWidget); + const char* pDesc = pAtkObject ? atk_object_get_description(pAtkObject) : nullptr; + if (pDesc && pDesc[0]) + { + gtk_tooltip_set_text(tooltip, pDesc); + return true; + } +#endif + + // fallback to the mechanism which needs help installed + OUString sHelpId = ::get_help_id(pWidget); + Help* pHelp = !sHelpId.isEmpty() ? Application::GetHelp() : nullptr; + if (pHelp) + { + OUString sHelpText = pHelp->GetHelpText(sHelpId, static_cast(nullptr)); + if (!sHelpText.isEmpty()) + { + gtk_tooltip_set_text(tooltip, OUStringToOString(sHelpText, RTL_TEXTENCODING_UTF8).getStr()); + return true; + } + } + } + + const char* pDesc = gtk_widget_get_tooltip_text(pWidget); + if (pDesc && pDesc[0]) + { + gtk_tooltip_set_text(tooltip, pDesc); + return true; + } + + return false; +} + +#if GTK_CHECK_VERSION(4, 0, 0) + +class GtkInstanceComboBox : public GtkInstanceWidget, public vcl::ISearchableStringList, public virtual weld::ComboBox +{ +private: + GtkComboBox* m_pComboBox; +// GtkOverlay* m_pOverlay; +// GtkTreeView* m_pTreeView; +// GtkMenuButton* m_pOverlayButton; // button that the StyleDropdown uses on an active row + GtkWidget* m_pMenuWindow; + GtkTreeModel* m_pTreeModel; + GtkCellRenderer* m_pButtonTextRenderer; + GtkWidget* m_pEntry; + GtkEditable* m_pEditable; +// GtkCellView* m_pCellView; + GtkEventController* m_pKeyController; + GtkEventController* m_pEntryKeyController; + GtkEventController* m_pMenuKeyController; + GtkEventController* m_pEntryFocusController; +// std::unique_ptr m_xCustomMenuButtonHelper; + WidgetFont m_aCustomFont; + std::optional m_xEntryFont; + std::unique_ptr m_xSorter; + vcl::QuickSelectionEngine m_aQuickSelectionEngine; + std::vector> m_aSeparatorRows; +#if 0 + OUString m_sMenuButtonRow; +#endif +// bool m_bHoverSelection; +// bool m_bMouseInOverlayButton; + bool m_bPopupActive; + bool m_bAutoComplete; + bool m_bAutoCompleteCaseSensitive; + bool m_bChangedByMenu; + bool m_bCustomRenderer; + bool m_bUserSelectEntry; + gint m_nTextCol; + gint m_nIdCol; +// gulong m_nToggleFocusInSignalId; +// gulong m_nToggleFocusOutSignalId; +// gulong m_nRowActivatedSignalId; + gulong m_nChangedSignalId; + gulong m_nPopupShownSignalId; + gulong m_nKeyPressEventSignalId; + gulong m_nEntryInsertTextSignalId; + gulong m_nEntryActivateSignalId; + gulong m_nEntryFocusInSignalId; + gulong m_nEntryFocusOutSignalId; + gulong m_nEntryKeyPressEventSignalId; + guint m_nAutoCompleteIdleId; +// gint m_nNonCustomLineHeight; + gint m_nPrePopupCursorPos; + int m_nMRUCount; + int m_nMaxMRUCount; + + static gboolean idleAutoComplete(gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->auto_complete(); + return false; + } + + void auto_complete() + { + m_nAutoCompleteIdleId = 0; + OUString aStartText = get_active_text(); + int nStartPos, nEndPos; + get_entry_selection_bounds(nStartPos, nEndPos); + int nMaxSelection = std::max(nStartPos, nEndPos); + if (nMaxSelection != aStartText.getLength()) + return; + + disable_notify_events(); + int nActive = get_active(); + int nStart = nActive; + + if (nStart == -1) + nStart = 0; + + int nPos = -1; + + int nZeroRow = 0; + if (m_nMRUCount) + nZeroRow += (m_nMRUCount + 1); + + if (!m_bAutoCompleteCaseSensitive) + { + // Try match case insensitive from current position + nPos = starts_with(m_pTreeModel, aStartText, 0, nStart, false); + if (nPos == -1 && nStart != 0) + { + // Try match case insensitive, but from start + nPos = starts_with(m_pTreeModel, aStartText, 0, nZeroRow, false); + } + } + + if (nPos == -1) + { + // Try match case sensitive from current position + nPos = starts_with(m_pTreeModel, aStartText, 0, nStart, true); + if (nPos == -1 && nStart != 0) + { + // Try match case sensitive, but from start + nPos = starts_with(m_pTreeModel, aStartText, 0, nZeroRow, true); + } + } + + if (nPos != -1) + { + OUString aText = get_text_including_mru(nPos); + if (aText != aStartText) + { + SolarMutexGuard aGuard; + set_active_including_mru(nPos, true); + } + select_entry_region(aText.getLength(), aStartText.getLength()); + } + enable_notify_events(); + } + + static void signalEntryInsertText(GtkEntry* pEntry, const gchar* pNewText, gint nNewTextLength, + gint* position, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_entry_insert_text(pEntry, pNewText, nNewTextLength, position); + } + + void signal_entry_insert_text(GtkEntry* pEntry, const gchar* pNewText, gint nNewTextLength, gint* position) + { + if (m_bPopupActive) // not entered by the user + return; + + // first filter inserted text + if (m_aEntryInsertTextHdl.IsSet()) + { + OUString sText(pNewText, nNewTextLength, RTL_TEXTENCODING_UTF8); + const bool bContinue = m_aEntryInsertTextHdl.Call(sText); + if (bContinue && !sText.isEmpty()) + { + OString sFinalText(OUStringToOString(sText, RTL_TEXTENCODING_UTF8)); + g_signal_handlers_block_by_func(pEntry, reinterpret_cast(signalEntryInsertText), this); + gtk_editable_insert_text(GTK_EDITABLE(pEntry), sFinalText.getStr(), sFinalText.getLength(), position); + g_signal_handlers_unblock_by_func(pEntry, reinterpret_cast(signalEntryInsertText), this); + } + g_signal_stop_emission_by_name(pEntry, "insert-text"); + } + + if (m_bAutoComplete) + { + // now check for autocompletes + if (m_nAutoCompleteIdleId) + g_source_remove(m_nAutoCompleteIdleId); + m_nAutoCompleteIdleId = g_idle_add(idleAutoComplete, this); + } + } + + static void signalChanged(GtkComboBox*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->fire_signal_changed(); + } + + void fire_signal_changed() + { + m_bUserSelectEntry = true; + m_bChangedByMenu = m_bPopupActive; + signal_changed(); + m_bChangedByMenu = false; + } + + static void signalPopupToggled(GObject*, GParamSpec*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_popup_toggled(); + } + +#if 0 + int get_popup_height(gint& rPopupWidth) + { + const StyleSettings& rSettings = Application::GetSettings().GetStyleSettings(); + + int nMaxRows = rSettings.GetListBoxMaximumLineCount(); + bool bAddScrollWidth = false; + int nRows = get_count_including_mru(); + if (nMaxRows < nRows) + { + nRows = nMaxRows; + bAddScrollWidth = true; + } + + GList* pColumns = gtk_tree_view_get_columns(m_pTreeView); + gint nRowHeight = get_height_row(m_pTreeView, pColumns); + g_list_free(pColumns); + + gint nSeparatorHeight = get_height_row_separator(m_pTreeView); + gint nHeight = get_height_rows(nRowHeight, nSeparatorHeight, nRows); + + // if we're using a custom renderer, limit the height to the height nMaxRows would be + // for a normal renderer, and then round down to how many custom rows fit in that + // space + if (m_nNonCustomLineHeight != -1 && nRowHeight) + { + gint nNormalHeight = get_height_rows(m_nNonCustomLineHeight, nSeparatorHeight, nMaxRows); + if (nHeight > nNormalHeight) + { + gint nRowsOnly = nNormalHeight - get_height_rows(0, nSeparatorHeight, nMaxRows); + gint nCustomRows = (nRowsOnly + (nRowHeight - 1)) / nRowHeight; + nHeight = get_height_rows(nRowHeight, nSeparatorHeight, nCustomRows); + } + } + + if (bAddScrollWidth) + rPopupWidth += rSettings.GetScrollBarSize(); + + return nHeight; + } +#endif + + bool toggle_button_get_active() + { + GValue value = G_VALUE_INIT; + g_value_init(&value, G_TYPE_BOOLEAN); + g_object_get_property(G_OBJECT(m_pComboBox), "popup-shown", &value); + return g_value_get_boolean(&value); + } + + void menu_toggled() + { + if (!m_bPopupActive) + { +#if 0 + if (m_bHoverSelection) + { + // turn hover selection back off until mouse is moved again + // *after* menu is shown again + gtk_tree_view_set_hover_selection(m_pTreeView, false); + m_bHoverSelection = false; + } +#endif + + if (!m_bUserSelectEntry) + set_active_including_mru(m_nPrePopupCursorPos, true); + +#if 0 + // undo show_menu tooltip blocking + GtkWidget* pParent = widget_get_toplevel(m_pToggleButton); + GtkSalFrame* pFrame = pParent ? GtkSalFrame::getFromWindow(pParent) : nullptr; + if (pFrame) + pFrame->UnblockTooltip(); +#endif + } + else + { + m_nPrePopupCursorPos = get_active(); + + m_bUserSelectEntry = false; + + // if we are in mru mode always start with the cursor at the top of the menu + if (m_nMaxMRUCount) + set_active_including_mru(0, true); + } + } + + virtual void signal_popup_toggled() override + { + m_aQuickSelectionEngine.Reset(); + + bool bOldPopupActive = m_bPopupActive; + m_bPopupActive = toggle_button_get_active(); + + menu_toggled(); + + if (bOldPopupActive != m_bPopupActive) + { + ComboBox::signal_popup_toggled(); + // restore focus to the GtkEntry when the popup is gone, which + // is what the vcl case does, to ease the transition a little, + // but don't do it if the focus was moved out of togglebutton + // by something else already (e.g. font combobox in toolbar + // on a "direct pick" from the menu which moves focus into + // the main document + if (!m_bPopupActive && m_pEntry && has_child_focus()) + { + disable_notify_events(); + gtk_widget_grab_focus(m_pEntry); + enable_notify_events(); + } + } + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalEntryFocusIn(GtkEventControllerFocus*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_entry_focus_in(); + } +#else + static gboolean signalEntryFocusIn(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->signal_entry_focus_in(); + return false; + } +#endif + + void signal_entry_focus_in() + { + signal_focus_in(); + } + +#if GTK_CHECK_VERSION(4, 0, 0) + static void signalEntryFocusOut(GtkEventControllerFocus*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_entry_focus_out(); + } +#else + static gboolean signalEntryFocusOut(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->signal_entry_focus_out(); + return false; + } +#endif + + void signal_entry_focus_out() + { + // if we have an untidy selection on losing focus remove the selection + int nStartPos, nEndPos; + if (get_entry_selection_bounds(nStartPos, nEndPos)) + { + int nMin = std::min(nStartPos, nEndPos); + int nMax = std::max(nStartPos, nEndPos); + if (nMin != 0 || nMax != get_active_text().getLength()) + select_entry_region(0, 0); + } + signal_focus_out(); + } + + static void signalEntryActivate(GtkEntry*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->signal_entry_activate(); + } + + void signal_entry_activate() + { + if (m_aEntryActivateHdl.IsSet()) + { + SolarMutexGuard aGuard; + if (m_aEntryActivateHdl.Call(*this)) + g_signal_stop_emission_by_name(m_pEntry, "activate"); + } + update_mru(); + } + + OUString get(int pos, int col) const + { + OUString sRet; + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + { + gchar* pStr; + gtk_tree_model_get(m_pTreeModel, &iter, col, &pStr, -1); + sRet = OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + g_free(pStr); + } + return sRet; + } + + void set(int pos, int col, std::u16string_view rText) + { + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + { + OString aStr(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)); + gtk_list_store_set(GTK_LIST_STORE(m_pTreeModel), &iter, col, aStr.getStr(), -1); + } + } + + int find(std::u16string_view rStr, int col, bool bSearchMRUArea) const + { + GtkTreeIter iter; + if (!gtk_tree_model_get_iter_first(m_pTreeModel, &iter)) + return -1; + + int nRet = 0; + + if (!bSearchMRUArea && m_nMRUCount) + { + if (!gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, m_nMRUCount + 1)) + return -1; + nRet += (m_nMRUCount + 1); + } + + OString aStr(OUStringToOString(rStr, RTL_TEXTENCODING_UTF8)); + do + { + gchar* pStr; + gtk_tree_model_get(m_pTreeModel, &iter, col, &pStr, -1); + const bool bEqual = g_strcmp0(pStr, aStr.getStr()) == 0; + g_free(pStr); + if (bEqual) + return nRet; + ++nRet; + } while (gtk_tree_model_iter_next(m_pTreeModel, &iter)); + + return -1; + } + + bool separator_function(const GtkTreePath* path) + { + return ::separator_function(path, m_aSeparatorRows); + } + + bool separator_function(int pos) + { + GtkTreePath* path = gtk_tree_path_new_from_indices(pos, -1); + bool bRet = separator_function(path); + gtk_tree_path_free(path); + return bRet; + } + + static gboolean separatorFunction(GtkTreeModel* pTreeModel, GtkTreeIter* pIter, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + GtkTreePath* path = gtk_tree_model_get_path(pTreeModel, pIter); + bool bRet = pThis->separator_function(path); + gtk_tree_path_free(path); + return bRet; + } + + // https://gitlab.gnome.org/GNOME/gtk/issues/310 + // + // in the absence of a built-in solution + // a) support typeahead for the case where there is no entry widget, typing ahead + // into the button itself will select via the vcl selection engine, a matching + // entry + static gboolean signalKeyPress(GtkEventControllerKey*, guint keyval, guint keycode, GdkModifierType state, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + SolarMutexGuard aGuard; + return pThis->signal_key_press(CreateKeyEvent(keyval, keycode, state, 0)); + } + + // tdf#131076 we want return in a ComboBox to act like return in a + // GtkEntry and activate the default dialog/assistant button + bool combobox_activate() + { + GtkWidget *pComboBox = GTK_WIDGET(m_pComboBox); + GtkWidget *pToplevel = widget_get_toplevel(pComboBox); + GtkWindow *pWindow = GTK_WINDOW(pToplevel); + if (!pWindow) + return false; + if (!GTK_IS_DIALOG(pWindow) && !GTK_IS_ASSISTANT(pWindow)) + return false; + bool bDone = false; + GtkWidget *pDefaultWidget = gtk_window_get_default_widget(pWindow); + if (pDefaultWidget && pDefaultWidget != pComboBox && gtk_widget_get_sensitive(pDefaultWidget)) + bDone = gtk_widget_activate(pDefaultWidget); + return bDone; + } + + static gboolean signalEntryKeyPress(GtkEventControllerKey*, guint keyval, guint keycode, GdkModifierType state, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + LocalizeDecimalSeparator(keyval); + return pThis->signal_entry_key_press(CreateKeyEvent(keyval, keycode, state, 0)); + } + + bool signal_entry_key_press(const KeyEvent& rKEvt) + { + vcl::KeyCode aKeyCode = rKEvt.GetKeyCode(); + + bool bDone = false; + + auto nCode = aKeyCode.GetCode(); + switch (nCode) + { + case KEY_DOWN: + { + sal_uInt16 nKeyMod = aKeyCode.GetModifier(); + if (!nKeyMod) + { + int nCount = get_count_including_mru(); + int nActive = get_active_including_mru() + 1; + while (nActive < nCount && separator_function(nActive)) + ++nActive; + if (nActive < nCount) + set_active_including_mru(nActive, true); + bDone = true; + } + else if (nKeyMod == KEY_MOD2 && !m_bPopupActive) + { + gtk_combo_box_popup(m_pComboBox); + bDone = true; + } + break; + } + case KEY_UP: + { + sal_uInt16 nKeyMod = aKeyCode.GetModifier(); + if (!nKeyMod) + { + int nStartBound = m_bPopupActive ? 0 : (m_nMRUCount + 1); + int nActive = get_active_including_mru() - 1; + while (nActive >= nStartBound && separator_function(nActive)) + --nActive; + if (nActive >= nStartBound) + set_active_including_mru(nActive, true); + bDone = true; + } + break; + } + case KEY_PAGEUP: + { + sal_uInt16 nKeyMod = aKeyCode.GetModifier(); + if (!nKeyMod) + { + int nCount = get_count_including_mru(); + int nStartBound = m_bPopupActive ? 0 : (m_nMRUCount + 1); + int nActive = nStartBound; + while (nActive < nCount && separator_function(nActive)) + ++nActive; + if (nActive < nCount) + set_active_including_mru(nActive, true); + bDone = true; + } + break; + } + case KEY_PAGEDOWN: + { + sal_uInt16 nKeyMod = aKeyCode.GetModifier(); + if (!nKeyMod) + { + int nActive = get_count_including_mru() - 1; + int nStartBound = m_bPopupActive ? 0 : (m_nMRUCount + 1); + while (nActive >= nStartBound && separator_function(nActive)) + --nActive; + if (nActive >= nStartBound) + set_active_including_mru(nActive, true); + bDone = true; + } + break; + } + default: + break; + } + + return bDone; + } + + bool signal_key_press(const KeyEvent& rKEvt) + { +#if 0 + if (m_bHoverSelection) + { + // once a key is pressed, turn off hover selection until mouse is + // moved again otherwise when the treeview scrolls it jumps to the + // position under the mouse. + gtk_tree_view_set_hover_selection(m_pTreeView, false); + m_bHoverSelection = false; + } +#endif + + vcl::KeyCode aKeyCode = rKEvt.GetKeyCode(); + + bool bDone = false; + + auto nCode = aKeyCode.GetCode(); + switch (nCode) + { + case KEY_DOWN: + case KEY_UP: + case KEY_PAGEUP: + case KEY_PAGEDOWN: + case KEY_HOME: + case KEY_END: + case KEY_LEFT: + case KEY_RIGHT: + case KEY_RETURN: + { + m_aQuickSelectionEngine.Reset(); + sal_uInt16 nKeyMod = aKeyCode.GetModifier(); + // tdf#131076 don't let bare return toggle menu popup active, but do allow deactivate + if (nCode == KEY_RETURN && !nKeyMod) + { + if (!m_bPopupActive) + bDone = combobox_activate(); + else + { + // treat 'return' as if the active entry was clicked on + signalChanged(m_pComboBox, this); + gtk_combo_box_popdown(m_pComboBox); + bDone = true; + } + } + else if (nCode == KEY_UP && nKeyMod == KEY_MOD2 && m_bPopupActive) + { + gtk_combo_box_popdown(m_pComboBox); + bDone = true; + } + else if (nCode == KEY_DOWN && nKeyMod == KEY_MOD2 && !m_bPopupActive) + { + gtk_combo_box_popup(m_pComboBox); + bDone = true; + } + break; + } + case KEY_ESCAPE: + { + m_aQuickSelectionEngine.Reset(); + if (m_bPopupActive) + { + gtk_combo_box_popdown(m_pComboBox); + bDone = true; + } + break; + } + default: + // tdf#131076 let base space toggle menu popup when it's not already visible + if (nCode == KEY_SPACE && !aKeyCode.GetModifier() && !m_bPopupActive) + bDone = false; + else + bDone = m_aQuickSelectionEngine.HandleKeyEvent(rKEvt); + break; + } + + if (!bDone) + { + if (!m_pEntry) + bDone = signal_entry_key_press(rKEvt); + else + { + // with gtk4-4.2.1 the unconsumed keystrokes don't appear to get to + // the GtkEntry for up/down to move to the next entry without this extra help + // (which means this extra indirection is probably effectively + // the same as if calling signal_entry_key_press directly here) + bDone = gtk_event_controller_key_forward(GTK_EVENT_CONTROLLER_KEY(m_pMenuKeyController), m_pEntry); + } + } + + return bDone; + } + + vcl::StringEntryIdentifier typeahead_getEntry(int nPos, OUString& out_entryText) const + { + int nEntryCount(get_count_including_mru()); + if (nPos >= nEntryCount) + nPos = 0; + out_entryText = get_text_including_mru(nPos); + + // vcl::StringEntryIdentifier does not allow for 0 values, but our position is 0-based + // => normalize + return reinterpret_cast(nPos + 1); + } + + static int typeahead_getEntryPos(vcl::StringEntryIdentifier entry) + { + // our pos is 0-based, but StringEntryIdentifier does not allow for a NULL + return reinterpret_cast(entry) - 1; + } + + int tree_view_get_cursor() const + { + int nRet = -1; +#if 0 + GtkTreePath* path; + gtk_tree_view_get_cursor(m_pTreeView, &path, nullptr); + if (path) + { + gint depth; + gint* indices = gtk_tree_path_get_indices_with_depth(path, &depth); + nRet = indices[depth-1]; + gtk_tree_path_free(path); + } +#endif + + return nRet; + } + + int get_selected_entry() const + { + if (m_bPopupActive) + return tree_view_get_cursor(); + else + return get_active_including_mru(); + } + + void set_typeahead_selected_entry(int nSelect) + { + set_active_including_mru(nSelect, true); + } + + virtual vcl::StringEntryIdentifier CurrentEntry(OUString& out_entryText) const override + { + int nCurrentPos = get_selected_entry(); + return typeahead_getEntry((nCurrentPos == -1) ? 0 : nCurrentPos, out_entryText); + } + + virtual vcl::StringEntryIdentifier NextEntry(vcl::StringEntryIdentifier currentEntry, OUString& out_entryText) const override + { + int nNextPos = typeahead_getEntryPos(currentEntry) + 1; + return typeahead_getEntry(nNextPos, out_entryText); + } + + virtual void SelectEntry(vcl::StringEntryIdentifier entry) override + { + int nSelect = typeahead_getEntryPos(entry); + if (nSelect == get_selected_entry()) + { + // ignore that. This method is a callback from the QuickSelectionEngine, which means the user attempted + // to select the given entry by typing its starting letters. No need to act. + return; + } + + // normalize + int nCount = get_count_including_mru(); + if (nSelect >= nCount) + nSelect = nCount ? nCount-1 : -1; + + set_typeahead_selected_entry(nSelect); + } + +#if 0 + static void signalGrabBroken(GtkWidget*, GdkEventGrabBroken *pEvent, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->grab_broken(pEvent); + } + + void grab_broken(const GdkEventGrabBroken *event) + { + if (event->grab_window == nullptr) + { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pToggleButton), false); + } + else if (!g_object_get_data(G_OBJECT(event->grab_window), "g-lo-InstancePopup")) // another LibreOffice popover took a grab + { + //try and regrab, so when we lose the grab to the menu of the color palette + //combobox we regain it so the color palette doesn't itself disappear on next + //click on the color palette combobox + do_grab(GTK_WIDGET(m_pMenuWindow)); + } + } + + static gboolean signalButtonPress(GtkWidget* pWidget, GdkEventButton* pEvent, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + return pThis->button_press(pWidget, pEvent); + } + + bool button_press(GtkWidget* pWidget, GdkEventButton* pEvent) + { + //we want to pop down if the button was pressed outside our popup + gdouble x = pEvent->x_root; + gdouble y = pEvent->y_root; + gint xoffset, yoffset; + gdk_window_get_root_origin(widget_get_surface(pWidget), &xoffset, &yoffset); + + GtkAllocation alloc; + gtk_widget_get_allocation(pWidget, &alloc); + xoffset += alloc.x; + yoffset += alloc.y; + + gtk_widget_get_allocation(GTK_WIDGET(m_pMenuWindow), &alloc); + gint x1 = alloc.x + xoffset; + gint y1 = alloc.y + yoffset; + gint x2 = x1 + alloc.width; + gint y2 = y1 + alloc.height; + + if (x > x1 && x < x2 && y > y1 && y < y2) + return false; + + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pToggleButton), false); + + return false; + } + + static gboolean signalMotion(GtkWidget*, GdkEventMotion*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->signal_motion(); + return false; + } + + void signal_motion() + { + // if hover-selection was disabled after pressing a key, then turn it back on again + if (!m_bHoverSelection && !m_bMouseInOverlayButton) + { + gtk_tree_view_set_hover_selection(m_pTreeView, true); + m_bHoverSelection = true; + } + } +#endif + + static void signalRowActivated(GtkTreeView*, GtkTreePath*, GtkTreeViewColumn*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->handle_row_activated(); + } + + void handle_row_activated() + { + m_bUserSelectEntry = true; + m_bChangedByMenu = true; + disable_notify_events(); + int nActive = get_active(); + if (m_pEditable) + gtk_editable_set_text(m_pEditable, OUStringToOString(get_text(nActive), RTL_TEXTENCODING_UTF8).getStr()); +#if 0 + else + tree_view_set_cursor(nActive); +#endif + enable_notify_events(); +// gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pToggleButton), false); + fire_signal_changed(); + update_mru(); + } + + void do_clear() + { + disable_notify_events(); + gtk_combo_box_set_row_separator_func(m_pComboBox, nullptr, nullptr, nullptr); + m_aSeparatorRows.clear(); + gtk_list_store_clear(GTK_LIST_STORE(m_pTreeModel)); + m_nMRUCount = 0; + enable_notify_events(); + } + + virtual int get_max_mru_count() const override + { + return m_nMaxMRUCount; + } + + virtual void set_max_mru_count(int nMaxMRUCount) override + { + m_nMaxMRUCount = nMaxMRUCount; + update_mru(); + } + + void update_mru() + { + int nMRUCount = m_nMRUCount; + + if (m_nMaxMRUCount) + { + OUString sActiveText = get_active_text(); + OUString sActiveId = get_active_id(); + insert_including_mru(0, sActiveText, &sActiveId, nullptr, nullptr); + ++m_nMRUCount; + + for (int i = 1; i < m_nMRUCount - 1; ++i) + { + if (get_text_including_mru(i) == sActiveText) + { + remove_including_mru(i); + --m_nMRUCount; + break; + } + } + } + + while (m_nMRUCount > m_nMaxMRUCount) + { + remove_including_mru(m_nMRUCount - 1); + --m_nMRUCount; + } + + if (m_nMRUCount && !nMRUCount) + insert_separator_including_mru(m_nMRUCount, "separator"); + else if (!m_nMRUCount && nMRUCount) + remove_including_mru(m_nMRUCount); // remove separator + } + + int get_count_including_mru() const + { + return gtk_tree_model_iter_n_children(m_pTreeModel, nullptr); + } + + int get_active_including_mru() const + { + return gtk_combo_box_get_active(m_pComboBox); + } + + void set_active_including_mru(int pos, bool bInteractive) + { + disable_notify_events(); + + gtk_combo_box_set_active(m_pComboBox, pos); + + m_bChangedByMenu = false; + enable_notify_events(); + + if (bInteractive && !m_bPopupActive) + signal_changed(); + } + + int find_text_including_mru(std::u16string_view rStr, bool bSearchMRU) const + { + return find(rStr, m_nTextCol, bSearchMRU); + } + + int find_id_including_mru(std::u16string_view rId, bool bSearchMRU) const + { + return find(rId, m_nIdCol, bSearchMRU); + } + + OUString get_text_including_mru(int pos) const + { + return get(pos, m_nTextCol); + } + + OUString get_id_including_mru(int pos) const + { + return get(pos, m_nIdCol); + } + + void set_id_including_mru(int pos, std::u16string_view rId) + { + set(pos, m_nIdCol, rId); + } + + void remove_including_mru(int pos) + { + disable_notify_events(); + GtkTreeIter iter; + gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos); + if (!m_aSeparatorRows.empty()) + { + bool bFound = false; + + GtkTreePath* pPath = gtk_tree_path_new_from_indices(pos, -1); + + for (auto aIter = m_aSeparatorRows.begin(); aIter != m_aSeparatorRows.end(); ++aIter) + { + GtkTreePath* seppath = gtk_tree_row_reference_get_path(aIter->get()); + if (seppath) + { + if (gtk_tree_path_compare(pPath, seppath) == 0) + bFound = true; + gtk_tree_path_free(seppath); + } + if (bFound) + { + m_aSeparatorRows.erase(aIter); + break; + } + } + + gtk_tree_path_free(pPath); + } + gtk_list_store_remove(GTK_LIST_STORE(m_pTreeModel), &iter); + enable_notify_events(); + } + + void insert_separator_including_mru(int pos, const OUString& rId) + { + disable_notify_events(); + GtkTreeIter iter; + if (!gtk_combo_box_get_row_separator_func(m_pComboBox)) + gtk_combo_box_set_row_separator_func(m_pComboBox, separatorFunction, this, nullptr); + insert_row(GTK_LIST_STORE(m_pTreeModel), iter, pos, &rId, u"", nullptr, nullptr); + GtkTreePath* pPath = gtk_tree_path_new_from_indices(pos, -1); + m_aSeparatorRows.emplace_back(gtk_tree_row_reference_new(m_pTreeModel, pPath)); + gtk_tree_path_free(pPath); + enable_notify_events(); + } + + void insert_including_mru(int pos, std::u16string_view rText, const OUString* pId, const OUString* pIconName, const VirtualDevice* pImageSurface) + { + disable_notify_events(); + GtkTreeIter iter; + insert_row(GTK_LIST_STORE(m_pTreeModel), iter, pos, pId, rText, pIconName, pImageSurface); + enable_notify_events(); + } + +#if 0 + static gboolean signalGetChildPosition(GtkOverlay*, GtkWidget*, GdkRectangle* pAllocation, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + return pThis->signal_get_child_position(pAllocation); + } + + bool signal_get_child_position(GdkRectangle* pAllocation) + { + if (!gtk_widget_get_visible(GTK_WIDGET(m_pOverlayButton))) + return false; + if (!gtk_widget_get_realized(GTK_WIDGET(m_pTreeView))) + return false; + int nRow = find_id_including_mru(m_sMenuButtonRow, true); + if (nRow == -1) + return false; + + gtk_widget_get_preferred_width(GTK_WIDGET(m_pOverlayButton), &pAllocation->width, nullptr); + + GtkTreePath* pPath = gtk_tree_path_new_from_indices(nRow, -1); + GList* pColumns = gtk_tree_view_get_columns(m_pTreeView); + tools::Rectangle aRect = get_row_area(m_pTreeView, pColumns, pPath); + gtk_tree_path_free(pPath); + g_list_free(pColumns); + + pAllocation->x = aRect.Right() - pAllocation->width; + pAllocation->y = aRect.Top(); + pAllocation->height = aRect.GetHeight(); + + return true; + } + + static gboolean signalOverlayButtonCrossing(GtkWidget*, GdkEventCrossing* pEvent, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->signal_overlay_button_crossing(pEvent->type == GDK_ENTER_NOTIFY); + return false; + } + + void signal_overlay_button_crossing(bool bEnter) + { + m_bMouseInOverlayButton = bEnter; + if (!bEnter) + return; + + if (m_bHoverSelection) + { + // once toggled button is pressed, turn off hover selection until + // mouse leaves the overlay button + gtk_tree_view_set_hover_selection(m_pTreeView, false); + m_bHoverSelection = false; + } + int nRow = find_id_including_mru(m_sMenuButtonRow, true); + assert(nRow != -1); + tree_view_set_cursor(nRow); // select the buttons row + } +#endif + + int include_mru(int pos) + { + if (m_nMRUCount && pos != -1) + pos += (m_nMRUCount + 1); + return pos; + } + +public: + GtkInstanceComboBox(GtkComboBox* pComboBox, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pComboBox), pBuilder, bTakeOwnership) + , m_pComboBox(pComboBox) +// , m_pOverlay(GTK_OVERLAY(gtk_builder_get_object(pComboBuilder, "overlay"))) +// , m_pTreeView(GTK_TREE_VIEW(gtk_builder_get_object(pComboBuilder, "treeview"))) +// , m_pOverlayButton(GTK_MENU_BUTTON(gtk_builder_get_object(pComboBuilder, "overlaybutton"))) + , m_pMenuWindow(nullptr) + , m_pTreeModel(gtk_combo_box_get_model(pComboBox)) + , m_pButtonTextRenderer(nullptr) +// , m_pToggleButton(GTK_WIDGET(gtk_builder_get_object(pComboBuilder, "button"))) + , m_pEntry(GTK_IS_ENTRY(gtk_combo_box_get_child(pComboBox)) ? gtk_combo_box_get_child(pComboBox) : nullptr) + , m_pEditable(GTK_EDITABLE(m_pEntry)) + , m_aCustomFont(m_pWidget) +// , m_pCellView(nullptr) + , m_aQuickSelectionEngine(*this) +// , m_bHoverSelection(false) +// , m_bMouseInOverlayButton(false) + , m_bPopupActive(false) + , m_bAutoComplete(false) + , m_bAutoCompleteCaseSensitive(false) + , m_bChangedByMenu(false) + , m_bCustomRenderer(false) + , m_bUserSelectEntry(false) + , m_nTextCol(gtk_combo_box_get_entry_text_column(pComboBox)) + , m_nIdCol(gtk_combo_box_get_id_column(pComboBox)) +// , m_nToggleFocusInSignalId(0) +// , m_nToggleFocusOutSignalId(0) +// , m_nRowActivatedSignalId(g_signal_connect(m_pTreeView, "row-activated", G_CALLBACK(signalRowActivated), this)) + , m_nChangedSignalId(g_signal_connect(m_pComboBox, "changed", G_CALLBACK(signalChanged), this)) + , m_nPopupShownSignalId(g_signal_connect(m_pComboBox, "notify::popup-shown", G_CALLBACK(signalPopupToggled), this)) + , m_nAutoCompleteIdleId(0) +// , m_nNonCustomLineHeight(-1) + , m_nPrePopupCursorPos(-1) + , m_nMRUCount(0) + , m_nMaxMRUCount(0) + { + for (GtkWidget* pChild = gtk_widget_get_first_child(GTK_WIDGET(m_pComboBox)); + pChild; pChild = gtk_widget_get_next_sibling(pChild)) + { + if (GTK_IS_POPOVER(pChild)) + { + m_pMenuWindow = pChild; + break; + } + } + SAL_WARN_IF(!m_pMenuWindow, "vcl.gtk", "GtkInstanceComboBox: couldn't find popup menu"); + + bool bHasEntry = gtk_combo_box_get_has_entry(m_pComboBox); + bool bPixbufUsedSurface = gtk_tree_model_get_n_columns(m_pTreeModel) == 4; + + bool bFindButtonTextRenderer = !bHasEntry; + GtkCellLayout* pCellLayout = GTK_CELL_LAYOUT(m_pComboBox); + GList* cells = gtk_cell_layout_get_cells(pCellLayout); + guint i = g_list_length(cells) - 1;; + // reorder the cell renderers + for (GList* pRenderer = g_list_first(cells); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + gtk_cell_layout_reorder(pCellLayout, pCellRenderer, i--); + if (bFindButtonTextRenderer) + { + m_pButtonTextRenderer = pCellRenderer; + bFindButtonTextRenderer = false; + } + } + + // Seeing as GtkCellRendererPixbuf no longer takes a surface, then insert our own replacement + // to render that instead here + if (bPixbufUsedSurface) + { + GtkCellRenderer* pSurfaceRenderer = surface_cell_renderer_new(); + gtk_cell_layout_pack_start(pCellLayout, pSurfaceRenderer, false); + gtk_cell_layout_reorder(pCellLayout, pSurfaceRenderer, 0); + gtk_cell_layout_set_attributes(pCellLayout, pSurfaceRenderer, "surface", 3, nullptr); + } + + if (bHasEntry) + { + m_bAutoComplete = true; + m_nEntryInsertTextSignalId = g_signal_connect(m_pEditable, "insert-text", G_CALLBACK(signalEntryInsertText), this); + m_nEntryActivateSignalId = g_signal_connect(m_pEntry, "activate", G_CALLBACK(signalEntryActivate), this); + m_pEntryFocusController = GTK_EVENT_CONTROLLER(gtk_event_controller_focus_new()); + m_nEntryFocusInSignalId = g_signal_connect(m_pEntryFocusController, "enter", G_CALLBACK(signalEntryFocusIn), this); + m_nEntryFocusOutSignalId = g_signal_connect(m_pEntryFocusController, "leave", G_CALLBACK(signalEntryFocusOut), this); + gtk_widget_add_controller(m_pEntry, m_pEntryFocusController); + m_pEntryKeyController = GTK_EVENT_CONTROLLER(gtk_event_controller_key_new()); + m_nEntryKeyPressEventSignalId = g_signal_connect(m_pEntryKeyController, "key-pressed", G_CALLBACK(signalEntryKeyPress), this); + gtk_widget_add_controller(m_pEntry, m_pEntryKeyController); + m_nKeyPressEventSignalId = 0; + m_pKeyController = nullptr; + } + else + { + m_nEntryInsertTextSignalId = 0; + m_nEntryActivateSignalId = 0; + m_pEntryFocusController = nullptr; + m_nEntryFocusInSignalId = 0; + m_nEntryFocusOutSignalId = 0; + m_pEntryKeyController = nullptr; + m_nEntryKeyPressEventSignalId = 0; + m_pKeyController = GTK_EVENT_CONTROLLER(gtk_event_controller_key_new()); + m_nKeyPressEventSignalId = g_signal_connect(m_pKeyController, "key-pressed", G_CALLBACK(signalKeyPress), this); + gtk_widget_add_controller(GTK_WIDGET(m_pComboBox), m_pKeyController); + } + +// g_signal_connect(m_pMenuWindow, "grab-broken-event", G_CALLBACK(signalGrabBroken), this); +// g_signal_connect(m_pMenuWindow, "button-press-event", G_CALLBACK(signalButtonPress), this); +// g_signal_connect(m_pMenuWindow, "motion-notify-event", G_CALLBACK(signalMotion), this); + + // support typeahead for the menu itself, typing into the menu will + // select via the vcl selection engine, a matching entry. + if (m_pMenuWindow) + { + m_pMenuKeyController = GTK_EVENT_CONTROLLER(gtk_event_controller_key_new()); + g_signal_connect(m_pMenuKeyController, "key-pressed", G_CALLBACK(signalKeyPress), this); + gtk_widget_add_controller(m_pMenuWindow, m_pMenuKeyController); + } + else + m_pMenuKeyController = nullptr; +#if 0 + g_signal_connect(m_pOverlay, "get-child-position", G_CALLBACK(signalGetChildPosition), this); + gtk_overlay_add_overlay(m_pOverlay, GTK_WIDGET(m_pOverlayButton)); + g_signal_connect(m_pOverlayButton, "leave-notify-event", G_CALLBACK(signalOverlayButtonCrossing), this); + g_signal_connect(m_pOverlayButton, "enter-notify-event", G_CALLBACK(signalOverlayButtonCrossing), this); +#endif + } + + virtual int get_active() const override + { + int nActive = get_active_including_mru(); + if (nActive == -1) + return -1; + + if (m_nMRUCount) + { + if (nActive < m_nMRUCount) + nActive = find_text(get_text_including_mru(nActive)); + else + nActive -= (m_nMRUCount + 1); + } + + return nActive; + } + + virtual OUString get_active_id() const override + { + int nActive = get_active(); + return nActive != -1 ? get_id(nActive) : OUString(); + } + + virtual void set_active_id(const OUString& rStr) override + { + set_active(find_id(rStr)); + m_bChangedByMenu = false; + } + + virtual void set_size_request(int nWidth, int nHeight) override + { + if (m_pButtonTextRenderer) + { + // tweak the cell render to get a narrower size to stick + if (nWidth != -1) + { + // this bit isn't great, I really want to be able to ellipse the text in the comboboxtext itself and let + // the popup menu render them in full, in the interim ellipse both of them + g_object_set(G_OBJECT(m_pButtonTextRenderer), "ellipsize", PANGO_ELLIPSIZE_MIDDLE, nullptr); + + // to find out how much of the width of the combobox belongs to the cell, set + // the cell and widget to the min cell width and see what the difference is + int min; + gtk_cell_renderer_get_preferred_width(m_pButtonTextRenderer, m_pWidget, &min, nullptr); + gtk_cell_renderer_set_fixed_size(m_pButtonTextRenderer, min, -1); + gtk_widget_set_size_request(m_pWidget, min, -1); + int nNonCellWidth = get_preferred_size().Width() - min; + + int nCellWidth = nWidth - nNonCellWidth; + if (nCellWidth >= 0) + { + // now set the cell to the max width which it can be within the + // requested widget width + gtk_cell_renderer_set_fixed_size(m_pButtonTextRenderer, nWidth - nNonCellWidth, -1); + } + } + else + { + g_object_set(G_OBJECT(m_pButtonTextRenderer), "ellipsize", PANGO_ELLIPSIZE_NONE, nullptr); + gtk_cell_renderer_set_fixed_size(m_pButtonTextRenderer, -1, -1); + } + } + + gtk_widget_set_size_request(m_pWidget, nWidth, nHeight); + } + + virtual void set_active(int pos) override + { + set_active_including_mru(include_mru(pos), false); + } + + virtual OUString get_active_text() const override + { + if (m_pEditable) + { + const gchar* pText = gtk_editable_get_text(m_pEditable); + return OUString(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + } + + int nActive = get_active(); + if (nActive == -1) + return OUString(); + + return get_text(nActive); + } + + virtual OUString get_text(int pos) const override + { + if (m_nMRUCount) + pos += (m_nMRUCount + 1); + return get_text_including_mru(pos); + } + + virtual OUString get_id(int pos) const override + { + if (m_nMRUCount) + pos += (m_nMRUCount + 1); + return get_id_including_mru(pos); + } + + virtual void set_id(int pos, const OUString& rId) override + { + if (m_nMRUCount) + pos += (m_nMRUCount + 1); + set_id_including_mru(pos, rId); + } + + virtual void insert_vector(const std::vector& rItems, bool bKeepExisting) override + { + freeze(); + + int nInsertionPoint; + if (!bKeepExisting) + { + clear(); + nInsertionPoint = 0; + } + else + nInsertionPoint = get_count(); + + GtkTreeIter iter; + // tdf#125241 inserting backwards is faster + for (auto aI = rItems.rbegin(); aI != rItems.rend(); ++aI) + { + const auto& rItem = *aI; + insert_row(GTK_LIST_STORE(m_pTreeModel), iter, nInsertionPoint, rItem.sId.isEmpty() ? nullptr : &rItem.sId, + rItem.sString, rItem.sImage.isEmpty() ? nullptr : &rItem.sImage, nullptr); + } + + thaw(); + } + + virtual void remove(int pos) override + { + if (m_nMRUCount) + pos += (m_nMRUCount + 1); + remove_including_mru(pos); + } + + virtual void insert(int pos, const OUString& rText, const OUString* pId, const OUString* pIconName, VirtualDevice* pImageSurface) override + { + insert_including_mru(include_mru(pos), rText, pId, pIconName, pImageSurface); + } + + virtual void insert_separator(int pos, const OUString& rId) override + { + pos = pos == -1 ? get_count() : pos; + if (m_nMRUCount) + pos += (m_nMRUCount + 1); + insert_separator_including_mru(pos, rId); + } + + virtual int get_count() const override + { + int nCount = get_count_including_mru(); + if (m_nMRUCount) + nCount -= (m_nMRUCount + 1); + return nCount; + } + + virtual int find_text(const OUString& rStr) const override + { + int nPos = find_text_including_mru(rStr, false); + if (nPos != -1 && m_nMRUCount) + nPos -= (m_nMRUCount + 1); + return nPos; + } + + virtual int find_id(const OUString& rId) const override + { + int nPos = find_id_including_mru(rId, false); + if (nPos != -1 && m_nMRUCount) + nPos -= (m_nMRUCount + 1); + return nPos; + } + + virtual void clear() override + { + do_clear(); + } + + virtual void make_sorted() override + { + m_xSorter.reset(new comphelper::string::NaturalStringSorter( + ::comphelper::getProcessComponentContext(), + Application::GetSettings().GetUILanguageTag().getLocale())); + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_set_sort_column_id(pSortable, m_nTextCol, GTK_SORT_ASCENDING); + gtk_tree_sortable_set_sort_func(pSortable, m_nTextCol, default_sort_func, m_xSorter.get(), nullptr); + } + + virtual bool has_entry() const override + { + return gtk_combo_box_get_has_entry(m_pComboBox); + } + + virtual void set_entry_message_type(weld::EntryMessageType eType) override + { + assert(m_pEntry); + ::set_entry_message_type(GTK_ENTRY(m_pEntry), eType); + } + + virtual void set_entry_text(const OUString& rText) override + { + assert(m_pEditable); + disable_notify_events(); + gtk_editable_set_text(m_pEditable, OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); + enable_notify_events(); + } + + virtual void set_entry_width_chars(int nChars) override + { + assert(m_pEditable); + disable_notify_events(); + gtk_editable_set_width_chars(m_pEditable, nChars); + gtk_editable_set_max_width_chars(m_pEditable, nChars); + enable_notify_events(); + } + + virtual void set_entry_max_length(int nChars) override + { + assert(m_pEntry); + disable_notify_events(); + gtk_entry_set_max_length(GTK_ENTRY(m_pEntry), nChars); + enable_notify_events(); + } + + virtual void select_entry_region(int nStartPos, int nEndPos) override + { + assert(m_pEditable); + disable_notify_events(); + gtk_editable_select_region(m_pEditable, nStartPos, nEndPos); + enable_notify_events(); + } + + virtual bool get_entry_selection_bounds(int& rStartPos, int &rEndPos) override + { + assert(m_pEditable); + return gtk_editable_get_selection_bounds(m_pEditable, &rStartPos, &rEndPos); + } + + virtual void set_entry_completion(bool bEnable, bool bCaseSensitive) override + { + m_bAutoComplete = bEnable; + m_bAutoCompleteCaseSensitive = bCaseSensitive; + } + + virtual void set_entry_placeholder_text(const OUString& rText) override + { + assert(m_pEntry); + gtk_entry_set_placeholder_text(GTK_ENTRY(m_pEntry), rText.toUtf8().getStr()); + } + + virtual void set_entry_editable(bool bEditable) override + { + assert(m_pEditable); + gtk_editable_set_editable(m_pEditable, bEditable); + } + + virtual void cut_entry_clipboard() override + { + assert(m_pEntry); + gtk_widget_activate_action(m_pEntry, "cut.clipboard", nullptr); + } + + virtual void copy_entry_clipboard() override + { + assert(m_pEntry); + gtk_widget_activate_action(m_pEntry, "copy.clipboard", nullptr); + } + + virtual void paste_entry_clipboard() override + { + assert(m_pEntry); + gtk_widget_activate_action(m_pEntry, "paste.clipboard", nullptr); + } + + virtual void set_font(const vcl::Font& rFont) override + { + m_aCustomFont.use_custom_font(&rFont, u"combobox"); + } + + virtual vcl::Font get_font() override + { + if (const vcl::Font* pFont = m_aCustomFont.get_custom_font()) + return *pFont; + return GtkInstanceWidget::get_font(); + } + + virtual void set_entry_font(const vcl::Font& rFont) override + { + m_xEntryFont = rFont; + assert(m_pEntry); + PangoAttrList* pOrigList = gtk_entry_get_attributes(GTK_ENTRY(m_pEntry)); + PangoAttrList* pAttrList = pOrigList ? pango_attr_list_copy(pOrigList) : pango_attr_list_new(); + update_attr_list(pAttrList, rFont); + gtk_entry_set_attributes(GTK_ENTRY(m_pEntry), pAttrList); + pango_attr_list_unref(pAttrList); + } + + virtual vcl::Font get_entry_font() override + { + if (m_xEntryFont) + return *m_xEntryFont; + assert(m_pEntry); + PangoContext* pContext = gtk_widget_get_pango_context(m_pEntry); + return pango_to_vcl(pango_context_get_font_description(pContext), + Application::GetSettings().GetUILanguageTag().getLocale()); + } + + virtual void disable_notify_events() override + { + if (m_pEditable) + { + g_signal_handler_block(m_pEditable, m_nEntryInsertTextSignalId); + g_signal_handler_block(m_pEntry, m_nEntryActivateSignalId); + g_signal_handler_block(m_pEntryFocusController, m_nEntryFocusInSignalId); + g_signal_handler_block(m_pEntryFocusController, m_nEntryFocusOutSignalId); + g_signal_handler_block(m_pEntryKeyController, m_nEntryKeyPressEventSignalId); + } + else + g_signal_handler_block(m_pKeyController, m_nKeyPressEventSignalId); + +// if (m_nToggleFocusInSignalId) +// g_signal_handler_block(m_pToggleButton, m_nToggleFocusInSignalId); +// if (m_nToggleFocusOutSignalId) +// g_signal_handler_block(m_pToggleButton, m_nToggleFocusOutSignalId); +// g_signal_handler_block(m_pTreeView, m_nRowActivatedSignalId); + g_signal_handler_block(m_pComboBox, m_nPopupShownSignalId); + g_signal_handler_block(m_pComboBox, m_nChangedSignalId); + GtkInstanceWidget::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceWidget::enable_notify_events(); + g_signal_handler_unblock(m_pComboBox, m_nChangedSignalId); + g_signal_handler_unblock(m_pComboBox, m_nPopupShownSignalId); +// g_signal_handler_unblock(m_pTreeView, m_nRowActivatedSignalId); +// if (m_nToggleFocusInSignalId) +// g_signal_handler_unblock(m_pToggleButton, m_nToggleFocusInSignalId); +// if (m_nToggleFocusOutSignalId) +// g_signal_handler_unblock(m_pToggleButton, m_nToggleFocusOutSignalId); + if (m_pEditable) + { + g_signal_handler_unblock(m_pEntry, m_nEntryActivateSignalId); + g_signal_handler_unblock(m_pEntryFocusController, m_nEntryFocusInSignalId); + g_signal_handler_unblock(m_pEntryFocusController, m_nEntryFocusOutSignalId); + g_signal_handler_unblock(m_pEntryKeyController, m_nEntryKeyPressEventSignalId); + g_signal_handler_unblock(m_pEditable, m_nEntryInsertTextSignalId); + } + else + g_signal_handler_unblock(m_pKeyController, m_nKeyPressEventSignalId); + } + + virtual void freeze() override + { + disable_notify_events(); + bool bIsFirstFreeze = IsFirstFreeze(); + GtkInstanceWidget::freeze(); + if (bIsFirstFreeze) + { + g_object_ref(m_pTreeModel); +// gtk_tree_view_set_model(m_pTreeView, nullptr); + g_object_freeze_notify(G_OBJECT(m_pTreeModel)); + if (m_xSorter) + { + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_set_sort_column_id(pSortable, GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, GTK_SORT_ASCENDING); + } + } + enable_notify_events(); + } + + virtual void thaw() override + { + disable_notify_events(); + if (IsLastThaw()) + { + if (m_xSorter) + { + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_set_sort_column_id(pSortable, m_nTextCol, GTK_SORT_ASCENDING); + } + g_object_thaw_notify(G_OBJECT(m_pTreeModel)); +// gtk_tree_view_set_model(m_pTreeView, m_pTreeModel); + g_object_unref(m_pTreeModel); + } + GtkInstanceWidget::thaw(); + enable_notify_events(); + } + + virtual bool get_popup_shown() const override + { + return m_bPopupActive; + } + + virtual void connect_focus_in(const Link& rLink) override + { +// if (!m_nToggleFocusInSignalId) +// m_nToggleFocusInSignalId = g_signal_connect_after(m_pToggleButton, "focus-in-event", G_CALLBACK(signalFocusIn), this); + GtkInstanceWidget::connect_focus_in(rLink); + } + + virtual void connect_focus_out(const Link& rLink) override + { +// if (!m_nToggleFocusOutSignalId) +// m_nToggleFocusOutSignalId = g_signal_connect_after(m_pToggleButton, "focus-out-event", G_CALLBACK(signalFocusOut), this); + GtkInstanceWidget::connect_focus_out(rLink); + } + + virtual void grab_focus() override + { + if (has_focus()) + return; + if (m_pEntry) + gtk_widget_grab_focus(m_pEntry); + else + { +// gtk_widget_grab_focus(m_pToggleButton); + gtk_widget_grab_focus(GTK_WIDGET(m_pComboBox)); + } + } + + virtual bool has_focus() const override + { + if (m_pEntry && gtk_widget_has_focus(m_pEntry)) + return true; + +// if (gtk_widget_has_focus(m_pToggleButton)) +// return true; + +#if 0 + if (gtk_widget_get_visible(GTK_WIDGET(m_pMenuWindow))) + { + if (gtk_widget_has_focus(GTK_WIDGET(m_pOverlayButton)) || gtk_widget_has_focus(GTK_WIDGET(m_pTreeView))) + return true; + } +#endif + + return GtkInstanceWidget::has_focus(); + } + + virtual bool changed_by_direct_pick() const override + { + return m_bChangedByMenu; + } + + virtual void set_custom_renderer(bool bOn) override + { + if (bOn == m_bCustomRenderer) + return; +#if 0 + GList* pColumns = gtk_tree_view_get_columns(m_pTreeView); + // keep the original height around for optimal popup height calculation + m_nNonCustomLineHeight = bOn ? ::get_height_row(m_pTreeView, pColumns) : -1; + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pColumns->data); + gtk_cell_layout_clear(GTK_CELL_LAYOUT(pColumn)); + if (bOn) + { + GtkCellRenderer *pRenderer = custom_cell_renderer_new(); + GValue value = G_VALUE_INIT; + g_value_init(&value, G_TYPE_POINTER); + g_value_set_pointer(&value, static_cast(this)); + g_object_set_property(G_OBJECT(pRenderer), "instance", &value); + gtk_tree_view_column_pack_start(pColumn, pRenderer, true); + gtk_tree_view_column_add_attribute(pColumn, pRenderer, "text", m_nTextCol); + gtk_tree_view_column_add_attribute(pColumn, pRenderer, "id", m_nIdCol); + } + else + { + GtkCellRenderer *pRenderer = gtk_cell_renderer_text_new(); + gtk_tree_view_column_pack_start(pColumn, pRenderer, true); + gtk_tree_view_column_add_attribute(pColumn, pRenderer, "text", m_nTextCol); + } + g_list_free(pColumns); + m_bCustomRenderer = bOn; +#endif + } + + void call_signal_custom_render(VirtualDevice& rOutput, const tools::Rectangle& rRect, bool bSelected, const OUString& rId) + { + signal_custom_render(rOutput, rRect, bSelected, rId); + } + + Size call_signal_custom_get_size(VirtualDevice& rOutput) + { + return signal_custom_get_size(rOutput); + } + + VclPtr create_render_virtual_device() const override + { + return create_virtual_device(); + } + + virtual void set_item_menu(const OUString& rIdent, weld::Menu* pMenu) override + { +#if 0 + m_xCustomMenuButtonHelper.reset(); + GtkInstanceMenu* pPopoverWidget = dynamic_cast(pMenu); + GtkWidget* pMenuWidget = GTK_WIDGET(pPopoverWidget ? pPopoverWidget->getMenu() : nullptr); + gtk_menu_button_set_popup(m_pOverlayButton, pMenuWidget); + gtk_widget_set_visible(GTK_WIDGET(m_pOverlayButton), pMenuWidget != nullptr); + gtk_widget_queue_resize_no_redraw(GTK_WIDGET(m_pOverlayButton)); // force location recalc + if (pMenuWidget) + m_xCustomMenuButtonHelper.reset(new CustomRenderMenuButtonHelper(GTK_MENU(pMenuWidget), GTK_TOGGLE_BUTTON(m_pToggleButton))); + m_sMenuButtonRow = rIdent; +#else + (void)rIdent; (void)pMenu; +#endif + } + + OUString get_mru_entries() const override + { + const sal_Unicode cSep = ';'; + + OUStringBuffer aEntries; + for (sal_Int32 n = 0; n < m_nMRUCount; n++) + { + aEntries.append(get_text_including_mru(n)); + if (n < m_nMRUCount - 1) + aEntries.append(cSep); + } + return aEntries.makeStringAndClear(); + } + + virtual void set_mru_entries(const OUString& rEntries) override + { + const sal_Unicode cSep = ';'; + + // Remove old MRU entries + for (sal_Int32 n = m_nMRUCount; n;) + remove_including_mru(--n); + + sal_Int32 nMRUCount = 0; + sal_Int32 nIndex = 0; + do + { + OUString aEntry = rEntries.getToken(0, cSep, nIndex); + // Accept only existing entries + int nPos = find_text(aEntry); + if (nPos != -1) + { + OUString sId = get_id(nPos); + insert_including_mru(0, aEntry, &sId, nullptr, nullptr); + ++nMRUCount; + } + } + while (nIndex >= 0); + + if (nMRUCount && !m_nMRUCount) + insert_separator_including_mru(nMRUCount, "separator"); + else if (!nMRUCount && m_nMRUCount) + remove_including_mru(m_nMRUCount); // remove separator + + m_nMRUCount = nMRUCount; + } + + int get_menu_button_width() const override + { +#if 0 + bool bVisible = gtk_widget_get_visible(GTK_WIDGET(m_pOverlayButton)); + if (!bVisible) + gtk_widget_set_visible(GTK_WIDGET(m_pOverlayButton), true); + gint nWidth; + gtk_widget_get_preferred_width(GTK_WIDGET(m_pOverlayButton), &nWidth, nullptr); + if (!bVisible) + gtk_widget_set_visible(GTK_WIDGET(m_pOverlayButton), false); + return nWidth; +#else + return 0; +#endif + } + + virtual ~GtkInstanceComboBox() override + { +// m_xCustomMenuButtonHelper.reset(); + do_clear(); + if (m_nAutoCompleteIdleId) + g_source_remove(m_nAutoCompleteIdleId); + if (m_pEditable) + { + g_signal_handler_disconnect(m_pEditable, m_nEntryInsertTextSignalId); + g_signal_handler_disconnect(m_pEntry, m_nEntryActivateSignalId); + g_signal_handler_disconnect(m_pEntryFocusController, m_nEntryFocusInSignalId); + g_signal_handler_disconnect(m_pEntryFocusController, m_nEntryFocusOutSignalId); + g_signal_handler_disconnect(m_pEntryKeyController, m_nEntryKeyPressEventSignalId); + } + else + g_signal_handler_disconnect(m_pKeyController, m_nKeyPressEventSignalId); +// if (m_nToggleFocusInSignalId) +// g_signal_handler_disconnect(m_pToggleButton, m_nToggleFocusInSignalId); +// if (m_nToggleFocusOutSignalId) +// g_signal_handler_disconnect(m_pToggleButton, m_nToggleFocusOutSignalId); +// g_signal_handler_disconnect(m_pTreeView, m_nRowActivatedSignalId); + g_signal_handler_disconnect(m_pComboBox, m_nPopupShownSignalId); + g_signal_handler_disconnect(m_pComboBox, m_nChangedSignalId); + +// gtk_tree_view_set_model(m_pTreeView, nullptr); + + } +}; + +#else + +class GtkInstanceComboBox : public GtkInstanceContainer, public vcl::ISearchableStringList, public virtual weld::ComboBox +{ +private: + GtkBuilder* m_pComboBuilder; + GtkComboBox* m_pComboBox; + GtkOverlay* m_pOverlay; + GtkTreeView* m_pTreeView; + GtkMenuButton* m_pOverlayButton; // button that the StyleDropdown uses on an active row + GtkWindow* m_pMenuWindow; + GtkTreeModel* m_pTreeModel; + GtkCellRenderer* m_pButtonTextRenderer; + GtkCellRenderer* m_pMenuTextRenderer; + GtkWidget* m_pToggleButton; + GtkWidget* m_pEntry; + GtkCellView* m_pCellView; + WidgetFont m_aCustomFont; + std::unique_ptr m_xCustomMenuButtonHelper; + std::optional m_xEntryFont; + std::unique_ptr m_xSorter; + vcl::QuickSelectionEngine m_aQuickSelectionEngine; + std::vector> m_aSeparatorRows; + OUString m_sMenuButtonRow; + bool m_bHoverSelection; + bool m_bMouseInOverlayButton; + bool m_bPopupActive; + bool m_bAutoComplete; + bool m_bAutoCompleteCaseSensitive; + bool m_bChangedByMenu; + bool m_bCustomRenderer; + bool m_bActivateCalled; + gint m_nTextCol; + gint m_nIdCol; + gulong m_nToggleFocusInSignalId; + gulong m_nToggleFocusOutSignalId; + gulong m_nRowActivatedSignalId; + gulong m_nChangedSignalId; + gulong m_nPopupShownSignalId; + gulong m_nKeyPressEventSignalId; + gulong m_nEntryInsertTextSignalId; + gulong m_nEntryActivateSignalId; + gulong m_nEntryFocusInSignalId; + gulong m_nEntryFocusOutSignalId; + gulong m_nEntryKeyPressEventSignalId; + gulong m_nEntryPopulatePopupMenuSignalId; + guint m_nAutoCompleteIdleId; + gint m_nNonCustomLineHeight; + gint m_nPrePopupCursorPos; + int m_nMRUCount; + int m_nMaxMRUCount; + + static gboolean idleAutoComplete(gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->auto_complete(); + return false; + } + + void auto_complete() + { + m_nAutoCompleteIdleId = 0; + OUString aStartText = get_active_text(); + int nStartPos, nEndPos; + get_entry_selection_bounds(nStartPos, nEndPos); + int nMaxSelection = std::max(nStartPos, nEndPos); + if (nMaxSelection != aStartText.getLength()) + return; + + disable_notify_events(); + int nActive = get_active(); + int nStart = nActive; + + if (nStart == -1) + nStart = 0; + + int nPos = -1; + + int nZeroRow = 0; + if (m_nMRUCount) + nZeroRow += (m_nMRUCount + 1); + + if (!m_bAutoCompleteCaseSensitive) + { + // Try match case insensitive from current position + nPos = starts_with(m_pTreeModel, aStartText, 0, nStart, false); + if (nPos == -1 && nStart != 0) + { + // Try match case insensitive, but from start + nPos = starts_with(m_pTreeModel, aStartText, 0, nZeroRow, false); + } + } + + if (nPos == -1) + { + // Try match case sensitive from current position + nPos = starts_with(m_pTreeModel, aStartText, 0, nStart, true); + if (nPos == -1 && nStart != 0) + { + // Try match case sensitive, but from start + nPos = starts_with(m_pTreeModel, aStartText, 0, nZeroRow, true); + } + } + + if (nPos != -1) + { + OUString aText = get_text_including_mru(nPos); + if (aText != aStartText) + { + SolarMutexGuard aGuard; + set_active_including_mru(nPos, true); + } + select_entry_region(aText.getLength(), aStartText.getLength()); + } + enable_notify_events(); + } + + static void signalEntryInsertText(GtkEntry* pEntry, const gchar* pNewText, gint nNewTextLength, + gint* position, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_entry_insert_text(pEntry, pNewText, nNewTextLength, position); + } + + void signal_entry_insert_text(GtkEntry* pEntry, const gchar* pNewText, gint nNewTextLength, gint* position) + { + // first filter inserted text + if (m_aEntryInsertTextHdl.IsSet()) + { + OUString sText(pNewText, nNewTextLength, RTL_TEXTENCODING_UTF8); + const bool bContinue = m_aEntryInsertTextHdl.Call(sText); + if (bContinue && !sText.isEmpty()) + { + OString sFinalText(OUStringToOString(sText, RTL_TEXTENCODING_UTF8)); + g_signal_handlers_block_by_func(pEntry, reinterpret_cast(signalEntryInsertText), this); + gtk_editable_insert_text(GTK_EDITABLE(pEntry), sFinalText.getStr(), sFinalText.getLength(), position); + g_signal_handlers_unblock_by_func(pEntry, reinterpret_cast(signalEntryInsertText), this); + } + g_signal_stop_emission_by_name(pEntry, "insert-text"); + } + if (m_bAutoComplete) + { + // now check for autocompletes + if (m_nAutoCompleteIdleId) + g_source_remove(m_nAutoCompleteIdleId); + m_nAutoCompleteIdleId = g_idle_add(idleAutoComplete, this); + } + } + + static void signalChanged(GtkEntry*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->fire_signal_changed(); + } + + void fire_signal_changed() + { + signal_changed(); + m_bChangedByMenu = false; + } + + static void signalPopupToggled(GtkToggleButton* /*pToggleButton*/, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->signal_popup_toggled(); + } + + int get_popup_height(gint& rPopupWidth) + { + const StyleSettings& rSettings = Application::GetSettings().GetStyleSettings(); + + int nMaxRows = rSettings.GetListBoxMaximumLineCount(); + bool bAddScrollWidth = false; + int nRows = get_count_including_mru(); + if (nMaxRows < nRows) + { + nRows = nMaxRows; + bAddScrollWidth = true; + } + + GList* pColumns = gtk_tree_view_get_columns(m_pTreeView); + gint nRowHeight = get_height_row(m_pTreeView, pColumns); + g_list_free(pColumns); + + gint nSeparatorHeight = get_height_row_separator(m_pTreeView); + gint nHeight = get_height_rows(nRowHeight, nSeparatorHeight, nRows); + + // if we're using a custom renderer, limit the height to the height nMaxRows would be + // for a normal renderer, and then round down to how many custom rows fit in that + // space + if (m_nNonCustomLineHeight != -1 && nRowHeight) + { + gint nNormalHeight = get_height_rows(m_nNonCustomLineHeight, nSeparatorHeight, nMaxRows); + if (nHeight > nNormalHeight) + { + gint nRowsOnly = nNormalHeight - get_height_rows(0, nSeparatorHeight, nMaxRows); + gint nCustomRows = (nRowsOnly + (nRowHeight - 1)) / nRowHeight; + nHeight = get_height_rows(nRowHeight, nSeparatorHeight, nCustomRows); + } + } + + if (bAddScrollWidth) + rPopupWidth += rSettings.GetScrollBarSize(); + + return nHeight; + } + + void menu_toggled() + { + if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(m_pToggleButton))) + { + if (m_bHoverSelection) + { + // turn hover selection back off until mouse is moved again + // *after* menu is shown again + gtk_tree_view_set_hover_selection(m_pTreeView, false); + m_bHoverSelection = false; + } + + bool bHadFocus = gtk_window_has_toplevel_focus(m_pMenuWindow); + + do_ungrab(GTK_WIDGET(m_pMenuWindow)); + + gtk_widget_hide(GTK_WIDGET(m_pMenuWindow)); + + GdkSurface* pSurface = widget_get_surface(GTK_WIDGET(m_pMenuWindow)); + g_object_set_data(G_OBJECT(pSurface), "g-lo-InstancePopup", GINT_TO_POINTER(false)); + + // so gdk_window_move_to_rect will work again the next time + gtk_widget_unrealize(GTK_WIDGET(m_pMenuWindow)); + + gtk_widget_set_size_request(GTK_WIDGET(m_pMenuWindow), -1, -1); + + if (!m_bActivateCalled) + tree_view_set_cursor(m_nPrePopupCursorPos); + + // undo show_menu tooltip blocking + GtkWidget* pParent = widget_get_toplevel(m_pToggleButton); + GtkSalFrame* pFrame = pParent ? GtkSalFrame::getFromWindow(pParent) : nullptr; + if (pFrame) + pFrame->UnblockTooltip(); + + if (bHadFocus) + { + GdkSurface* pParentSurface = pParent ? widget_get_surface(pParent) : nullptr; + void* pParentIsPopover = pParentSurface ? g_object_get_data(G_OBJECT(pParentSurface), "g-lo-InstancePopup") : nullptr; + if (pParentIsPopover) + do_grab(m_pToggleButton); + gtk_widget_grab_focus(m_pToggleButton); + } + } + else + { + GtkWidget* pComboBox = GTK_WIDGET(getContainer()); + + gint nComboWidth = gtk_widget_get_allocated_width(pComboBox); + GtkRequisition size; + gtk_widget_get_preferred_size(GTK_WIDGET(m_pMenuWindow), nullptr, &size); + + gint nPopupWidth = size.width; + gint nPopupHeight = get_popup_height(nPopupWidth); + nPopupWidth = std::max(nPopupWidth, nComboWidth); + + gtk_widget_set_size_request(GTK_WIDGET(m_pMenuWindow), nPopupWidth, nPopupHeight); + + m_nPrePopupCursorPos = get_active(); + + m_bActivateCalled = false; + + // if we are in mru mode always start with the cursor at the top of the menu + if (m_nMaxMRUCount) + tree_view_set_cursor(0); + + GdkRectangle aAnchor {0, 0, gtk_widget_get_allocated_width(pComboBox), gtk_widget_get_allocated_height(pComboBox) }; + show_menu(pComboBox, m_pMenuWindow, aAnchor, weld::Placement::Under, true); + GdkSurface* pSurface = widget_get_surface(GTK_WIDGET(m_pMenuWindow)); + g_object_set_data(G_OBJECT(pSurface), "g-lo-InstancePopup", GINT_TO_POINTER(true)); + } + } + + virtual void signal_popup_toggled() override + { + m_aQuickSelectionEngine.Reset(); + + menu_toggled(); + + bool bIsShown = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(m_pToggleButton)); + if (m_bPopupActive == bIsShown) + return; + + m_bPopupActive = bIsShown; + ComboBox::signal_popup_toggled(); + if (!m_bPopupActive && m_pEntry) + { + disable_notify_events(); + //restore focus to the GtkEntry when the popup is gone, which + //is what the vcl case does, to ease the transition a little + gtk_widget_grab_focus(m_pEntry); + enable_notify_events(); + } + } + + static gboolean signalEntryFocusIn(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->signal_entry_focus_in(); + return false; + } + + void signal_entry_focus_in() + { + signal_focus_in(); + } + + static gboolean signalEntryFocusOut(GtkWidget*, GdkEvent*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->signal_entry_focus_out(); + return false; + } + + void signal_entry_focus_out() + { + // if we have an untidy selection on losing focus remove the selection + int nStartPos, nEndPos; + if (get_entry_selection_bounds(nStartPos, nEndPos)) + { + int nMin = std::min(nStartPos, nEndPos); + int nMax = std::max(nStartPos, nEndPos); + if (nMin != 0 || nMax != get_active_text().getLength()) + select_entry_region(0, 0); + } + signal_focus_out(); + } + + static void signalEntryActivate(GtkEntry*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->signal_entry_activate(); + } + + void signal_entry_activate() + { + if (m_aEntryActivateHdl.IsSet()) + { + SolarMutexGuard aGuard; + if (m_aEntryActivateHdl.Call(*this)) + g_signal_stop_emission_by_name(m_pEntry, "activate"); + } + update_mru(); + } + + OUString get(int pos, int col) const + { + OUString sRet; + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + { + gchar* pStr; + gtk_tree_model_get(m_pTreeModel, &iter, col, &pStr, -1); + sRet = OUString(pStr, pStr ? strlen(pStr) : 0, RTL_TEXTENCODING_UTF8); + g_free(pStr); + } + return sRet; + } + + void set(int pos, int col, std::u16string_view rText) + { + GtkTreeIter iter; + if (gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos)) + { + OString aStr(OUStringToOString(rText, RTL_TEXTENCODING_UTF8)); + gtk_list_store_set(GTK_LIST_STORE(m_pTreeModel), &iter, col, aStr.getStr(), -1); + } + } + + int find(std::u16string_view rStr, int col, bool bSearchMRUArea) const + { + GtkTreeIter iter; + if (!gtk_tree_model_get_iter_first(m_pTreeModel, &iter)) + return -1; + + int nRet = 0; + + if (!bSearchMRUArea && m_nMRUCount) + { + if (!gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, m_nMRUCount + 1)) + return -1; + nRet += (m_nMRUCount + 1); + } + + OString aStr(OUStringToOString(rStr, RTL_TEXTENCODING_UTF8)); + do + { + gchar* pStr; + gtk_tree_model_get(m_pTreeModel, &iter, col, &pStr, -1); + const bool bEqual = g_strcmp0(pStr, aStr.getStr()) == 0; + g_free(pStr); + if (bEqual) + return nRet; + ++nRet; + } while (gtk_tree_model_iter_next(m_pTreeModel, &iter)); + + return -1; + } + + bool separator_function(const GtkTreePath* path) + { + return ::separator_function(path, m_aSeparatorRows); + } + + bool separator_function(int pos) + { + GtkTreePath* path = gtk_tree_path_new_from_indices(pos, -1); + bool bRet = separator_function(path); + gtk_tree_path_free(path); + return bRet; + } + + static gboolean separatorFunction(GtkTreeModel* pTreeModel, GtkTreeIter* pIter, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + GtkTreePath* path = gtk_tree_model_get_path(pTreeModel, pIter); + bool bRet = pThis->separator_function(path); + gtk_tree_path_free(path); + return bRet; + } + + // https://gitlab.gnome.org/GNOME/gtk/issues/310 + // + // in the absence of a built-in solution + // a) support typeahead for the case where there is no entry widget, typing ahead + // into the button itself will select via the vcl selection engine, a matching + // entry + static gboolean signalKeyPress(GtkWidget*, GdkEventKey* pEvent, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + return pThis->signal_key_press(pEvent); + } + + // tdf#131076 we want return in a ComboBox to act like return in a + // GtkEntry and activate the default dialog/assistant button + bool combobox_activate() + { + GtkWidget *pComboBox = GTK_WIDGET(m_pToggleButton); + GtkWidget *pToplevel = widget_get_toplevel(pComboBox); + GtkWindow *pWindow = GTK_WINDOW(pToplevel); + if (!pWindow) + return false; + if (!GTK_IS_DIALOG(pWindow) && !GTK_IS_ASSISTANT(pWindow)) + return false; + bool bDone = false; + GtkWidget *pDefaultWidget = gtk_window_get_default_widget(pWindow); + if (pDefaultWidget && pDefaultWidget != m_pToggleButton && gtk_widget_get_sensitive(pDefaultWidget)) + bDone = gtk_widget_activate(pDefaultWidget); + return bDone; + } + + static gboolean signalEntryKeyPress(GtkEntry* pEntry, GdkEventKey* pEvent, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + LocalizeDecimalSeparator(pEvent->keyval); + if (signalEntryInsertSpecialCharKeyPress(pEntry, pEvent, nullptr)) + return true; + return pThis->signal_entry_key_press(pEvent); + } + + bool signal_entry_key_press(const GdkEventKey* pEvent) + { + KeyEvent aKEvt(GtkToVcl(*pEvent)); + + vcl::KeyCode aKeyCode = aKEvt.GetKeyCode(); + + bool bDone = false; + + auto nCode = aKeyCode.GetCode(); + switch (nCode) + { + case KEY_DOWN: + { + sal_uInt16 nKeyMod = aKeyCode.GetModifier(); + if (!nKeyMod) + { + int nCount = get_count_including_mru(); + int nActive = get_active_including_mru() + 1; + while (nActive < nCount && separator_function(nActive)) + ++nActive; + if (nActive < nCount) + set_active_including_mru(nActive, true); + bDone = true; + } + else if (nKeyMod == KEY_MOD2 && !m_bPopupActive) + { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pToggleButton), true); + bDone = true; + } + break; + } + case KEY_UP: + { + sal_uInt16 nKeyMod = aKeyCode.GetModifier(); + if (!nKeyMod) + { + int nStartBound = m_bPopupActive || !m_nMRUCount ? 0 : (m_nMRUCount + 1); + int nActive = get_active_including_mru() - 1; + while (nActive >= nStartBound && separator_function(nActive)) + --nActive; + if (nActive >= nStartBound) + set_active_including_mru(nActive, true); + bDone = true; + } + break; + } + case KEY_PAGEUP: + { + sal_uInt16 nKeyMod = aKeyCode.GetModifier(); + if (!nKeyMod) + { + int nCount = get_count_including_mru(); + int nStartBound = m_bPopupActive || !m_nMaxMRUCount ? 0 : (m_nMRUCount + 1); + int nActive = nStartBound; + while (nActive < nCount && separator_function(nActive)) + ++nActive; + if (nActive < nCount) + set_active_including_mru(nActive, true); + bDone = true; + } + break; + } + case KEY_PAGEDOWN: + { + sal_uInt16 nKeyMod = aKeyCode.GetModifier(); + if (!nKeyMod) + { + int nActive = get_count_including_mru() - 1; + int nStartBound = m_bPopupActive ? 0 : (m_nMRUCount + 1); + while (nActive >= nStartBound && separator_function(nActive)) + --nActive; + if (nActive >= nStartBound) + set_active_including_mru(nActive, true); + bDone = true; + } + break; + } + default: + break; + } + + return bDone; + } + + bool signal_key_press(const GdkEventKey* pEvent) + { + if (m_bHoverSelection) + { + // once a key is pressed, turn off hover selection until mouse is + // moved again otherwise when the treeview scrolls it jumps to the + // position under the mouse. + gtk_tree_view_set_hover_selection(m_pTreeView, false); + m_bHoverSelection = false; + } + + KeyEvent aKEvt(GtkToVcl(*pEvent)); + + vcl::KeyCode aKeyCode = aKEvt.GetKeyCode(); + + bool bDone = false; + + auto nCode = aKeyCode.GetCode(); + switch (nCode) + { + case KEY_DOWN: + case KEY_UP: + case KEY_PAGEUP: + case KEY_PAGEDOWN: + case KEY_HOME: + case KEY_END: + case KEY_LEFT: + case KEY_RIGHT: + case KEY_RETURN: + { + m_aQuickSelectionEngine.Reset(); + sal_uInt16 nKeyMod = aKeyCode.GetModifier(); + // tdf#131076 don't let bare return toggle menu popup active, but do allow deactivate + if (nCode == KEY_RETURN && !nKeyMod && !m_bPopupActive) + bDone = combobox_activate(); + else if (nCode == KEY_UP && nKeyMod == KEY_MOD2 && m_bPopupActive) + { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pToggleButton), false); + bDone = true; + } + else if (nCode == KEY_DOWN && nKeyMod == KEY_MOD2 && !m_bPopupActive) + { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pToggleButton), true); + bDone = true; + } + break; + } + case KEY_ESCAPE: + { + m_aQuickSelectionEngine.Reset(); + if (m_bPopupActive) + { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pToggleButton), false); + bDone = true; + } + break; + } + default: + // tdf#131076 let base space toggle menu popup when it's not already visible + if (nCode == KEY_SPACE && !aKeyCode.GetModifier() && !m_bPopupActive) + bDone = false; + else + bDone = m_aQuickSelectionEngine.HandleKeyEvent(aKEvt); + break; + } + + if (!bDone && !m_pEntry) + bDone = signal_entry_key_press(pEvent); + + return bDone; + } + + vcl::StringEntryIdentifier typeahead_getEntry(int nPos, OUString& out_entryText) const + { + int nEntryCount(get_count_including_mru()); + if (nPos >= nEntryCount) + nPos = 0; + out_entryText = get_text_including_mru(nPos); + + // vcl::StringEntryIdentifier does not allow for 0 values, but our position is 0-based + // => normalize + return reinterpret_cast(nPos + 1); + } + + static int typeahead_getEntryPos(vcl::StringEntryIdentifier entry) + { + // our pos is 0-based, but StringEntryIdentifier does not allow for a NULL + return reinterpret_cast(entry) - 1; + } + + void tree_view_set_cursor(int pos) + { + GtkTreePath* path; + if (pos == -1) + { + path = gtk_tree_path_new_from_indices(G_MAXINT, -1); + gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(m_pTreeView)); + if (m_pCellView) + gtk_cell_view_set_displayed_row(m_pCellView, nullptr); + } + else + { + path = gtk_tree_path_new_from_indices(pos, -1); + if (gtk_tree_view_get_model(m_pTreeView)) + gtk_tree_view_scroll_to_cell(m_pTreeView, path, nullptr, false, 0, 0); + if (m_pCellView) + gtk_cell_view_set_displayed_row(m_pCellView, path); + } + gtk_tree_view_set_cursor(m_pTreeView, path, nullptr, false); + gtk_tree_path_free(path); + } + + int tree_view_get_cursor() const + { + int nRet = -1; + + GtkTreePath* path; + gtk_tree_view_get_cursor(m_pTreeView, &path, nullptr); + if (path) + { + gint depth; + gint* indices = gtk_tree_path_get_indices_with_depth(path, &depth); + nRet = indices[depth-1]; + gtk_tree_path_free(path); + } + + return nRet; + } + + int get_selected_entry() const + { + if (m_bPopupActive) + return tree_view_get_cursor(); + else + return get_active_including_mru(); + } + + void set_typeahead_selected_entry(int nSelect) + { + if (m_bPopupActive) + tree_view_set_cursor(nSelect); + else + set_active_including_mru(nSelect, true); + } + + virtual vcl::StringEntryIdentifier CurrentEntry(OUString& out_entryText) const override + { + int nCurrentPos = get_selected_entry(); + return typeahead_getEntry((nCurrentPos == -1) ? 0 : nCurrentPos, out_entryText); + } + + virtual vcl::StringEntryIdentifier NextEntry(vcl::StringEntryIdentifier currentEntry, OUString& out_entryText) const override + { + int nNextPos = typeahead_getEntryPos(currentEntry) + 1; + return typeahead_getEntry(nNextPos, out_entryText); + } + + virtual void SelectEntry(vcl::StringEntryIdentifier entry) override + { + int nSelect = typeahead_getEntryPos(entry); + if (nSelect == get_selected_entry()) + { + // ignore that. This method is a callback from the QuickSelectionEngine, which means the user attempted + // to select the given entry by typing its starting letters. No need to act. + return; + } + + // normalize + int nCount = get_count_including_mru(); + if (nSelect >= nCount) + nSelect = nCount ? nCount-1 : -1; + + set_typeahead_selected_entry(nSelect); + } + + static void signalGrabBroken(GtkWidget*, GdkEventGrabBroken *pEvent, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->grab_broken(pEvent); + } + + void grab_broken(const GdkEventGrabBroken *event) + { + if (event->grab_window == nullptr) + { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pToggleButton), false); + } + else if (!g_object_get_data(G_OBJECT(event->grab_window), "g-lo-InstancePopup")) // another LibreOffice popover took a grab + { + //try and regrab, so when we lose the grab to the menu of the color palette + //combobox we regain it so the color palette doesn't itself disappear on next + //click on the color palette combobox + do_grab(GTK_WIDGET(m_pMenuWindow)); + } + } + + static gboolean signalButtonPress(GtkWidget*, GdkEventButton* pEvent, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + return pThis->button_press(pEvent); + } + + bool button_press(GdkEventButton* pEvent) + { + //we want to pop down if the button was pressed outside our popup + if (button_event_is_outside(GTK_WIDGET(m_pMenuWindow), pEvent)) + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pToggleButton), false); + return false; + } + + static gboolean signalMotion(GtkWidget*, GdkEventMotion*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->signal_motion(); + return false; + } + + void signal_motion() + { + // if hover-selection was disabled after pressing a key, then turn it back on again + if (!m_bHoverSelection && !m_bMouseInOverlayButton) + { + gtk_tree_view_set_hover_selection(m_pTreeView, true); + m_bHoverSelection = true; + } + } + + static void signalRowActivated(GtkTreeView*, GtkTreePath*, GtkTreeViewColumn*, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->handle_row_activated(); + } + + void handle_row_activated() + { + m_bActivateCalled = true; + m_bChangedByMenu = true; + disable_notify_events(); + int nActive = get_active(); + if (m_pEntry) + gtk_entry_set_text(GTK_ENTRY(m_pEntry), OUStringToOString(get_text(nActive), RTL_TEXTENCODING_UTF8).getStr()); + else + tree_view_set_cursor(nActive); + enable_notify_events(); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(m_pToggleButton), false); + fire_signal_changed(); + update_mru(); + } + + void do_clear() + { + disable_notify_events(); + gtk_tree_view_set_row_separator_func(m_pTreeView, nullptr, nullptr, nullptr); + m_aSeparatorRows.clear(); + gtk_list_store_clear(GTK_LIST_STORE(m_pTreeModel)); + m_nMRUCount = 0; + enable_notify_events(); + } + + virtual int get_max_mru_count() const override + { + return m_nMaxMRUCount; + } + + virtual void set_max_mru_count(int nMaxMRUCount) override + { + m_nMaxMRUCount = nMaxMRUCount; + update_mru(); + } + + void update_mru() + { + int nMRUCount = m_nMRUCount; + + if (m_nMaxMRUCount) + { + OUString sActiveText = get_active_text(); + OUString sActiveId = get_active_id(); + insert_including_mru(0, sActiveText, &sActiveId, nullptr, nullptr); + ++m_nMRUCount; + + for (int i = 1; i < m_nMRUCount - 1; ++i) + { + if (get_text_including_mru(i) == sActiveText) + { + remove_including_mru(i); + --m_nMRUCount; + break; + } + } + } + + while (m_nMRUCount > m_nMaxMRUCount) + { + remove_including_mru(m_nMRUCount - 1); + --m_nMRUCount; + } + + if (m_nMRUCount && !nMRUCount) + insert_separator_including_mru(m_nMRUCount, "separator"); + else if (!m_nMRUCount && nMRUCount) + remove_including_mru(m_nMRUCount); // remove separator + } + + int get_count_including_mru() const + { + return gtk_tree_model_iter_n_children(m_pTreeModel, nullptr); + } + + int get_active_including_mru() const + { + return tree_view_get_cursor(); + } + + void set_active_including_mru(int pos, bool bInteractive) + { + assert(gtk_tree_view_get_model(m_pTreeView) && "don't set_active when frozen, set_active after thaw. Note selection doesn't survive a freeze"); + + disable_notify_events(); + + tree_view_set_cursor(pos); + + if (m_pEntry) + { + if (pos != -1) + gtk_entry_set_text(GTK_ENTRY(m_pEntry), OUStringToOString(get_text_including_mru(pos), RTL_TEXTENCODING_UTF8).getStr()); + else + gtk_entry_set_text(GTK_ENTRY(m_pEntry), ""); + } + + m_bChangedByMenu = false; + enable_notify_events(); + + if (bInteractive && !m_bPopupActive) + signal_changed(); + } + + int find_text_including_mru(std::u16string_view rStr, bool bSearchMRU) const + { + return find(rStr, m_nTextCol, bSearchMRU); + } + + int find_id_including_mru(std::u16string_view rId, bool bSearchMRU) const + { + return find(rId, m_nIdCol, bSearchMRU); + } + + OUString get_text_including_mru(int pos) const + { + return get(pos, m_nTextCol); + } + + OUString get_id_including_mru(int pos) const + { + return get(pos, m_nIdCol); + } + + void set_id_including_mru(int pos, std::u16string_view rId) + { + set(pos, m_nIdCol, rId); + } + + void remove_including_mru(int pos) + { + disable_notify_events(); + GtkTreeIter iter; + gtk_tree_model_iter_nth_child(m_pTreeModel, &iter, nullptr, pos); + if (!m_aSeparatorRows.empty()) + { + bool bFound = false; + + GtkTreePath* pPath = gtk_tree_path_new_from_indices(pos, -1); + + for (auto aIter = m_aSeparatorRows.begin(); aIter != m_aSeparatorRows.end(); ++aIter) + { + GtkTreePath* seppath = gtk_tree_row_reference_get_path(aIter->get()); + if (seppath) + { + if (gtk_tree_path_compare(pPath, seppath) == 0) + bFound = true; + gtk_tree_path_free(seppath); + } + if (bFound) + { + m_aSeparatorRows.erase(aIter); + break; + } + } + + gtk_tree_path_free(pPath); + } + gtk_list_store_remove(GTK_LIST_STORE(m_pTreeModel), &iter); + enable_notify_events(); + } + + void insert_separator_including_mru(int pos, const OUString& rId) + { + disable_notify_events(); + GtkTreeIter iter; + if (!gtk_tree_view_get_row_separator_func(m_pTreeView)) + gtk_tree_view_set_row_separator_func(m_pTreeView, separatorFunction, this, nullptr); + insert_row(GTK_LIST_STORE(m_pTreeModel), iter, pos, &rId, u"", nullptr, nullptr); + GtkTreePath* pPath = gtk_tree_path_new_from_indices(pos, -1); + m_aSeparatorRows.emplace_back(gtk_tree_row_reference_new(m_pTreeModel, pPath)); + gtk_tree_path_free(pPath); + enable_notify_events(); + } + + void insert_including_mru(int pos, std::u16string_view rText, const OUString* pId, const OUString* pIconName, const VirtualDevice* pImageSurface) + { + disable_notify_events(); + GtkTreeIter iter; + insert_row(GTK_LIST_STORE(m_pTreeModel), iter, pos, pId, rText, pIconName, pImageSurface); + enable_notify_events(); + } + + static gboolean signalGetChildPosition(GtkOverlay*, GtkWidget*, GdkRectangle* pAllocation, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + return pThis->signal_get_child_position(pAllocation); + } + + bool signal_get_child_position(GdkRectangle* pAllocation) + { + if (!gtk_widget_get_visible(GTK_WIDGET(m_pOverlayButton))) + return false; + if (!gtk_widget_get_realized(GTK_WIDGET(m_pTreeView))) + return false; + int nRow = find_id_including_mru(m_sMenuButtonRow, true); + if (nRow == -1) + return false; + + gtk_widget_get_preferred_width(GTK_WIDGET(m_pOverlayButton), &pAllocation->width, nullptr); + + GtkTreePath* pPath = gtk_tree_path_new_from_indices(nRow, -1); + GList* pColumns = gtk_tree_view_get_columns(m_pTreeView); + tools::Rectangle aRect = get_row_area(m_pTreeView, pColumns, pPath); + gtk_tree_path_free(pPath); + g_list_free(pColumns); + + pAllocation->x = aRect.Right() - pAllocation->width; + pAllocation->y = aRect.Top(); + pAllocation->height = aRect.GetHeight(); + + return true; + } + + static gboolean signalOverlayButtonCrossing(GtkWidget*, GdkEventCrossing* pEvent, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->signal_overlay_button_crossing(pEvent->type == GDK_ENTER_NOTIFY); + return false; + } + + void signal_overlay_button_crossing(bool bEnter) + { + m_bMouseInOverlayButton = bEnter; + if (!bEnter) + return; + + if (m_bHoverSelection) + { + // once toggled button is pressed, turn off hover selection until + // mouse leaves the overlay button + gtk_tree_view_set_hover_selection(m_pTreeView, false); + m_bHoverSelection = false; + } + int nRow = find_id_including_mru(m_sMenuButtonRow, true); + assert(nRow != -1); + tree_view_set_cursor(nRow); // select the buttons row + } + + void signal_combo_mnemonic_activate() + { + if (m_pEntry) + gtk_widget_grab_focus(m_pEntry); + else + gtk_widget_grab_focus(m_pToggleButton); + } + + static gboolean signalComboMnemonicActivate(GtkWidget*, gboolean, gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + pThis->signal_combo_mnemonic_activate(); + return true; + } + + static gboolean signalComboTooltipQuery(GtkWidget* /*pWidget*/, gint x, gint y, + gboolean keyboard_mode, GtkTooltip *tooltip, + gpointer widget) + { + GtkInstanceComboBox* pThis = static_cast(widget); + return signalTooltipQuery(GTK_WIDGET(pThis->m_pComboBox), x, y, keyboard_mode, tooltip); + } + + int include_mru(int pos) + { + if (m_nMRUCount && pos != -1) + pos += (m_nMRUCount + 1); + return pos; + } + +public: + GtkInstanceComboBox(GtkBuilder* pComboBuilder, GtkComboBox* pComboBox, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceContainer(GTK_CONTAINER(gtk_builder_get_object(pComboBuilder, "box")), pBuilder, bTakeOwnership) + , m_pComboBuilder(pComboBuilder) + , m_pComboBox(pComboBox) + , m_pOverlay(GTK_OVERLAY(gtk_builder_get_object(pComboBuilder, "overlay"))) + , m_pTreeView(GTK_TREE_VIEW(gtk_builder_get_object(pComboBuilder, "treeview"))) + , m_pOverlayButton(GTK_MENU_BUTTON(gtk_builder_get_object(pComboBuilder, "overlaybutton"))) + , m_pMenuWindow(GTK_WINDOW(gtk_builder_get_object(pComboBuilder, "popup"))) + , m_pTreeModel(gtk_combo_box_get_model(pComboBox)) + , m_pButtonTextRenderer(nullptr) + , m_pToggleButton(GTK_WIDGET(gtk_builder_get_object(pComboBuilder, "button"))) + , m_pEntry(GTK_WIDGET(gtk_builder_get_object(pComboBuilder, "entry"))) + , m_pCellView(nullptr) + , m_aCustomFont(m_pWidget) + , m_aQuickSelectionEngine(*this) + , m_bHoverSelection(false) + , m_bMouseInOverlayButton(false) + , m_bPopupActive(false) + , m_bAutoComplete(false) + , m_bAutoCompleteCaseSensitive(false) + , m_bChangedByMenu(false) + , m_bCustomRenderer(false) + , m_bActivateCalled(false) + , m_nTextCol(gtk_combo_box_get_entry_text_column(pComboBox)) + , m_nIdCol(gtk_combo_box_get_id_column(pComboBox)) + , m_nToggleFocusInSignalId(0) + , m_nToggleFocusOutSignalId(0) + , m_nRowActivatedSignalId(g_signal_connect(m_pTreeView, "row-activated", G_CALLBACK(signalRowActivated), this)) + , m_nChangedSignalId(g_signal_connect(m_pEntry, "changed", G_CALLBACK(signalChanged), this)) + , m_nPopupShownSignalId(g_signal_connect(m_pToggleButton, "toggled", G_CALLBACK(signalPopupToggled), this)) + , m_nAutoCompleteIdleId(0) + , m_nNonCustomLineHeight(-1) + , m_nPrePopupCursorPos(-1) + , m_nMRUCount(0) + , m_nMaxMRUCount(0) + { + int nActive = gtk_combo_box_get_active(m_pComboBox); + + if (gtk_style_context_has_class(gtk_widget_get_style_context(GTK_WIDGET(m_pComboBox)), "small-button")) + gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(getContainer())), "small-button"); + + if (gtk_widget_get_has_tooltip(GTK_WIDGET(m_pComboBox))) + { + gtk_widget_set_has_tooltip(GTK_WIDGET(getContainer()), true); + g_signal_connect(getContainer(), "query-tooltip", G_CALLBACK(signalComboTooltipQuery), this); + } + + insertAsParent(GTK_WIDGET(m_pComboBox), GTK_WIDGET(getContainer())); + gtk_widget_set_visible(GTK_WIDGET(m_pComboBox), false); + gtk_widget_set_no_show_all(GTK_WIDGET(m_pComboBox), true); + + gtk_tree_view_set_model(m_pTreeView, m_pTreeModel); + /* tdf#136455 gtk_combo_box_set_model with a null Model should be good + enough. But in practice, while the ComboBox model is unset, GTK + doesn't unset the ComboBox menus model, so that remains listening to + additions to the ListStore and slowing things down massively. + Using a new model does reset the menu to listen to that unused one instead */ + gtk_combo_box_set_model(m_pComboBox, GTK_TREE_MODEL(gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_STRING))); + + GtkTreeViewColumn* pCol = gtk_tree_view_column_new(); + gtk_tree_view_append_column(m_pTreeView, pCol); + + bool bPixbufUsedSurface = gtk_tree_model_get_n_columns(m_pTreeModel) == 4; + + GList* cells = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(m_pComboBox)); + // move the cell renderers from the combobox to the replacement treeview + m_pMenuTextRenderer = static_cast(cells->data); + for (GList* pRenderer = g_list_first(cells); pRenderer; pRenderer = g_list_next(pRenderer)) + { + GtkCellRenderer* pCellRenderer = GTK_CELL_RENDERER(pRenderer->data); + bool bTextRenderer = pCellRenderer == m_pMenuTextRenderer; + gtk_tree_view_column_pack_end(pCol, pCellRenderer, bTextRenderer); + if (!bTextRenderer) + { + if (bPixbufUsedSurface) + gtk_tree_view_column_set_attributes(pCol, pCellRenderer, "surface", 3, nullptr); + else + gtk_tree_view_column_set_attributes(pCol, pCellRenderer, "pixbuf", 2, nullptr); + } + } + + gtk_tree_view_column_set_attributes(pCol, m_pMenuTextRenderer, "text", m_nTextCol, nullptr); + + if (gtk_combo_box_get_has_entry(m_pComboBox)) + { + m_bAutoComplete = true; + m_nEntryInsertTextSignalId = g_signal_connect(m_pEntry, "insert-text", G_CALLBACK(signalEntryInsertText), this); + m_nEntryActivateSignalId = g_signal_connect(m_pEntry, "activate", G_CALLBACK(signalEntryActivate), this); + m_nEntryFocusInSignalId = g_signal_connect(m_pEntry, "focus-in-event", G_CALLBACK(signalEntryFocusIn), this); + m_nEntryFocusOutSignalId = g_signal_connect(m_pEntry, "focus-out-event", G_CALLBACK(signalEntryFocusOut), this); + m_nEntryKeyPressEventSignalId = g_signal_connect(m_pEntry, "key-press-event", G_CALLBACK(signalEntryKeyPress), this); + m_nEntryPopulatePopupMenuSignalId = g_signal_connect(m_pEntry, "populate-popup", G_CALLBACK(signalEntryPopulatePopup), nullptr); + m_nKeyPressEventSignalId = 0; + } + else + { + gtk_widget_set_visible(m_pEntry, false); + m_pEntry = nullptr; + + GtkWidget* pArrow = GTK_WIDGET(gtk_builder_get_object(pComboBuilder, "arrow")); + gtk_container_child_set(getContainer(), m_pToggleButton, "expand", true, nullptr); + + auto m_pCellArea = gtk_cell_area_box_new(); + m_pCellView = GTK_CELL_VIEW(gtk_cell_view_new_with_context(m_pCellArea, nullptr)); + gtk_widget_set_hexpand(GTK_WIDGET(m_pCellView), true); + GtkBox* pBox = GTK_BOX(gtk_widget_get_parent(pArrow)); + + gint nImageSpacing(2); + GtkStyleContext *pContext = gtk_widget_get_style_context(GTK_WIDGET(m_pToggleButton)); + gtk_style_context_get_style(pContext, "image-spacing", &nImageSpacing, nullptr); + gtk_box_set_spacing(pBox, nImageSpacing); + + gtk_box_pack_start(pBox, GTK_WIDGET(m_pCellView), false, true, 0); + + gtk_cell_view_set_fit_model(m_pCellView, true); + gtk_cell_view_set_model(m_pCellView, m_pTreeModel); + + m_pButtonTextRenderer = gtk_cell_renderer_text_new(); + gtk_cell_layout_pack_end(GTK_CELL_LAYOUT(m_pCellView), m_pButtonTextRenderer, true); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(m_pCellView), m_pButtonTextRenderer, "text", m_nTextCol, nullptr); + if (g_list_length(cells) > 1) + { + GtkCellRenderer* pCellRenderer = gtk_cell_renderer_pixbuf_new(); + gtk_cell_layout_pack_end(GTK_CELL_LAYOUT(m_pCellView), pCellRenderer, false); + if (bPixbufUsedSurface) + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(m_pCellView), pCellRenderer, "surface", 3, nullptr); + else + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(m_pCellView), pCellRenderer, "pixbuf", 2, nullptr); + } + + gtk_widget_show_all(GTK_WIDGET(m_pCellView)); + + m_nEntryInsertTextSignalId = 0; + m_nEntryActivateSignalId = 0; + m_nEntryFocusInSignalId = 0; + m_nEntryFocusOutSignalId = 0; + m_nEntryKeyPressEventSignalId = 0; + m_nEntryPopulatePopupMenuSignalId = 0; + m_nKeyPressEventSignalId = g_signal_connect(m_pToggleButton, "key-press-event", G_CALLBACK(signalKeyPress), this); + } + + g_list_free(cells); + + if (nActive != -1) + tree_view_set_cursor(nActive); + + g_signal_connect(getContainer(), "mnemonic-activate", G_CALLBACK(signalComboMnemonicActivate), this); + + g_signal_connect(m_pMenuWindow, "grab-broken-event", G_CALLBACK(signalGrabBroken), this); + g_signal_connect(m_pMenuWindow, "button-press-event", G_CALLBACK(signalButtonPress), this); + g_signal_connect(m_pMenuWindow, "motion-notify-event", G_CALLBACK(signalMotion), this); + // support typeahead for the menu itself, typing into the menu will + // select via the vcl selection engine, a matching entry. + g_signal_connect(m_pMenuWindow, "key-press-event", G_CALLBACK(signalKeyPress), this); + + g_signal_connect(m_pOverlay, "get-child-position", G_CALLBACK(signalGetChildPosition), this); + gtk_overlay_add_overlay(m_pOverlay, GTK_WIDGET(m_pOverlayButton)); + g_signal_connect(m_pOverlayButton, "leave-notify-event", G_CALLBACK(signalOverlayButtonCrossing), this); + g_signal_connect(m_pOverlayButton, "enter-notify-event", G_CALLBACK(signalOverlayButtonCrossing), this); + } + + virtual int get_active() const override + { + int nActive = get_active_including_mru(); + if (nActive == -1) + return -1; + + if (m_nMRUCount) + { + if (nActive < m_nMRUCount) + nActive = find_text(get_text_including_mru(nActive)); + else + nActive -= (m_nMRUCount + 1); + } + + return nActive; + } + + virtual OUString get_active_id() const override + { + int nActive = get_active(); + return nActive != -1 ? get_id(nActive) : OUString(); + } + + virtual void set_active_id(const OUString& rStr) override + { + set_active(find_id(rStr)); + m_bChangedByMenu = false; + } + + virtual void set_size_request(int nWidth, int nHeight) override + { + if (m_pButtonTextRenderer) + { + // tweak the cell render to get a narrower size to stick + if (nWidth != -1) + { + // this bit isn't great, I really want to be able to ellipse the text in the comboboxtext itself and let + // the popup menu render them in full, in the interim ellipse both of them + g_object_set(G_OBJECT(m_pButtonTextRenderer), "ellipsize", PANGO_ELLIPSIZE_MIDDLE, nullptr); + + // to find out how much of the width of the combobox belongs to the cell, set + // the cell and widget to the min cell width and see what the difference is + int min; + gtk_cell_renderer_get_preferred_width(m_pButtonTextRenderer, m_pWidget, &min, nullptr); + gtk_cell_renderer_set_fixed_size(m_pButtonTextRenderer, min, -1); + gtk_widget_set_size_request(m_pWidget, min, -1); + int nNonCellWidth = get_preferred_size().Width() - min; + + int nCellWidth = nWidth - nNonCellWidth; + if (nCellWidth >= 0) + { + // now set the cell to the max width which it can be within the + // requested widget width + gtk_cell_renderer_set_fixed_size(m_pButtonTextRenderer, nWidth - nNonCellWidth, -1); + } + } + else + { + g_object_set(G_OBJECT(m_pButtonTextRenderer), "ellipsize", PANGO_ELLIPSIZE_NONE, nullptr); + gtk_cell_renderer_set_fixed_size(m_pButtonTextRenderer, -1, -1); + } + } + + gtk_widget_set_size_request(m_pWidget, nWidth, nHeight); + } + + virtual void set_active(int pos) override + { + set_active_including_mru(include_mru(pos), false); + } + + virtual OUString get_active_text() const override + { + if (m_pEntry) + { + const gchar* pText = gtk_entry_get_text(GTK_ENTRY(m_pEntry)); + return OUString(pText, pText ? strlen(pText) : 0, RTL_TEXTENCODING_UTF8); + } + + int nActive = get_active(); + if (nActive == -1) + return OUString(); + + return get_text(nActive); + } + + virtual OUString get_text(int pos) const override + { + if (m_nMRUCount) + pos += (m_nMRUCount + 1); + return get_text_including_mru(pos); + } + + virtual OUString get_id(int pos) const override + { + if (m_nMRUCount) + pos += (m_nMRUCount + 1); + return get_id_including_mru(pos); + } + + virtual void set_id(int pos, const OUString& rId) override + { + if (m_nMRUCount) + pos += (m_nMRUCount + 1); + set_id_including_mru(pos, rId); + } + + virtual void insert_vector(const std::vector& rItems, bool bKeepExisting) override + { + freeze(); + + int nInsertionPoint; + if (!bKeepExisting) + { + clear(); + nInsertionPoint = 0; + } + else + nInsertionPoint = get_count(); + + GtkTreeIter iter; + // tdf#125241 inserting backwards is faster + for (auto aI = rItems.rbegin(); aI != rItems.rend(); ++aI) + { + const auto& rItem = *aI; + insert_row(GTK_LIST_STORE(m_pTreeModel), iter, nInsertionPoint, rItem.sId.isEmpty() ? nullptr : &rItem.sId, + rItem.sString, rItem.sImage.isEmpty() ? nullptr : &rItem.sImage, nullptr); + } + + thaw(); + } + + virtual void remove(int pos) override + { + if (m_nMRUCount) + pos += (m_nMRUCount + 1); + remove_including_mru(pos); + } + + virtual void insert(int pos, const OUString& rText, const OUString* pId, const OUString* pIconName, VirtualDevice* pImageSurface) override + { + insert_including_mru(include_mru(pos), rText, pId, pIconName, pImageSurface); + } + + virtual void insert_separator(int pos, const OUString& rId) override + { + pos = pos == -1 ? get_count() : pos; + if (m_nMRUCount) + pos += (m_nMRUCount + 1); + insert_separator_including_mru(pos, rId); + } + + virtual int get_count() const override + { + int nCount = get_count_including_mru(); + if (m_nMRUCount) + nCount -= (m_nMRUCount + 1); + return nCount; + } + + virtual int find_text(const OUString& rStr) const override + { + int nPos = find_text_including_mru(rStr, false); + if (nPos != -1 && m_nMRUCount) + nPos -= (m_nMRUCount + 1); + return nPos; + } + + virtual int find_id(const OUString& rId) const override + { + int nPos = find_id_including_mru(rId, false); + if (nPos != -1 && m_nMRUCount) + nPos -= (m_nMRUCount + 1); + return nPos; + } + + virtual void clear() override + { + do_clear(); + } + + virtual void make_sorted() override + { + m_xSorter.reset(new comphelper::string::NaturalStringSorter( + ::comphelper::getProcessComponentContext(), + Application::GetSettings().GetUILanguageTag().getLocale())); + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_set_sort_column_id(pSortable, m_nTextCol, GTK_SORT_ASCENDING); + gtk_tree_sortable_set_sort_func(pSortable, m_nTextCol, default_sort_func, m_xSorter.get(), nullptr); + } + + virtual bool has_entry() const override + { + return gtk_combo_box_get_has_entry(m_pComboBox); + } + + virtual void set_entry_message_type(weld::EntryMessageType eType) override + { + assert(m_pEntry); + ::set_entry_message_type(GTK_ENTRY(m_pEntry), eType); + } + + virtual void set_entry_text(const OUString& rText) override + { + assert(m_pEntry); + disable_notify_events(); + gtk_entry_set_text(GTK_ENTRY(m_pEntry), OUStringToOString(rText, RTL_TEXTENCODING_UTF8).getStr()); + enable_notify_events(); + } + + virtual void set_entry_width_chars(int nChars) override + { + assert(m_pEntry); + disable_notify_events(); + gtk_entry_set_width_chars(GTK_ENTRY(m_pEntry), nChars); + gtk_entry_set_max_width_chars(GTK_ENTRY(m_pEntry), nChars); + enable_notify_events(); + } + + virtual void set_entry_max_length(int nChars) override + { + assert(m_pEntry); + disable_notify_events(); + gtk_entry_set_max_length(GTK_ENTRY(m_pEntry), nChars); + enable_notify_events(); + } + + virtual void select_entry_region(int nStartPos, int nEndPos) override + { + assert(m_pEntry); + disable_notify_events(); + gtk_editable_select_region(GTK_EDITABLE(m_pEntry), nStartPos, nEndPos); + enable_notify_events(); + } + + virtual bool get_entry_selection_bounds(int& rStartPos, int &rEndPos) override + { + assert(m_pEntry); + return gtk_editable_get_selection_bounds(GTK_EDITABLE(m_pEntry), &rStartPos, &rEndPos); + } + + virtual void set_entry_completion(bool bEnable, bool bCaseSensitive) override + { + m_bAutoComplete = bEnable; + m_bAutoCompleteCaseSensitive = bCaseSensitive; + } + + virtual void set_entry_placeholder_text(const OUString& rText) override + { + assert(m_pEntry); + gtk_entry_set_placeholder_text(GTK_ENTRY(m_pEntry), rText.toUtf8().getStr()); + } + + virtual void set_entry_editable(bool bEditable) override + { + assert(m_pEntry); + gtk_editable_set_editable(GTK_EDITABLE(m_pEntry), bEditable); + } + + virtual void cut_entry_clipboard() override + { + assert(m_pEntry); + gtk_editable_cut_clipboard(GTK_EDITABLE(m_pEntry)); + } + + virtual void copy_entry_clipboard() override + { + assert(m_pEntry); + gtk_editable_copy_clipboard(GTK_EDITABLE(m_pEntry)); + } + + virtual void paste_entry_clipboard() override + { + assert(m_pEntry); + gtk_editable_paste_clipboard(GTK_EDITABLE(m_pEntry)); + } + + virtual void set_font(const vcl::Font& rFont) override + { + m_aCustomFont.use_custom_font(&rFont, u"box#combobox"); + } + + virtual vcl::Font get_font() override + { + if (const vcl::Font* pFont = m_aCustomFont.get_custom_font()) + return *pFont; + return GtkInstanceWidget::get_font(); + } + + virtual void set_entry_font(const vcl::Font& rFont) override + { + m_xEntryFont = rFont; + assert(m_pEntry); + PangoAttrList* pOrigList = gtk_entry_get_attributes(GTK_ENTRY(m_pEntry)); + PangoAttrList* pAttrList = pOrigList ? pango_attr_list_copy(pOrigList) : pango_attr_list_new(); + update_attr_list(pAttrList, rFont); + gtk_entry_set_attributes(GTK_ENTRY(m_pEntry), pAttrList); + pango_attr_list_unref(pAttrList); + } + + virtual vcl::Font get_entry_font() override + { + if (m_xEntryFont) + return *m_xEntryFont; + assert(m_pEntry); + PangoContext* pContext = gtk_widget_get_pango_context(m_pEntry); + return pango_to_vcl(pango_context_get_font_description(pContext), + Application::GetSettings().GetUILanguageTag().getLocale()); + } + + virtual void disable_notify_events() override + { + if (m_pEntry) + { + g_signal_handler_block(m_pEntry, m_nEntryInsertTextSignalId); + g_signal_handler_block(m_pEntry, m_nEntryActivateSignalId); + g_signal_handler_block(m_pEntry, m_nEntryFocusInSignalId); + g_signal_handler_block(m_pEntry, m_nEntryFocusOutSignalId); + g_signal_handler_block(m_pEntry, m_nEntryKeyPressEventSignalId); + g_signal_handler_block(m_pEntry, m_nChangedSignalId); + } + else + g_signal_handler_block(m_pToggleButton, m_nKeyPressEventSignalId); + if (m_nToggleFocusInSignalId) + g_signal_handler_block(m_pToggleButton, m_nToggleFocusInSignalId); + if (m_nToggleFocusOutSignalId) + g_signal_handler_block(m_pToggleButton, m_nToggleFocusOutSignalId); + g_signal_handler_block(m_pTreeView, m_nRowActivatedSignalId); + g_signal_handler_block(m_pToggleButton, m_nPopupShownSignalId); + GtkInstanceContainer::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkInstanceContainer::enable_notify_events(); + g_signal_handler_unblock(m_pToggleButton, m_nPopupShownSignalId); + g_signal_handler_unblock(m_pTreeView, m_nRowActivatedSignalId); + if (m_nToggleFocusInSignalId) + g_signal_handler_unblock(m_pToggleButton, m_nToggleFocusInSignalId); + if (m_nToggleFocusOutSignalId) + g_signal_handler_unblock(m_pToggleButton, m_nToggleFocusOutSignalId); + if (m_pEntry) + { + g_signal_handler_unblock(m_pEntry, m_nChangedSignalId); + g_signal_handler_unblock(m_pEntry, m_nEntryActivateSignalId); + g_signal_handler_unblock(m_pEntry, m_nEntryFocusInSignalId); + g_signal_handler_unblock(m_pEntry, m_nEntryFocusOutSignalId); + g_signal_handler_unblock(m_pEntry, m_nEntryKeyPressEventSignalId); + g_signal_handler_unblock(m_pEntry, m_nEntryInsertTextSignalId); + } + else + g_signal_handler_unblock(m_pToggleButton, m_nKeyPressEventSignalId); + } + + virtual void freeze() override + { + disable_notify_events(); + bool bIsFirstFreeze = IsFirstFreeze(); + GtkInstanceContainer::freeze(); + if (bIsFirstFreeze) + { + g_object_ref(m_pTreeModel); + gtk_tree_view_set_model(m_pTreeView, nullptr); + g_object_freeze_notify(G_OBJECT(m_pTreeModel)); + if (m_xSorter) + { + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_set_sort_column_id(pSortable, GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, GTK_SORT_ASCENDING); + } + } + enable_notify_events(); + } + + virtual void thaw() override + { + disable_notify_events(); + if (IsLastThaw()) + { + if (m_xSorter) + { + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(m_pTreeModel); + gtk_tree_sortable_set_sort_column_id(pSortable, m_nTextCol, GTK_SORT_ASCENDING); + } + g_object_thaw_notify(G_OBJECT(m_pTreeModel)); + gtk_tree_view_set_model(m_pTreeView, m_pTreeModel); + g_object_unref(m_pTreeModel); + } + GtkInstanceContainer::thaw(); + enable_notify_events(); + } + + virtual bool get_popup_shown() const override + { + return m_bPopupActive; + } + + virtual void connect_focus_in(const Link& rLink) override + { + if (!m_nToggleFocusInSignalId) + m_nToggleFocusInSignalId = g_signal_connect_after(m_pToggleButton, "focus-in-event", G_CALLBACK(signalFocusIn), this); + GtkInstanceContainer::connect_focus_in(rLink); + } + + virtual void connect_focus_out(const Link& rLink) override + { + if (!m_nToggleFocusOutSignalId) + m_nToggleFocusOutSignalId = g_signal_connect_after(m_pToggleButton, "focus-out-event", G_CALLBACK(signalFocusOut), this); + GtkInstanceContainer::connect_focus_out(rLink); + } + + virtual void grab_focus() override + { + if (has_focus()) + return; + if (m_pEntry) + gtk_widget_grab_focus(m_pEntry); + else + gtk_widget_grab_focus(m_pToggleButton); + } + + virtual bool has_focus() const override + { + if (m_pEntry && gtk_widget_has_focus(m_pEntry)) + return true; + + if (gtk_widget_has_focus(m_pToggleButton)) + return true; + + if (gtk_widget_get_visible(GTK_WIDGET(m_pMenuWindow))) + { + if (gtk_widget_has_focus(GTK_WIDGET(m_pOverlayButton)) || gtk_widget_has_focus(GTK_WIDGET(m_pTreeView))) + return true; + } + + return GtkInstanceWidget::has_focus(); + } + + virtual bool changed_by_direct_pick() const override + { + return m_bChangedByMenu; + } + + virtual void set_custom_renderer(bool bOn) override + { + if (bOn == m_bCustomRenderer) + return; + GList* pColumns = gtk_tree_view_get_columns(m_pTreeView); + // keep the original height around for optimal popup height calculation + m_nNonCustomLineHeight = bOn ? ::get_height_row(m_pTreeView, pColumns) : -1; + GtkTreeViewColumn* pColumn = GTK_TREE_VIEW_COLUMN(pColumns->data); + gtk_cell_layout_clear(GTK_CELL_LAYOUT(pColumn)); + if (bOn) + { + GtkCellRenderer *pRenderer = custom_cell_renderer_new(); + GValue value = G_VALUE_INIT; + g_value_init(&value, G_TYPE_POINTER); + g_value_set_pointer(&value, static_cast(this)); + g_object_set_property(G_OBJECT(pRenderer), "instance", &value); + gtk_tree_view_column_pack_start(pColumn, pRenderer, true); + gtk_tree_view_column_add_attribute(pColumn, pRenderer, "text", m_nTextCol); + gtk_tree_view_column_add_attribute(pColumn, pRenderer, "id", m_nIdCol); + } + else + { + GtkCellRenderer *pRenderer = gtk_cell_renderer_text_new(); + gtk_tree_view_column_pack_start(pColumn, pRenderer, true); + gtk_tree_view_column_add_attribute(pColumn, pRenderer, "text", m_nTextCol); + } + g_list_free(pColumns); + m_bCustomRenderer = bOn; + } + + void call_signal_custom_render(VirtualDevice& rOutput, const tools::Rectangle& rRect, bool bSelected, const OUString& rId) + { + signal_custom_render(rOutput, rRect, bSelected, rId); + } + + Size call_signal_custom_get_size(VirtualDevice& rOutput) + { + return signal_custom_get_size(rOutput); + } + + VclPtr create_render_virtual_device() const override + { + return create_virtual_device(); + } + + virtual void set_item_menu(const OUString& rIdent, weld::Menu* pMenu) override + { + m_xCustomMenuButtonHelper.reset(); + GtkInstanceMenu* pPopoverWidget = dynamic_cast(pMenu); + GtkWidget* pMenuWidget = GTK_WIDGET(pPopoverWidget ? pPopoverWidget->getMenu() : nullptr); + gtk_menu_button_set_popup(m_pOverlayButton, pMenuWidget); + gtk_widget_set_visible(GTK_WIDGET(m_pOverlayButton), pMenuWidget != nullptr); + gtk_widget_queue_resize_no_redraw(GTK_WIDGET(m_pOverlayButton)); // force location recalc + if (pMenuWidget) + m_xCustomMenuButtonHelper.reset(new CustomRenderMenuButtonHelper(GTK_MENU(pMenuWidget), GTK_TOGGLE_BUTTON(m_pToggleButton))); + m_sMenuButtonRow = rIdent; + } + + OUString get_mru_entries() const override + { + const sal_Unicode cSep = ';'; + + OUStringBuffer aEntries; + for (sal_Int32 n = 0; n < m_nMRUCount; n++) + { + aEntries.append(get_text_including_mru(n)); + if (n < m_nMRUCount - 1) + aEntries.append(cSep); + } + return aEntries.makeStringAndClear(); + } + + virtual void set_mru_entries(const OUString& rEntries) override + { + const sal_Unicode cSep = ';'; + + // Remove old MRU entries + for (sal_Int32 n = m_nMRUCount; n;) + remove_including_mru(--n); + + sal_Int32 nMRUCount = 0; + sal_Int32 nIndex = 0; + do + { + OUString aEntry = rEntries.getToken(0, cSep, nIndex); + // Accept only existing entries + int nPos = find_text(aEntry); + if (nPos != -1) + { + OUString sId = get_id(nPos); + insert_including_mru(0, aEntry, &sId, nullptr, nullptr); + ++nMRUCount; + } + } + while (nIndex >= 0); + + if (nMRUCount && !m_nMRUCount) + insert_separator_including_mru(nMRUCount, "separator"); + else if (!nMRUCount && m_nMRUCount) + remove_including_mru(m_nMRUCount); // remove separator + + m_nMRUCount = nMRUCount; + } + + int get_menu_button_width() const override + { + bool bVisible = gtk_widget_get_visible(GTK_WIDGET(m_pOverlayButton)); + if (!bVisible) + gtk_widget_set_visible(GTK_WIDGET(m_pOverlayButton), true); + gint nWidth; + gtk_widget_get_preferred_width(GTK_WIDGET(m_pOverlayButton), &nWidth, nullptr); + if (!bVisible) + gtk_widget_set_visible(GTK_WIDGET(m_pOverlayButton), false); + return nWidth; + } + + virtual ~GtkInstanceComboBox() override + { + m_xCustomMenuButtonHelper.reset(); + do_clear(); + if (m_nAutoCompleteIdleId) + g_source_remove(m_nAutoCompleteIdleId); + if (m_pEntry) + { + g_signal_handler_disconnect(m_pEntry, m_nChangedSignalId); + g_signal_handler_disconnect(m_pEntry, m_nEntryInsertTextSignalId); + g_signal_handler_disconnect(m_pEntry, m_nEntryActivateSignalId); + g_signal_handler_disconnect(m_pEntry, m_nEntryFocusInSignalId); + g_signal_handler_disconnect(m_pEntry, m_nEntryFocusOutSignalId); + g_signal_handler_disconnect(m_pEntry, m_nEntryKeyPressEventSignalId); + g_signal_handler_disconnect(m_pEntry, m_nEntryPopulatePopupMenuSignalId); + } + else + g_signal_handler_disconnect(m_pToggleButton, m_nKeyPressEventSignalId); + if (m_nToggleFocusInSignalId) + g_signal_handler_disconnect(m_pToggleButton, m_nToggleFocusInSignalId); + if (m_nToggleFocusOutSignalId) + g_signal_handler_disconnect(m_pToggleButton, m_nToggleFocusOutSignalId); + g_signal_handler_disconnect(m_pTreeView, m_nRowActivatedSignalId); + g_signal_handler_disconnect(m_pToggleButton, m_nPopupShownSignalId); + + gtk_combo_box_set_model(m_pComboBox, m_pTreeModel); + gtk_tree_view_set_model(m_pTreeView, nullptr); + + // restore original hierarchy in dtor so a new GtkInstanceComboBox will + // result in the same layout each time + { + DisconnectMouseEvents(); + + g_object_ref(m_pComboBox); + + GtkContainer* pContainer = getContainer(); + + gtk_container_remove(pContainer, GTK_WIDGET(m_pComboBox)); + + replaceWidget(GTK_WIDGET(pContainer), GTK_WIDGET(m_pComboBox)); + + g_object_unref(m_pComboBox); + } + + g_object_unref(m_pComboBuilder); + } +}; + +#endif + +} + +void custom_cell_renderer_ensure_device(CustomCellRenderer *cellsurface, gpointer user_data) +{ + if (!cellsurface->device) + { + cellsurface->device = VclPtr::Create(); + cellsurface->device->SetBackground(COL_TRANSPARENT); + GtkInstanceWidget* pWidget = static_cast(user_data); + // expand the point size of the desired font to the equivalent pixel size + weld::SetPointFont(*cellsurface->device, pWidget->get_font()); + } +} + +Size custom_cell_renderer_get_size(VirtualDevice& rDevice, const OUString& rCellId, gpointer user_data) +{ + GtkInstanceWidget* pWidget = static_cast(user_data); + if (GtkInstanceTreeView* pTreeView = dynamic_cast(pWidget)) + return pTreeView->call_signal_custom_get_size(rDevice, rCellId); + else if (GtkInstanceComboBox* pComboBox = dynamic_cast(pWidget)) + return pComboBox->call_signal_custom_get_size(rDevice); + return Size(); +} + +void custom_cell_renderer_render(VirtualDevice& rDevice, const tools::Rectangle& rRect, bool bSelected, const OUString& rCellId, gpointer user_data) +{ + GtkInstanceWidget* pWidget = static_cast(user_data); + if (GtkInstanceTreeView* pTreeView = dynamic_cast(pWidget)) + pTreeView->call_signal_custom_render(rDevice, rRect, bSelected, rCellId); + else if (GtkInstanceComboBox* pComboBox = dynamic_cast(pWidget)) + pComboBox->call_signal_custom_render(rDevice, rRect, bSelected, rCellId); +} + +namespace { + +class GtkInstanceEntryTreeView : public GtkInstanceContainer, public virtual weld::EntryTreeView +{ +private: + GtkInstanceEntry* m_pEntry; + GtkInstanceTreeView* m_pTreeView; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong m_nKeyPressSignalId; +#endif + gulong m_nEntryInsertTextSignalId; + guint m_nAutoCompleteIdleId; + bool m_bAutoCompleteCaseSensitive; + bool m_bTreeChange; + +#if !GTK_CHECK_VERSION(4, 0, 0) + bool signal_key_press(GdkEventKey* pEvent) + { + if (GtkSalFrame::GetMouseModCode(pEvent->state)) // only with no modifiers held + return false; + + if (pEvent->keyval == GDK_KEY_KP_Up || pEvent->keyval == GDK_KEY_Up || pEvent->keyval == GDK_KEY_KP_Page_Up || pEvent->keyval == GDK_KEY_Page_Up || + pEvent->keyval == GDK_KEY_KP_Down || pEvent->keyval == GDK_KEY_Down || pEvent->keyval == GDK_KEY_KP_Page_Down || pEvent->keyval == GDK_KEY_Page_Down) + { + gboolean ret; + disable_notify_events(); + GtkWidget* pWidget = m_pTreeView->getWidget(); + if (m_pTreeView->get_selected_index() == -1) + { + m_pTreeView->set_cursor(0); + m_pTreeView->select(0); + m_xEntry->set_text(m_xTreeView->get_selected_text()); + } + else + { + gtk_widget_grab_focus(pWidget); + g_signal_emit_by_name(pWidget, "key-press-event", pEvent, &ret); + m_xEntry->set_text(m_xTreeView->get_selected_text()); + gtk_widget_grab_focus(m_pEntry->getWidget()); + } + m_xEntry->select_region(0, -1); + enable_notify_events(); + m_bTreeChange = true; + m_pEntry->fire_signal_changed(); + m_bTreeChange = false; + return true; + } + return false; + } + + static gboolean signalKeyPress(GtkWidget*, GdkEventKey* pEvent, gpointer widget) + { + GtkInstanceEntryTreeView* pThis = static_cast(widget); + return pThis->signal_key_press(pEvent); + } +#endif + + static gboolean idleAutoComplete(gpointer widget) + { + GtkInstanceEntryTreeView* pThis = static_cast(widget); + pThis->auto_complete(); + return false; + } + + void auto_complete() + { + m_nAutoCompleteIdleId = 0; + OUString aStartText = get_active_text(); + int nStartPos, nEndPos; + get_entry_selection_bounds(nStartPos, nEndPos); + int nMaxSelection = std::max(nStartPos, nEndPos); + if (nMaxSelection != aStartText.getLength()) + return; + + disable_notify_events(); + int nActive = get_active(); + int nStart = nActive; + + if (nStart == -1) + nStart = 0; + + // Try match case sensitive from current position + int nPos = m_pTreeView->starts_with(aStartText, nStart, true); + if (nPos == -1 && nStart != 0) + { + // Try match case insensitive, but from start + nPos = m_pTreeView->starts_with(aStartText, 0, true); + } + + if (!m_bAutoCompleteCaseSensitive) + { + // Try match case insensitive from current position + nPos = m_pTreeView->starts_with(aStartText, nStart, false); + if (nPos == -1 && nStart != 0) + { + // Try match case insensitive, but from start + nPos = m_pTreeView->starts_with(aStartText, 0, false); + } + } + + if (nPos == -1) + { + // Try match case sensitive from current position + nPos = m_pTreeView->starts_with(aStartText, nStart, true); + if (nPos == -1 && nStart != 0) + { + // Try match case sensitive, but from start + nPos = m_pTreeView->starts_with(aStartText, 0, true); + } + } + + if (nPos != -1) + { + OUString aText = get_text(nPos); + if (aText != aStartText) + set_active_text(aText); + select_entry_region(aText.getLength(), aStartText.getLength()); + } + enable_notify_events(); + } + + void signal_entry_insert_text(GtkEntry*, const gchar*, gint, gint*) + { + // now check for autocompletes + if (m_nAutoCompleteIdleId) + g_source_remove(m_nAutoCompleteIdleId); + m_nAutoCompleteIdleId = g_idle_add(idleAutoComplete, this); + } + + static void signalEntryInsertText(GtkEntry* pEntry, const gchar* pNewText, gint nNewTextLength, + gint* position, gpointer widget) + { + GtkInstanceEntryTreeView* pThis = static_cast(widget); + pThis->signal_entry_insert_text(pEntry, pNewText, nNewTextLength, position); + } + + +public: +#if GTK_CHECK_VERSION(4, 0, 0) + GtkInstanceEntryTreeView(GtkWidget* pContainer, GtkInstanceBuilder* pBuilder, bool bTakeOwnership, + std::unique_ptr xEntry, std::unique_ptr xTreeView) +#else + GtkInstanceEntryTreeView(GtkContainer* pContainer, GtkInstanceBuilder* pBuilder, bool bTakeOwnership, + std::unique_ptr xEntry, std::unique_ptr xTreeView) +#endif + : EntryTreeView(std::move(xEntry), std::move(xTreeView)) + , GtkInstanceContainer(pContainer, pBuilder, bTakeOwnership) + , m_pEntry(dynamic_cast(m_xEntry.get())) + , m_pTreeView(dynamic_cast(m_xTreeView.get())) + , m_nAutoCompleteIdleId(0) + , m_bAutoCompleteCaseSensitive(false) + , m_bTreeChange(false) + { + assert(m_pEntry); + GtkWidget* pWidget = m_pEntry->getWidget(); +#if !GTK_CHECK_VERSION(4, 0, 0) + m_nKeyPressSignalId = g_signal_connect(pWidget, "key-press-event", G_CALLBACK(signalKeyPress), this); +#endif + m_nEntryInsertTextSignalId = g_signal_connect(pWidget, "insert-text", G_CALLBACK(signalEntryInsertText), this); + } + + virtual void insert_separator(int /*pos*/, const OUString& /*rId*/) override + { + assert(false); + } + + virtual void make_sorted() override + { + GtkWidget* pTreeView = m_pTreeView->getWidget(); + GtkTreeModel* pModel = gtk_tree_view_get_model(GTK_TREE_VIEW(pTreeView)); + GtkTreeSortable* pSortable = GTK_TREE_SORTABLE(pModel); + gtk_tree_sortable_set_sort_column_id(pSortable, 1, GTK_SORT_ASCENDING); + } + + virtual void set_entry_completion(bool bEnable, bool bCaseSensitive) override + { + assert(!bEnable && "not implemented yet"); (void)bEnable; + m_bAutoCompleteCaseSensitive = bCaseSensitive; + } + + virtual void set_entry_placeholder_text(const OUString& rText) override + { + m_xEntry->set_placeholder_text(rText); + } + + virtual void set_entry_editable(bool bEditable) override + { + m_xEntry->set_editable(bEditable); + } + + virtual void cut_entry_clipboard() override + { + m_xEntry->cut_clipboard(); + } + + virtual void copy_entry_clipboard() override + { + m_xEntry->copy_clipboard(); + } + + virtual void paste_entry_clipboard() override + { + m_xEntry->paste_clipboard(); + } + + virtual void set_font(const vcl::Font&) override + { + assert(false && "not implemented"); + } + + virtual void set_entry_font(const vcl::Font& rFont) override + { + m_xEntry->set_font(rFont); + } + + virtual vcl::Font get_entry_font() override + { + return m_xEntry->get_font(); + } + + virtual void grab_focus() override { m_xEntry->grab_focus(); } + + virtual void connect_focus_in(const Link& rLink) override + { + m_xEntry->connect_focus_in(rLink); + } + + virtual void connect_focus_out(const Link& rLink) override + { + m_xEntry->connect_focus_out(rLink); + } + + virtual void disable_notify_events() override + { + GtkWidget* pWidget = m_pEntry->getWidget(); + g_signal_handler_block(pWidget, m_nEntryInsertTextSignalId); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_block(pWidget, m_nKeyPressSignalId); +#endif + m_pTreeView->disable_notify_events(); + GtkInstanceContainer::disable_notify_events(); + } + + virtual void enable_notify_events() override + { + GtkWidget* pWidget = m_pEntry->getWidget(); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_unblock(pWidget, m_nKeyPressSignalId); +#endif + g_signal_handler_unblock(pWidget, m_nEntryInsertTextSignalId); + m_pTreeView->enable_notify_events(); + GtkInstanceContainer::enable_notify_events(); + } + + virtual bool changed_by_direct_pick() const override + { + return m_bTreeChange; + } + + virtual void set_custom_renderer(bool /*bOn*/) override + { + assert(false && "not implemented"); + } + + virtual int get_max_mru_count() const override + { + assert(false && "not implemented"); + return 0; + } + + virtual void set_max_mru_count(int) override + { + assert(false && "not implemented"); + } + + virtual OUString get_mru_entries() const override + { + assert(false && "not implemented"); + return OUString(); + } + + virtual void set_mru_entries(const OUString&) override + { + assert(false && "not implemented"); + } + + virtual void set_item_menu(const OUString&, weld::Menu*) override + { + assert(false && "not implemented"); + } + + VclPtr create_render_virtual_device() const override + { + return create_virtual_device(); + } + + int get_menu_button_width() const override + { + assert(false && "not implemented"); + return 0; + } + + virtual ~GtkInstanceEntryTreeView() override + { + if (m_nAutoCompleteIdleId) + g_source_remove(m_nAutoCompleteIdleId); + GtkWidget* pWidget = m_pEntry->getWidget(); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(pWidget, m_nKeyPressSignalId); +#endif + g_signal_handler_disconnect(pWidget, m_nEntryInsertTextSignalId); + } +}; + +} + +namespace { + +class GtkInstanceExpander : public GtkInstanceWidget, public virtual weld::Expander +{ +private: + GtkExpander* m_pExpander; + gulong m_nSignalId; +#if !GTK_CHECK_VERSION(4, 0, 0) + gulong m_nButtonPressEventSignalId; + gulong m_nMappedSignalId; +#endif + + static void signalExpanded(GtkExpander* /*pExpander*/, GParamSpec*, gpointer widget) + { + GtkInstanceExpander* pThis = static_cast(widget); + SolarMutexGuard aGuard; + pThis->signal_expanded(); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static gboolean signalButton(GtkWidget*, GdkEventButton*, gpointer) + { + // don't let button press get to parent window, for the case of the + // an expander in a sidebar where otherwise single click to expand + // doesn't work + return true; + } + + /* tdf#141186 if the expander is initially collapsed then when mapped all its + children are mapped too. If they are mapped then the mnemonics of the + children are taken into account on shortcuts and non-visible children in a + collapsed expander can be triggered which is confusing. + + If the expander is expanded and collapsed the child is unmapped and the + problem doesn't occur. + + So to avoid the problem of an initially collapsed expander, listen to + the map event and if the expander is mapped but collapsed then unmap the + child of the expander. + + This problem was seen in gtk3-3.24.33 and not with gtk4-4.6.4 so a gtk3 + fix only needed. + */ + static void signalMap(GtkWidget*, gpointer widget) + { + GtkInstanceExpander* pThis = static_cast(widget); + if (!gtk_expander_get_expanded(pThis->m_pExpander)) + { + if (GtkWidget* pChild = gtk_bin_get_child(GTK_BIN(pThis->m_pExpander))) + gtk_widget_unmap(pChild); + } + } +#endif + +public: + GtkInstanceExpander(GtkExpander* pExpander, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) + : GtkInstanceWidget(GTK_WIDGET(pExpander), pBuilder, bTakeOwnership) + , m_pExpander(pExpander) + , m_nSignalId(g_signal_connect(m_pExpander, "notify::expanded", G_CALLBACK(signalExpanded), this)) +#if !GTK_CHECK_VERSION(4, 0, 0) + , m_nButtonPressEventSignalId(g_signal_connect_after(m_pExpander, "button-press-event", G_CALLBACK(signalButton), this)) + , m_nMappedSignalId(g_signal_connect_after(m_pExpander, "map", G_CALLBACK(signalMap), this)) +#endif + { + } + + virtual void set_label(const OUString& rText) override + { + ::set_label(GTK_LABEL(gtk_expander_get_label_widget(m_pExpander)), rText); + } + + virtual OUString get_label() const override + { + return ::get_label(GTK_LABEL(gtk_expander_get_label_widget(m_pExpander))); + } + + virtual bool get_expanded() const override + { + return gtk_expander_get_expanded(m_pExpander); + } + + virtual void set_expanded(bool bExpand) override + { + gtk_expander_set_expanded(m_pExpander, bExpand); + } + + virtual ~GtkInstanceExpander() override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_handler_disconnect(m_pExpander, m_nMappedSignalId); + g_signal_handler_disconnect(m_pExpander, m_nButtonPressEventSignalId); +#endif + g_signal_handler_disconnect(m_pExpander, m_nSignalId); + } +}; + +} + +namespace { + +class GtkInstancePopover : public GtkInstanceContainer, public virtual weld::Popover +{ +private: +#if !GTK_CHECK_VERSION(4, 0, 0) + //popover cannot escape dialog under X so we might need to stick up own window instead + GtkWindow* m_pMenuHack; + bool m_bMenuPoppedUp; + bool m_nButtonPressSeen; +#endif + GtkPopover* m_pPopover; + gulong m_nSignalId; + ImplSVEvent* m_pClosedEvent; + + static void signalClosed(GtkPopover*, gpointer widget) + { + GtkInstancePopover* pThis = static_cast(widget); + // call signal-closed async so the closed callback isn't called + // while the GtkPopover handler is still in-execution + pThis->launch_signal_closed(); + } + + DECL_LINK(async_signal_closed, void*, void); + + void launch_signal_closed() + { + if (m_pClosedEvent) + Application::RemoveUserEvent(m_pClosedEvent); + m_pClosedEvent = Application::PostUserEvent(LINK(this, GtkInstancePopover, async_signal_closed)); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + static gboolean keyPress(GtkWidget*, GdkEventKey* pEvent, gpointer widget) + { + GtkInstancePopover* pThis = static_cast(widget); + return pThis->key_press(pEvent); + } + + bool key_press(const GdkEventKey* pEvent) + { + if (pEvent->keyval == GDK_KEY_Escape) + { + popdown(); + return true; + } + return false; + } + + static gboolean signalButtonPress(GtkWidget* /*pWidget*/, GdkEventButton* /*pEvent*/, gpointer widget) + { + GtkInstancePopover* pThis = static_cast(widget); + pThis->m_nButtonPressSeen = true; + return false; + } + + static gboolean signalButtonRelease(GtkWidget* /*pWidget*/, GdkEventButton* pEvent, gpointer widget) + { + GtkInstancePopover* pThis = static_cast(widget); + if (pThis->m_nButtonPressSeen && button_event_is_outside(GTK_WIDGET(pThis->m_pMenuHack), pEvent)) + pThis->popdown(); + return false; + } + + bool forward_event_if_popup_under_mouse(GdkEvent* pEvent) + { + GtkWidget* pEventWidget = gtk_get_event_widget(pEvent); + GtkWidget* pTopLevel = widget_get_toplevel(pEventWidget); + + if (pTopLevel == GTK_WIDGET(m_pMenuHack)) + return false; + + GdkSurface* pSurface = widget_get_surface(pTopLevel); + void* pMouseEnteredAnotherPopup = g_object_get_data(G_OBJECT(pSurface), "g-lo-InstancePopup"); + if (!pMouseEnteredAnotherPopup) + return false; + + return gtk_widget_event(pEventWidget, pEvent); + } + + static gboolean signalButtonCrossing(GtkWidget*, GdkEvent* pEvent, gpointer widget) + { + GtkInstancePopover* pThis = static_cast(widget); + return pThis->forward_event_if_popup_under_mouse(pEvent); + } + + static gboolean signalMotion(GtkWidget*, GdkEvent* pEvent, gpointer widget) + { + GtkInstancePopover* pThis = static_cast(widget); + return pThis->forward_event_if_popup_under_mouse(pEvent); + } + + static void signalGrabBroken(GtkWidget*, GdkEventGrabBroken *pEvent, gpointer widget) + { + GtkInstancePopover* pThis = static_cast(widget); + pThis->grab_broken(pEvent); + } + + void grab_broken(const GdkEventGrabBroken *event) + { + if (event->grab_window == nullptr) + { + popdown(); + } + else if (!g_object_get_data(G_OBJECT(event->grab_window), "g-lo-InstancePopup")) // another LibreOffice popover took a grab + { + //try and regrab, so when we lose the grab to the menu of the color palette + //combobox we regain it so the color palette doesn't itself disappear on next + //click on the color palette combobox + do_grab(GTK_WIDGET(m_pMenuHack)); + } + } + +#endif + +public: + GtkInstancePopover(GtkPopover* pPopover, GtkInstanceBuilder* pBuilder, bool bTakeOwnership) +#if !GTK_CHECK_VERSION(4, 0, 0) + : GtkInstanceContainer(GTK_CONTAINER(pPopover), pBuilder, bTakeOwnership) + , m_pMenuHack(nullptr) + , m_bMenuPoppedUp(false) + , m_nButtonPressSeen(false) +#else + : GtkInstanceContainer(GTK_WIDGET(pPopover), pBuilder, bTakeOwnership) +#endif + , m_pPopover(pPopover) + , m_nSignalId(g_signal_connect(m_pPopover, "closed", G_CALLBACK(signalClosed), this)) + , m_pClosedEvent(nullptr) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + //under wayland a Popover will work to "escape" the parent dialog, not + //so under X, so come up with this hack to use a raw GtkWindow + GdkDisplay *pDisplay = gtk_widget_get_display(GTK_WIDGET(m_pPopover)); + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + { + m_pMenuHack = GTK_WINDOW(gtk_window_new(GTK_WINDOW_POPUP)); + gtk_window_set_type_hint(m_pMenuHack, GDK_WINDOW_TYPE_HINT_COMBO); + gtk_window_set_resizable(m_pMenuHack, false); + g_signal_connect(m_pMenuHack, "key-press-event", G_CALLBACK(keyPress), this); + g_signal_connect(m_pMenuHack, "grab-broken-event", G_CALLBACK(signalGrabBroken), this); + g_signal_connect(m_pMenuHack, "button-press-event", G_CALLBACK(signalButtonPress), this); + g_signal_connect(m_pMenuHack, "button-release-event", G_CALLBACK(signalButtonRelease), this); + // to emulate a modeless popover we forward the leave/enter/motion events to the widgets + // they would have gone to a if we were really modeless + if (!gtk_popover_get_modal(m_pPopover)) + { + g_signal_connect(m_pMenuHack, "leave-notify-event", G_CALLBACK(signalButtonCrossing), this); + g_signal_connect(m_pMenuHack, "enter-notify-event", G_CALLBACK(signalButtonCrossing), this); + g_signal_connect(m_pMenuHack, "motion-notify-event", G_CALLBACK(signalMotion), this); + } + } +#endif + } + + virtual void popup_at_rect(weld::Widget* pParent, const tools::Rectangle& rRect, weld::Placement ePlace) override + { + GtkInstanceWidget* pGtkWidget = dynamic_cast(pParent); + assert(pGtkWidget); + + GtkWidget* pWidget = pGtkWidget->getWidget(); + + GdkRectangle aRect; + pWidget = getPopupRect(pWidget, rRect, aRect); + +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_set_parent(GTK_WIDGET(m_pPopover), pWidget); +#else + gtk_popover_set_relative_to(m_pPopover, pWidget); +#endif + gtk_popover_set_pointing_to(m_pPopover, &aRect); + + if (ePlace == weld::Placement::Under) + gtk_popover_set_position(m_pPopover, GTK_POS_BOTTOM); + else + { + if (::SwapForRTL(pWidget)) + gtk_popover_set_position(m_pPopover, GTK_POS_LEFT); + else + gtk_popover_set_position(m_pPopover, GTK_POS_RIGHT); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + //under wayland a Popover will work to "escape" the parent dialog, not + //so under X, so come up with this hack to use a raw GtkWindow + GdkDisplay *pDisplay = gtk_widget_get_display(GTK_WIDGET(m_pPopover)); + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + { + if (!m_bMenuPoppedUp) + { + MovePopoverContentsToWindow(GTK_WIDGET(m_pPopover), m_pMenuHack, pWidget, aRect, ePlace); + m_bMenuPoppedUp = true; + } + return; + } +#endif + + gtk_popover_popup(m_pPopover); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + virtual bool get_visible() const override + { + if (m_pMenuHack) + return gtk_widget_get_visible(GTK_WIDGET(m_pMenuHack)); + return gtk_widget_get_visible(m_pWidget); + } + + virtual void ensureMouseEventWidget() override + { + if (!m_pMouseEventBox && m_pMenuHack) + { + m_pMouseEventBox = GTK_WIDGET(m_pMenuHack); + return; + } + GtkInstanceContainer::ensureMouseEventWidget(); + } +#endif + + virtual void popdown() override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + //under wayland a Popover will work to "escape" the parent dialog, not + //so under X, so come up with this hack to use a raw GtkWindow + GdkDisplay *pDisplay = gtk_widget_get_display(GTK_WIDGET(m_pPopover)); + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + { + if (m_bMenuPoppedUp) + { + m_nButtonPressSeen = false; + MoveWindowContentsToPopover(m_pMenuHack, GTK_WIDGET(m_pPopover), gtk_popover_get_relative_to(m_pPopover)); + m_bMenuPoppedUp = false; + signal_closed(); + } + return; + } +#endif + + gtk_popover_popdown(m_pPopover); + } + + void PopdownAndFlushClosedSignal() + { + if (get_visible()) + popdown(); + if (m_pClosedEvent) + { + Application::RemoveUserEvent(m_pClosedEvent); + async_signal_closed(nullptr); + } + } + + virtual void resize_to_request() override + { + // resizing to request is what gtk does automatically + } + + virtual ~GtkInstancePopover() override + { + PopdownAndFlushClosedSignal(); + DisconnectMouseEvents(); +#if !GTK_CHECK_VERSION(4, 0, 0) + if (m_pMenuHack) + gtk_widget_destroy(GTK_WIDGET(m_pMenuHack)); +#endif + g_signal_handler_disconnect(m_pPopover, m_nSignalId); + } +}; + +IMPL_LINK_NOARG(GtkInstancePopover, async_signal_closed, void*, void) +{ + m_pClosedEvent = nullptr; + signal_closed(); +} + +} + +#if !GTK_CHECK_VERSION(4, 0, 0) + +namespace +{ + +AtkObject* drawing_area_get_accessible(GtkWidget *pWidget) +{ + AtkObject* pDefaultAccessible = default_drawing_area_get_accessible(pWidget); + void* pData = g_object_get_data(G_OBJECT(pWidget), "g-lo-GtkInstanceDrawingArea"); + GtkInstanceDrawingArea* pDrawingArea = static_cast(pData); + AtkObject *pAtkObj = pDrawingArea ? pDrawingArea->GetAtkObject(pDefaultAccessible) : nullptr; + if (pAtkObj) + return pAtkObj; + return pDefaultAccessible; +} + +void ensure_intercept_drawing_area_accessibility() +{ + static bool bDone; + if (!bDone) + { + gpointer pClass = g_type_class_ref(GTK_TYPE_DRAWING_AREA); + GtkWidgetClass* pWidgetClass = GTK_WIDGET_CLASS(pClass); + default_drawing_area_get_accessible = pWidgetClass->get_accessible; + pWidgetClass->get_accessible = drawing_area_get_accessible; + g_type_class_unref(pClass); + bDone = true; + } +} + +void ensure_disable_ctrl_page_up_down(GType eType) +{ + gpointer pClass = g_type_class_ref(eType); + GtkWidgetClass* pWidgetClass = GTK_WIDGET_CLASS(pClass); + GtkBindingSet* pBindingSet = gtk_binding_set_by_class(pWidgetClass); + gtk_binding_entry_remove(pBindingSet, GDK_KEY_Page_Up, GDK_CONTROL_MASK); + gtk_binding_entry_remove(pBindingSet, GDK_KEY_Page_Up, static_cast(GDK_SHIFT_MASK|GDK_CONTROL_MASK)); + gtk_binding_entry_remove(pBindingSet, GDK_KEY_Page_Down, GDK_CONTROL_MASK); + gtk_binding_entry_remove(pBindingSet, GDK_KEY_Page_Down, static_cast(GDK_SHIFT_MASK|GDK_CONTROL_MASK)); + g_type_class_unref(pClass); +} + +// tdf#130400 disable ctrl+page_up and ctrl+page_down bindings so the +// keystrokes are consumed by the surrounding notebook bindings instead +void ensure_disable_ctrl_page_up_down_bindings() +{ + static bool bDone; + if (!bDone) + { + ensure_disable_ctrl_page_up_down(GTK_TYPE_TREE_VIEW); + ensure_disable_ctrl_page_up_down(GTK_TYPE_SPIN_BUTTON); + bDone = true; + } +} + +} +#endif + +namespace { + +bool IsAllowedBuiltInIcon(std::u16string_view iconName) +{ + // limit the named icons to those known by VclBuilder + return VclBuilder::mapStockToSymbol(iconName) != SymbolType::DONTKNOW; +} + +} + +namespace { + +#if !GTK_CHECK_VERSION(4, 0, 0) +void silence_gwarning(const gchar* /*log_domain*/, + GLogLevelFlags /*log_level*/, + const gchar* /*message*/, + gpointer /*user_data*/) +{ +} +#endif + +void load_ui_file(GtkBuilder* pBuilder, const OUString& rUri) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + builder_add_from_gtk3_file(pBuilder, rUri); +#else + guint nLogHandlerId = 0; + GLogLevelFlags nFatalMask(static_cast(G_LOG_FLAG_RECURSION|G_LOG_LEVEL_ERROR)); + if (rUri.endsWith("sfx/ui/tabbarcontents.ui")) + { + // gtk unhelpfully has a bogus warning for the accelerator in this .ui because it assumes menus with accelerators + // if attached to something are attached to a MenuShell, but it's a MenuButton in this case. Turn off warnings, and + // in the case of fatal-warnings temp disable fatal warnings, for this case. + nLogHandlerId = g_log_set_handler("GLib-GObject", + static_cast(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION), + silence_gwarning, nullptr); + nFatalMask = g_log_set_always_fatal(nFatalMask); + } + + OUString aPath; + osl::FileBase::getSystemPathFromFileURL(rUri, aPath); + GError *err = nullptr; + auto rc = gtk_builder_add_from_file(pBuilder, OUStringToOString(aPath, RTL_TEXTENCODING_UTF8).getStr(), &err); + + if (nLogHandlerId) + { + g_log_remove_handler("GLib-GObject", nLogHandlerId); + g_log_set_always_fatal(nFatalMask); + } + + if (!rc) + { + SAL_WARN( "vcl.gtk", "GtkInstanceBuilder: error when calling gtk_builder_add_from_file: " << err->message); + g_error_free(err); + } + assert(rc && "could not load UI file"); +#endif +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +void fix_expander(GtkExpander* pExpander, GParamSpec*, gpointer) +{ + if (gtk_expander_get_resize_toplevel(pExpander)) + { + GtkWidget *pToplevel = widget_get_toplevel(GTK_WIDGET(pExpander)); + + // https://gitlab.gnome.org/GNOME/gtk/issues/70 + // I imagine at some point a release with a fix will be available in which + // case this can be avoided depending on version number + if (pToplevel && GTK_IS_WINDOW(pToplevel) && gtk_widget_get_realized(pToplevel)) + { + int nToplevelWidth, nToplevelHeight; + int nChildHeight; + + GtkWidget* child = gtk_bin_get_child(GTK_BIN(pExpander)); + gtk_widget_get_preferred_height(child, &nChildHeight, nullptr); + gtk_window_get_size(GTK_WINDOW(pToplevel), &nToplevelWidth, &nToplevelHeight); + + if (gtk_expander_get_expanded(pExpander)) + nToplevelHeight += nChildHeight; + else + nToplevelHeight -= nChildHeight; + + gtk_window_resize(GTK_WINDOW(pToplevel), nToplevelWidth, nToplevelHeight); + } + } +} +#endif + +class GtkInstanceBuilder : public weld::Builder +{ +private: + ResHookProc m_pStringReplace; + OUString m_aHelpRoot; + OUString m_aIconTheme; + OUString m_aUILang; + GtkBuilder* m_pBuilder; + GSList* m_pObjectList; + GtkWidget* m_pParentWidget; + gulong m_nNotifySignalId; + std::vector m_aMnemonicButtons; +#if GTK_CHECK_VERSION(4, 0, 0) + std::vector m_aMnemonicCheckButtons; +#endif + std::vector m_aMnemonicLabels; + + VclPtr m_xInterimGlue; + bool m_bAllowCycleFocusOut; + + void postprocess_widget(GtkWidget* pWidget) + { + const bool bHideHelp = comphelper::LibreOfficeKit::isActive() && + officecfg::Office::Common::Help::HelpRootURL::get().isEmpty(); + + //fixup icons + //wanted: better way to do this, e.g. make gtk use gio for + //loading from a filename and provide gio protocol handler + //for our image in a zip urls + // + //unpack the images and keep them as dirs and just + //add the paths to the gtk icon theme dir + if (GTK_IS_IMAGE(pWidget)) + { + GtkImage* pImage = GTK_IMAGE(pWidget); + if (const gchar* icon_name = image_get_icon_name(pImage)) + { + OUString aIconName(icon_name, strlen(icon_name), RTL_TEXTENCODING_UTF8); + if (!IsAllowedBuiltInIcon(aIconName)) + image_set_from_icon_name_theme_lang(pImage, aIconName, m_aIconTheme, m_aUILang); + } + } +#if GTK_CHECK_VERSION(4, 0, 0) + else if (GTK_IS_PICTURE(pWidget)) + { + GtkPicture* pPicture = GTK_PICTURE(pWidget); + if (GFile* icon_file = gtk_picture_get_file(pPicture)) + { + char* icon_name = g_file_get_uri(icon_file); + OUString aIconName(icon_name, strlen(icon_name), RTL_TEXTENCODING_UTF8); + g_free(icon_name); + assert(aIconName.startsWith("private:///graphicrepository/")); + aIconName.startsWith("private:///graphicrepository/", &aIconName); + picture_set_from_icon_name_theme_lang(GTK_PICTURE(pWidget), aIconName, m_aIconTheme, m_aUILang); + } + } +#endif +#if !GTK_CHECK_VERSION(4, 0, 0) + else if (GTK_IS_TOOL_BUTTON(pWidget)) + { + GtkToolButton* pToolButton = GTK_TOOL_BUTTON(pWidget); + if (const gchar* icon_name = gtk_tool_button_get_icon_name(pToolButton)) + { + OUString aIconName(icon_name, strlen(icon_name), RTL_TEXTENCODING_UTF8); + if (!IsAllowedBuiltInIcon(aIconName)) + { + if (GtkWidget* pImage = image_new_from_icon_name_theme_lang(aIconName, m_aIconTheme, m_aUILang)) + { + gtk_tool_button_set_icon_widget(pToolButton, pImage); + gtk_widget_show(pImage); + } + } + } + + // if no tooltip reuse the label as default tooltip + if (!gtk_widget_get_tooltip_text(pWidget)) + { + if (const gchar* label = gtk_tool_button_get_label(pToolButton)) + gtk_widget_set_tooltip_text(pWidget, label); + } + } + else if (GTK_IS_EXPANDER(pWidget)) + { + g_signal_connect(pWidget, "notify::expanded", G_CALLBACK(fix_expander), this); + } +#else + else if (GTK_IS_BUTTON(pWidget)) + { + GtkButton* pButton = GTK_BUTTON(pWidget); + if (const gchar* icon_name = gtk_button_get_icon_name(pButton)) + { + OUString aIconName(icon_name, strlen(icon_name), RTL_TEXTENCODING_UTF8); + if (!IsAllowedBuiltInIcon(aIconName)) + { + if (GtkWidget* pImage = image_new_from_icon_name_theme_lang(aIconName, m_aIconTheme, m_aUILang)) + { + gtk_widget_set_halign(pImage, GTK_ALIGN_CENTER); + gtk_widget_set_valign(pImage, GTK_ALIGN_CENTER); + gtk_button_set_child(pButton, pImage); + gtk_widget_show(pImage); + } + } + } + } + else if (GTK_IS_MENU_BUTTON(pWidget)) + { + GtkMenuButton* pButton = GTK_MENU_BUTTON(pWidget); + if (const gchar* icon_name = gtk_menu_button_get_icon_name(pButton)) + { + OUString aIconName(icon_name, strlen(icon_name), RTL_TEXTENCODING_UTF8); + if (!IsAllowedBuiltInIcon(aIconName)) + { + if (GtkWidget* pImage = image_new_from_icon_name_theme_lang(aIconName, m_aIconTheme, m_aUILang)) + { + gtk_widget_set_halign(pImage, GTK_ALIGN_CENTER); + gtk_widget_set_valign(pImage, GTK_ALIGN_CENTER); + // TODO after gtk 4.6 is released require that version and drop this + static auto menu_button_set_child = reinterpret_cast(dlsym(nullptr, "gtk_menu_button_set_child")); + if (menu_button_set_child) + menu_button_set_child(pButton, pImage); + gtk_widget_show(pImage); + } + } + } + } +#endif + + //set helpids + OUString sBuildableName = ::get_buildable_id(GTK_BUILDABLE(pWidget)); + if (!sBuildableName.isEmpty()) + { + OUString sHelpId = m_aHelpRoot + sBuildableName; + set_help_id(pWidget, sHelpId); + //hook up for extended help + const ImplSVHelpData& aHelpData = ImplGetSVHelpData(); + if (aHelpData.mbBalloonHelp && !GTK_IS_DIALOG(pWidget) && !GTK_IS_ASSISTANT(pWidget)) + { + gtk_widget_set_has_tooltip(pWidget, true); + g_signal_connect(pWidget, "query-tooltip", G_CALLBACK(signalTooltipQuery), nullptr); + } + + if (bHideHelp && sBuildableName == "help") + gtk_widget_hide(pWidget); + } + + if (m_pStringReplace) + { + // tdf#136498 %PRODUCTNAME shown in tool tips + const char* pTooltip = gtk_widget_get_tooltip_text(pWidget); + if (pTooltip && pTooltip[0]) + { + OUString aTooltip(pTooltip, strlen(pTooltip), RTL_TEXTENCODING_UTF8); + aTooltip = (*m_pStringReplace)(aTooltip); + gtk_widget_set_tooltip_text(pWidget, OUStringToOString(aTooltip, RTL_TEXTENCODING_UTF8).getStr()); + } + } + + // expand placeholder and collect potentially missing mnemonics + if (GTK_IS_BUTTON(pWidget)) + { + GtkButton* pButton = GTK_BUTTON(pWidget); + if (m_pStringReplace) + { + OUString aLabel(button_get_label(pButton)); + if (!aLabel.isEmpty()) + button_set_label(pButton, (*m_pStringReplace)(aLabel)); + } + if (gtk_button_get_use_underline(pButton)) + m_aMnemonicButtons.push_back(pButton); + } +#if GTK_CHECK_VERSION(4, 0, 0) + else if (GTK_IS_CHECK_BUTTON(pWidget)) + { + GtkCheckButton* pButton = GTK_CHECK_BUTTON(pWidget); + if (m_pStringReplace) + { + OUString aLabel(get_label(pButton)); + if (!aLabel.isEmpty()) + set_label(pButton, (*m_pStringReplace)(aLabel)); + } + if (gtk_check_button_get_use_underline(pButton)) + m_aMnemonicCheckButtons.push_back(pButton); + } +#endif + else if (GTK_IS_LABEL(pWidget)) + { + GtkLabel* pLabel = GTK_LABEL(pWidget); + if (m_pStringReplace) + { + OUString aLabel(get_label(pLabel)); + if (!aLabel.isEmpty()) + set_label(pLabel, (*m_pStringReplace)(aLabel)); + } + if (gtk_label_get_use_underline(pLabel)) + m_aMnemonicLabels.push_back(pLabel); + } + else if (GTK_IS_TEXT_VIEW(pWidget)) + { + GtkTextView* pTextView = GTK_TEXT_VIEW(pWidget); + if (m_pStringReplace) + { + GtkTextBuffer* pBuffer = gtk_text_view_get_buffer(pTextView); + GtkTextIter start, end; + gtk_text_buffer_get_bounds(pBuffer, &start, &end); + char* pTextStr = gtk_text_buffer_get_text(pBuffer, &start, &end, true); + int nTextLen = pTextStr ? strlen(pTextStr) : 0; + if (nTextLen) + { + OUString sOldText(pTextStr, nTextLen, RTL_TEXTENCODING_UTF8); + OString sText(OUStringToOString((*m_pStringReplace)(sOldText), RTL_TEXTENCODING_UTF8)); + gtk_text_buffer_set_text(pBuffer, sText.getStr(), sText.getLength()); + } + g_free(pTextStr); + } + } +#if !GTK_CHECK_VERSION(4, 0, 0) + else if (GTK_IS_ENTRY(pWidget)) + { + g_signal_connect(pWidget, "key-press-event", G_CALLBACK(signalEntryInsertSpecialCharKeyPress), nullptr); + g_signal_connect(pWidget, "populate-popup", G_CALLBACK(signalEntryPopulatePopup), nullptr); + } +#endif + else if (GTK_IS_WINDOW(pWidget)) + { + if (m_pStringReplace) + { + GtkWindow* pWindow = GTK_WINDOW(pWidget); + set_title(pWindow, (*m_pStringReplace)(get_title(pWindow))); + if (GTK_IS_MESSAGE_DIALOG(pWindow)) + { + GtkMessageDialog* pMessageDialog = GTK_MESSAGE_DIALOG(pWindow); + set_primary_text(pMessageDialog, (*m_pStringReplace)(get_primary_text(pMessageDialog))); + set_secondary_text(pMessageDialog, (*m_pStringReplace)(get_secondary_text(pMessageDialog))); + } + } + } + } + + //GtkBuilder sets translation domain during parse, and unsets it again afterwards. + //In order for GtkBuilder to find the translations bindtextdomain has to be called + //for the domain. So here on the first setting of "domain" we call Translate::Create + //to make sure that happens. Without this, if some other part of LibreOffice has + //used the translation machinery for this domain it will still work, but if it + //hasn't, e.g. tdf#119929, then the translation fails + void translation_domain_set() + { + Translate::Create(gtk_builder_get_translation_domain(m_pBuilder), LanguageTag(m_aUILang)); + g_signal_handler_disconnect(m_pBuilder, m_nNotifySignalId); + } + + static void signalNotify(GObject*, GParamSpec *pSpec, gpointer pData) + { + g_return_if_fail(pSpec != nullptr); + if (strcmp(pSpec->name, "translation-domain") == 0) + { + GtkInstanceBuilder* pBuilder = static_cast(pData); + pBuilder->translation_domain_set(); + } + } + + static void postprocess(gpointer data, gpointer user_data) + { + GObject* pObject = static_cast(data); + if (!GTK_IS_WIDGET(pObject)) + return; + GtkInstanceBuilder* pThis = static_cast(user_data); + pThis->postprocess_widget(GTK_WIDGET(pObject)); + } + + void DisallowCycleFocusOut() + { + assert(!m_bAllowCycleFocusOut); // we only expect this to be called when this holds + + GtkWidget* pTopLevel = widget_get_toplevel(m_pParentWidget); + assert(pTopLevel); + GtkSalFrame* pFrame = GtkSalFrame::getFromWindow(pTopLevel); + assert(pFrame); + // unhook handler and let gtk cycle its own way through this widget's + // children because it has no non-gtk siblings + pFrame->DisallowCycleFocusOut(); + } + + static void signalMap(GtkWidget*, gpointer user_data) + { + GtkInstanceBuilder* pThis = static_cast(user_data); + // tdf#138047 wait until map to do this because the final SalFrame may + // not be the same as at ctor time + pThis->DisallowCycleFocusOut(); + } + + void AllowCycleFocusOut() + { + assert(!m_bAllowCycleFocusOut); // we only expect this to be called when this holds + + GtkWidget* pTopLevel = widget_get_toplevel(m_pParentWidget); + assert(pTopLevel); + GtkSalFrame* pFrame = GtkSalFrame::getFromWindow(pTopLevel); + assert(pFrame); + // rehook handler and let vcl cycle its own way through this widget's + // children + pFrame->AllowCycleFocusOut(); + + // tdf#145567 if the focus is in this hierarchy then, now that we are tearing down, + // move focus to the usual focus candidate for the frame + GtkWindow* pFocusWin = get_active_window(); + GtkWidget* pFocus = pFocusWin ? gtk_window_get_focus(pFocusWin) : nullptr; + bool bHasFocus = pFocus && gtk_widget_is_ancestor(pFocus, pTopLevel); + if (bHasFocus) + pFrame->GrabFocus(); + } + + static void signalUnmap(GtkWidget*, gpointer user_data) + { + GtkInstanceBuilder* pThis = static_cast(user_data); + pThis->AllowCycleFocusOut(); + } + +public: + GtkInstanceBuilder(GtkWidget* pParent, std::u16string_view rUIRoot, const OUString& rUIFile, + SystemChildWindow* pInterimGlue, bool bAllowCycleFocusOut) + : weld::Builder() + , m_pStringReplace(Translate::GetReadStringHook()) + , m_pParentWidget(pParent) + , m_nNotifySignalId(0) + , m_xInterimGlue(pInterimGlue) + , m_bAllowCycleFocusOut(bAllowCycleFocusOut) + { + OUString sHelpRoot(rUIFile); +#if !GTK_CHECK_VERSION(4, 0, 0) + ensure_intercept_drawing_area_accessibility(); + ensure_disable_ctrl_page_up_down_bindings(); +#endif + + sal_Int32 nIdx = sHelpRoot.lastIndexOf('.'); + if (nIdx != -1) + sHelpRoot = sHelpRoot.copy(0, nIdx); + sHelpRoot += "/"; + m_aHelpRoot = sHelpRoot; + m_aIconTheme = Application::GetSettings().GetStyleSettings().DetermineIconTheme(); + m_aUILang = Application::GetSettings().GetUILanguageTag().getBcp47(); + + OUString aUri(rUIRoot + rUIFile); + + m_pBuilder = gtk_builder_new(); + m_nNotifySignalId = g_signal_connect_data(G_OBJECT(m_pBuilder), "notify", G_CALLBACK(signalNotify), this, nullptr, G_CONNECT_AFTER); + + load_ui_file(m_pBuilder, aUri); + + m_pObjectList = gtk_builder_get_objects(m_pBuilder); + g_slist_foreach(m_pObjectList, postprocess, this); + + GenerateMissingMnemonics(); + + if (m_xInterimGlue) + { + assert(m_pParentWidget); + g_object_set_data(G_OBJECT(m_pParentWidget), "InterimWindowGlue", m_xInterimGlue.get()); + + if (!m_bAllowCycleFocusOut) + { + g_signal_connect(G_OBJECT(m_pParentWidget), "map", G_CALLBACK(signalMap), this); + g_signal_connect(G_OBJECT(m_pParentWidget), "unmap", G_CALLBACK(signalUnmap), this); + } + } + } + + void GenerateMissingMnemonics() + { + MnemonicGenerator aMnemonicGenerator('_'); + for (const auto a : m_aMnemonicButtons) + aMnemonicGenerator.RegisterMnemonic(button_get_label(a)); +#if GTK_CHECK_VERSION(4, 0, 0) + for (const auto a : m_aMnemonicCheckButtons) + aMnemonicGenerator.RegisterMnemonic(get_label(a)); +#endif + for (const auto a : m_aMnemonicLabels) + aMnemonicGenerator.RegisterMnemonic(get_label(a)); + + for (const auto a : m_aMnemonicButtons) + { + OUString aLabel(button_get_label(a)); + OUString aNewLabel = aMnemonicGenerator.CreateMnemonic(aLabel); + if (aLabel == aNewLabel) + continue; + button_set_label(a, aNewLabel); + } +#if GTK_CHECK_VERSION(4, 0, 0) + for (const auto a : m_aMnemonicCheckButtons) + { + OUString aLabel(get_label(a)); + OUString aNewLabel = aMnemonicGenerator.CreateMnemonic(aLabel); + if (aLabel == aNewLabel) + continue; + set_label(a, aNewLabel); + } +#endif + for (const auto a : m_aMnemonicLabels) + { + OUString aLabel(get_label(a)); + OUString aNewLabel = aMnemonicGenerator.CreateMnemonic(aLabel); + if (aLabel == aNewLabel) + continue; + set_label(a, aNewLabel); + } + + m_aMnemonicLabels.clear(); +#if GTK_CHECK_VERSION(4, 0, 0) + m_aMnemonicCheckButtons.clear(); +#endif + m_aMnemonicButtons.clear(); + } + + OUString get_current_page_help_id() + { + OUString sPageHelpId; + // check to see if there is a notebook called tabcontrol and get the + // helpid for the current page of that + std::unique_ptr xNotebook(weld_notebook("tabcontrol")); + if (xNotebook) + { + if (GtkInstanceContainer* pPage = dynamic_cast(xNotebook->get_page(xNotebook->get_current_page_ident()))) + { + GtkWidget* pContainer = pPage->getWidget(); + if (GtkWidget* pPageWidget = widget_get_first_child(pContainer)) + sPageHelpId = ::get_help_id(pPageWidget); + } + } + return sPageHelpId; + } + + virtual ~GtkInstanceBuilder() override + { + g_slist_free(m_pObjectList); + g_object_unref(m_pBuilder); + + if (m_xInterimGlue && !m_bAllowCycleFocusOut) + AllowCycleFocusOut(); + + m_xInterimGlue.disposeAndClear(); + } + + //ideally we would have/use weld::Container add and explicitly + //call add when we want to do this, but in the vcl impl the + //parent has to be set when the child is created, so for the + //gtk impl emulate this by doing this implicitly at weld time + void auto_add_parentless_widgets_to_container(GtkWidget* pWidget) + { + if (GTK_IS_POPOVER(pWidget)) + return; + if (GTK_IS_WINDOW(pWidget)) + return; +#if GTK_CHECK_VERSION(4, 0, 0) + if (!gtk_widget_get_parent(pWidget)) + gtk_widget_set_parent(pWidget, m_pParentWidget); +#else + if (widget_get_toplevel(pWidget) == pWidget) + gtk_container_add(GTK_CONTAINER(m_pParentWidget), pWidget); +#endif + } + + virtual std::unique_ptr weld_message_dialog(const OUString &id) override + { + GtkMessageDialog* pMessageDialog = GTK_MESSAGE_DIALOG(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pMessageDialog) + return nullptr; + gtk_window_set_transient_for(GTK_WINDOW(pMessageDialog), GTK_WINDOW(widget_get_toplevel(m_pParentWidget))); + return std::make_unique(pMessageDialog, this, true); + } + + virtual std::unique_ptr weld_assistant(const OUString &id) override + { + GtkAssistant* pAssistant = GTK_ASSISTANT(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pAssistant) + return nullptr; + if (m_pParentWidget) + gtk_window_set_transient_for(GTK_WINDOW(pAssistant), GTK_WINDOW(widget_get_toplevel(m_pParentWidget))); + return std::make_unique(pAssistant, this, true); + } + + virtual std::unique_ptr weld_dialog(const OUString &id) override + { + GtkWindow* pDialog = GTK_WINDOW(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pDialog) + return nullptr; + if (m_pParentWidget) + gtk_window_set_transient_for(pDialog, GTK_WINDOW(widget_get_toplevel(m_pParentWidget))); + return std::make_unique(pDialog, this, true); + } + + virtual std::unique_ptr create_screenshot_window() override + { + GtkWidget* pTopLevel = nullptr; + + for (GSList* l = m_pObjectList; l; l = g_slist_next(l)) + { + GObject* pObj = static_cast(l->data); + + if (!GTK_IS_WIDGET(pObj) || gtk_widget_get_parent(GTK_WIDGET(pObj))) + continue; + + if (!pTopLevel) + pTopLevel = GTK_WIDGET(pObj); + else if (GTK_IS_WINDOW(pObj)) + pTopLevel = GTK_WIDGET(pObj); + } + + if (!pTopLevel) + return nullptr; + + GtkWindow* pDialog; + if (GTK_IS_WINDOW(pTopLevel)) + pDialog = GTK_WINDOW(pTopLevel); + else + { + pDialog = GTK_WINDOW(gtk_dialog_new()); + ::set_help_id(GTK_WIDGET(pDialog), ::get_help_id(pTopLevel)); + + GtkWidget* pContentArea = gtk_dialog_get_content_area(GTK_DIALOG(pDialog)); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_add(GTK_CONTAINER(pContentArea), pTopLevel); + gtk_widget_show_all(pTopLevel); +#else + gtk_box_append(GTK_BOX(pContentArea), pTopLevel); + gtk_widget_show(pTopLevel); +#endif + } + + if (m_pParentWidget) + gtk_window_set_transient_for(pDialog, GTK_WINDOW(widget_get_toplevel(m_pParentWidget))); + return std::make_unique(pDialog, this, true); + } + + virtual std::unique_ptr weld_widget(const OUString &id) override + { + GtkWidget* pWidget = GTK_WIDGET(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pWidget) + return nullptr; + auto_add_parentless_widgets_to_container(pWidget); + return std::make_unique(pWidget, this, false); + } + + virtual std::unique_ptr weld_container(const OUString &id) override + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkContainer* pContainer = GTK_CONTAINER(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); +#else + GtkWidget* pContainer = GTK_WIDGET(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); +#endif + if (!pContainer) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pContainer)); + return std::make_unique(pContainer, this, false); + } + + virtual std::unique_ptr weld_box(const OUString &id) override + { + GtkBox* pBox = GTK_BOX(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pBox) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pBox)); + return std::make_unique(pBox, this, false); + } + + virtual std::unique_ptr weld_paned(const OUString &id) override + { + GtkPaned* pPaned = GTK_PANED(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pPaned) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pPaned)); + return std::make_unique(pPaned, this, false); + } + + virtual std::unique_ptr weld_frame(const OUString &id) override + { + GtkFrame* pFrame = GTK_FRAME(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pFrame) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pFrame)); + return std::make_unique(pFrame, this, false); + } + + virtual std::unique_ptr weld_scrolled_window(const OUString &id, bool bUserManagedScrolling = false) override + { + GtkScrolledWindow* pScrolledWindow = GTK_SCROLLED_WINDOW(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pScrolledWindow) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pScrolledWindow)); + return std::make_unique(pScrolledWindow, this, false, bUserManagedScrolling); + } + + virtual std::unique_ptr weld_notebook(const OUString &id) override + { + GtkNotebook* pNotebook = GTK_NOTEBOOK(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pNotebook) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pNotebook)); + return std::make_unique(pNotebook, this, false); + } + + virtual std::unique_ptr weld_button(const OUString &id) override + { + GtkButton* pButton = GTK_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pButton)); + return std::make_unique(pButton, this, false); + } + + virtual std::unique_ptr weld_menu_button(const OUString &id) override + { + GtkMenuButton* pButton = GTK_MENU_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pButton)); + return std::make_unique(pButton, nullptr, this, false); + } + + virtual std::unique_ptr weld_menu_toggle_button(const OUString &id) override + { + GtkMenuButton* pButton = GTK_MENU_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pButton)); + // gtk doesn't come with exactly the same concept + GtkBuilder* pMenuToggleButton = makeMenuToggleButtonBuilder(); + return std::make_unique(pMenuToggleButton, pButton, this, false); + } + + virtual std::unique_ptr weld_link_button(const OUString &id) override + { + GtkLinkButton* pButton = GTK_LINK_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pButton)); + return std::make_unique(pButton, this, false); + } + + virtual std::unique_ptr weld_toggle_button(const OUString &id) override + { + GtkToggleButton* pToggleButton = GTK_TOGGLE_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pToggleButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pToggleButton)); + return std::make_unique(pToggleButton, this, false); + } + + virtual std::unique_ptr weld_radio_button(const OUString &id) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkCheckButton* pRadioButton = GTK_CHECK_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); +#else + GtkRadioButton* pRadioButton = GTK_RADIO_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); +#endif + if (!pRadioButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pRadioButton)); + return std::make_unique(pRadioButton, this, false); + } + + virtual std::unique_ptr weld_check_button(const OUString &id) override + { + GtkCheckButton* pCheckButton = GTK_CHECK_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pCheckButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pCheckButton)); + return std::make_unique(pCheckButton, this, false); + } + + virtual std::unique_ptr weld_scale(const OUString &id) override + { + GtkScale* pScale = GTK_SCALE(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pScale) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pScale)); + return std::make_unique(pScale, this, false); + } + + virtual std::unique_ptr weld_progress_bar(const OUString &id) override + { + GtkProgressBar* pProgressBar = GTK_PROGRESS_BAR(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pProgressBar) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pProgressBar)); + return std::make_unique(pProgressBar, this, false); + } + + virtual std::unique_ptr weld_level_bar(const OUString& id) override + { + GtkLevelBar* pLevelBar = GTK_LEVEL_BAR(gtk_builder_get_object( + m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pLevelBar) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pLevelBar)); + return std::make_unique(pLevelBar, this, false); + } + + virtual std::unique_ptr weld_spinner(const OUString &id) override + { + GtkSpinner* pSpinner = GTK_SPINNER(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pSpinner) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pSpinner)); + return std::make_unique(pSpinner, this, false); + } + + virtual std::unique_ptr weld_image(const OUString &id) override + { + GtkWidget* pWidget = GTK_WIDGET(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pWidget) + return nullptr; + if (GTK_IS_IMAGE(pWidget)) + { + auto_add_parentless_widgets_to_container(pWidget); + return std::make_unique(GTK_IMAGE(pWidget), this, false); + } +#if GTK_CHECK_VERSION(4, 0, 0) + if (GTK_IS_PICTURE(pWidget)) + { + auto_add_parentless_widgets_to_container(pWidget); + return std::make_unique(GTK_PICTURE(pWidget), this, false); + } +#endif + return nullptr; + } + + virtual std::unique_ptr weld_calendar(const OUString &id) override + { + GtkCalendar* pCalendar = GTK_CALENDAR(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pCalendar) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pCalendar)); + return std::make_unique(pCalendar, this, false); + } + + virtual std::unique_ptr weld_entry(const OUString &id) override + { + GtkEntry* pEntry = GTK_ENTRY(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pEntry) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pEntry)); + return std::make_unique(pEntry, this, false); + } + + virtual std::unique_ptr weld_spin_button(const OUString &id) override + { + GtkSpinButton* pSpinButton = GTK_SPIN_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pSpinButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pSpinButton)); + return std::make_unique(pSpinButton, this, false); + } + + virtual std::unique_ptr weld_metric_spin_button(const OUString& id, FieldUnit eUnit) override + { + return std::make_unique(weld_spin_button(id), eUnit); + } + + virtual std::unique_ptr weld_formatted_spin_button(const OUString &id) override + { + GtkSpinButton* pSpinButton = GTK_SPIN_BUTTON(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pSpinButton) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pSpinButton)); + return std::make_unique(pSpinButton, this, false); + } + + virtual std::unique_ptr weld_combo_box(const OUString &id) override + { + GtkComboBox* pComboBox = GTK_COMBO_BOX(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pComboBox) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pComboBox)); + +#if GTK_CHECK_VERSION(4, 0, 0) + return std::make_unique(pComboBox, this, false); +#else + /* we replace GtkComboBox because of difficulties with too tall menus + + 1) https://gitlab.gnome.org/GNOME/gtk/issues/1910 + has_entry long menus take forever to appear (tdf#125388) + + on measuring each row, the GtkComboBox GtkTreeMenu will call + its area_apply_attributes_cb function on the row, but that calls + gtk_tree_menu_get_path_item which then loops through each child of the + menu looking for the widget of the row, so performance drops to useless. + + All area_apply_attributes_cb does it set menu item sensitivity, so block it from running + with fragile hackery which assumes that the unwanted callback is the only one with a + + 2) https://gitlab.gnome.org/GNOME/gtk/issues/94 + when a super tall combobox menu is activated, and the selected + entry is sufficiently far down the list, then the menu doesn't + appear under wayland + + 3) https://gitlab.gnome.org/GNOME/gtk/issues/310 + no typeahead support + + 4) we want to be able to control the width of the button, but have a drop down menu which + is not limited to the width of the button + + 5) https://bugs.documentfoundation.org/show_bug.cgi?id=131120 + super tall menu doesn't appear under X sometimes + */ + GtkBuilder* pComboBuilder = makeComboBoxBuilder(); + return std::make_unique(pComboBuilder, pComboBox, this, false); +#endif + } + + virtual std::unique_ptr weld_tree_view(const OUString &id) override + { + GtkTreeView* pTreeView = GTK_TREE_VIEW(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pTreeView) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pTreeView)); + return std::make_unique(pTreeView, this, false); + } + + virtual std::unique_ptr weld_icon_view(const OUString &id) override + { + GtkIconView* pIconView = GTK_ICON_VIEW(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pIconView) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pIconView)); + return std::make_unique(pIconView, this, false); + } + + virtual std::unique_ptr weld_entry_tree_view(const OUString& containerid, const OUString& entryid, const OUString& treeviewid) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pContainer = GTK_WIDGET(gtk_builder_get_object(m_pBuilder, OUStringToOString(containerid, RTL_TEXTENCODING_UTF8).getStr())); +#else + GtkContainer* pContainer = GTK_CONTAINER(gtk_builder_get_object(m_pBuilder, OUStringToOString(containerid, RTL_TEXTENCODING_UTF8).getStr())); +#endif + if (!pContainer) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pContainer)); + return std::make_unique(pContainer, this, false, + weld_entry(entryid), + weld_tree_view(treeviewid)); + } + + virtual std::unique_ptr weld_label(const OUString &id) override + { + GtkLabel* pLabel = GTK_LABEL(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pLabel) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pLabel)); + return std::make_unique(pLabel, this, false); + } + + virtual std::unique_ptr weld_text_view(const OUString &id) override + { + GtkTextView* pTextView = GTK_TEXT_VIEW(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pTextView) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pTextView)); + return std::make_unique(pTextView, this, false); + } + + virtual std::unique_ptr weld_expander(const OUString &id) override + { + GtkExpander* pExpander = GTK_EXPANDER(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pExpander) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pExpander)); + return std::make_unique(pExpander, this, false); + } + + virtual std::unique_ptr weld_drawing_area(const OUString &id, const a11yref& rA11y, + FactoryFunction /*pUITestFactoryFunction*/, void* /*pUserData*/) override + { + GtkDrawingArea* pDrawingArea = GTK_DRAWING_AREA(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pDrawingArea) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pDrawingArea)); + return std::make_unique(pDrawingArea, this, rA11y, false); + } + + virtual std::unique_ptr weld_menu(const OUString &id) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkPopoverMenu* pMenu = GTK_POPOVER_MENU(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); +#else + GtkMenu* pMenu = GTK_MENU(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); +#endif + if (!pMenu) + return nullptr; + return std::make_unique(pMenu, true); + } + + virtual std::unique_ptr weld_popover(const OUString &id) override + { + GtkPopover* pPopover = GTK_POPOVER(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pPopover) + return nullptr; +#if GTK_CHECK_VERSION(4, 0, 0) + return std::make_unique(pPopover, this, false); +#else + return std::make_unique(pPopover, this, true); +#endif + } + + virtual std::unique_ptr weld_toolbar(const OUString &id) override + { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkBox* pToolbar = GTK_BOX(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); +#else + GtkToolbar* pToolbar = GTK_TOOLBAR(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); +#endif + if (!pToolbar) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pToolbar)); + return std::make_unique(pToolbar, this, false); + } + + virtual std::unique_ptr weld_scrollbar(const OUString &id) override + { + GtkScrollbar* pScrollbar = GTK_SCROLLBAR(gtk_builder_get_object(m_pBuilder, OUStringToOString(id, RTL_TEXTENCODING_UTF8).getStr())); + if (!pScrollbar) + return nullptr; + auto_add_parentless_widgets_to_container(GTK_WIDGET(pScrollbar)); + return std::make_unique(pScrollbar, this, false); + } + + virtual std::unique_ptr create_size_group() override + { + return std::make_unique(); + } +}; + +} + +void GtkInstanceWindow::help() +{ + //show help for widget with keyboard focus + GtkWidget* pWidget = gtk_window_get_focus(m_pWindow); + if (!pWidget) + pWidget = GTK_WIDGET(m_pWindow); + OUString sHelpId = ::get_help_id(pWidget); + while (sHelpId.isEmpty()) + { + pWidget = gtk_widget_get_parent(pWidget); + if (!pWidget) + break; + sHelpId = ::get_help_id(pWidget); + } + std::unique_ptr xTemp(pWidget != m_pWidget ? new GtkInstanceWidget(pWidget, m_pBuilder, false) : nullptr); + weld::Widget* pSource = xTemp ? xTemp.get() : this; + bool bRunNormalHelpRequest = !m_aHelpRequestHdl.IsSet() || m_aHelpRequestHdl.Call(*pSource); + Help* pHelp = bRunNormalHelpRequest ? Application::GetHelp() : nullptr; + if (!pHelp) + return; + +#if !GTK_CHECK_VERSION(4, 0, 0) + // tdf#126007, there's a nice fallback route for offline help where + // the current page of a notebook will get checked when the help + // button is pressed and there was no help for the dialog found. + // + // But for online help that route doesn't get taken, so bodge this here + // by using the page help id if available and if the help button itself + // was the original id + if (m_pBuilder && sHelpId.endsWith("/help")) + { + OUString sPageId = m_pBuilder->get_current_page_help_id(); + if (!sPageId.isEmpty()) + sHelpId = sPageId; + else + { + // tdf#129068 likewise the help for the wrapping dialog is less + // helpful than the help for the content area could be + GtkContainer* pContainer = nullptr; + if (GTK_IS_DIALOG(m_pWindow)) + pContainer = GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(m_pWindow))); + else if (GTK_IS_ASSISTANT(m_pWindow)) + { + GtkAssistant* pAssistant = GTK_ASSISTANT(m_pWindow); + pContainer = GTK_CONTAINER(gtk_assistant_get_nth_page(pAssistant, gtk_assistant_get_current_page(pAssistant))); + } + if (pContainer) + { + GtkWidget* pContentWidget = widget_get_first_child(GTK_WIDGET(pContainer)); + if (pContentWidget) + sHelpId = ::get_help_id(pContentWidget); + } + } + } +#endif + pHelp->Start(sHelpId, pSource); +} + +//iterate upwards through the hierarchy from this widgets through its parents +//calling func with their helpid until func returns true or we run out of parents +void GtkInstanceWidget::help_hierarchy_foreach(const std::function& func) +{ + GtkWidget* pParent = m_pWidget; + while ((pParent = gtk_widget_get_parent(pParent))) + { + if (func(::get_help_id(pParent))) + return; + } +} + +std::unique_ptr GtkInstance::CreateBuilder(weld::Widget* pParent, const OUString& rUIRoot, const OUString& rUIFile) +{ + GtkInstanceWidget* pParentWidget = dynamic_cast(pParent); + GtkWidget* pBuilderParent = pParentWidget ? pParentWidget->getWidget() : nullptr; + return std::make_unique(pBuilderParent, rUIRoot, rUIFile, nullptr, true); +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +// tdf#135965 for the case of native widgets inside a GtkSalFrame and F1 pressed, run help +// on gtk widget help ids until we hit a vcl parent and then use vcl window help ids +gboolean GtkSalFrame::NativeWidgetHelpPressed(GtkAccelGroup*, GObject*, guint, GdkModifierType, gpointer pFrame) +{ + Help* pHelp = Application::GetHelp(); + if (!pHelp) + return true; + + GtkWindow* pWindow = static_cast(pFrame); + + vcl::Window* pChildWindow = nullptr; + + //show help for widget with keyboard focus + GtkWidget* pWidget = gtk_window_get_focus(pWindow); + if (!pWidget) + pWidget = GTK_WIDGET(pWindow); + OUString sHelpId = ::get_help_id(pWidget); + while (sHelpId.isEmpty()) + { + pWidget = gtk_widget_get_parent(pWidget); + if (!pWidget) + break; + pChildWindow = static_cast(g_object_get_data(G_OBJECT(pWidget), "InterimWindowGlue")); + if (pChildWindow) + { + sHelpId = pChildWindow->GetHelpId(); + break; + } + sHelpId = ::get_help_id(pWidget); + } + + if (pChildWindow) + { + while (sHelpId.isEmpty()) + { + pChildWindow = pChildWindow->GetParent(); + if (!pChildWindow) + break; + sHelpId = pChildWindow->GetHelpId(); + } + if (!pChildWindow) + return true; + pHelp->Start(sHelpId, pChildWindow); + return true; + } + + if (!pWidget) + return true; + std::unique_ptr xTemp(new GtkInstanceWidget(pWidget, nullptr, false)); + pHelp->Start(sHelpId, xTemp.get()); + return true; +} +#endif + +std::unique_ptr GtkInstance::CreateInterimBuilder(vcl::Window* pParent, const OUString& rUIRoot, const OUString& rUIFile, + bool bAllowCycleFocusOut, sal_uInt64) +{ + // Create a foreign window which we know is a GtkGrid and make the native widgets a child of that, so we can + // support GtkWidgets within a vcl::Window + SystemWindowData winData = {}; + winData.bClipUsingNativeWidget = true; + auto xEmbedWindow = VclPtr::Create(pParent, 0, &winData, false); + xEmbedWindow->Show(true, ShowFlags::NoActivate); + xEmbedWindow->set_expand(true); + + const SystemEnvData* pEnvData = xEmbedWindow->GetSystemData(); + if (!pEnvData) + return nullptr; + + GtkWidget *pWindow = static_cast(pEnvData->pWidget); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_show_all(pWindow); +#else + gtk_widget_show(pWindow); +#endif + + // build the widget tree as a child of the GtkEventBox GtkGrid parent + return std::make_unique(pWindow, rUIRoot, rUIFile, xEmbedWindow.get(), bAllowCycleFocusOut); +} + +weld::MessageDialog* GtkInstance::CreateMessageDialog(weld::Widget* pParent, VclMessageType eMessageType, VclButtonsType eButtonsType, const OUString &rPrimaryMessage) +{ + GtkInstanceWidget* pParentInstance = dynamic_cast(pParent); + GtkWindow* pParentWindow = pParentInstance ? pParentInstance->getWindow() : nullptr; + GtkMessageDialog* pMessageDialog = GTK_MESSAGE_DIALOG(gtk_message_dialog_new(pParentWindow, GTK_DIALOG_MODAL, + VclToGtk(eMessageType), VclToGtk(eButtonsType), "%s", + OUStringToOString(rPrimaryMessage, RTL_TEXTENCODING_UTF8).getStr())); + return new GtkInstanceMessageDialog(pMessageDialog, nullptr, true); +} + +weld::Window* GtkInstance::GetFrameWeld(const css::uno::Reference& rWindow) +{ + if (SalGtkXWindow* pGtkXWindow = dynamic_cast(rWindow.get())) + return pGtkXWindow->getFrameWeld(); + return SalInstance::GetFrameWeld(rWindow); +} + +weld::Window* GtkSalFrame::GetFrameWeld() const +{ + if (!m_xFrameWeld) + m_xFrameWeld.reset(new GtkInstanceWindow(GTK_WINDOW(widget_get_toplevel(getWindow())), nullptr, false)); + return m_xFrameWeld.get(); +} + +void* GtkInstance::CreateGStreamerSink(const SystemChildWindow *pWindow) +{ +#if ENABLE_GSTREAMER_1_0 + auto aSymbol = gstElementFactoryNameSymbol(); + if (!aSymbol) + return nullptr; + + const SystemEnvData* pEnvData = pWindow->GetSystemData(); + if (!pEnvData) + return nullptr; + + GstElement* pVideosink = aSymbol("gtksink", "gtksink"); + if (!pVideosink) + return nullptr; + + GtkWidget *pGstWidget; + g_object_get(pVideosink, "widget", &pGstWidget, nullptr); + gtk_widget_set_vexpand(pGstWidget, true); + gtk_widget_set_hexpand(pGstWidget, true); + + GtkWidget *pParent = static_cast(pEnvData->pWidget); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_add(GTK_CONTAINER(pParent), pGstWidget); +#endif + g_object_unref(pGstWidget); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_show_all(pParent); +#else + gtk_widget_show(pParent); +#endif + + return pVideosink; +#else + (void)pWindow; + return nullptr; +#endif +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtkobject.cxx b/vcl/unx/gtk3/gtkobject.cxx new file mode 100644 index 0000000000..bd553137a8 --- /dev/null +++ b/vcl/unx/gtk3/gtkobject.cxx @@ -0,0 +1,611 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include +#include +#include + +GtkSalObjectBase::GtkSalObjectBase(GtkSalFrame* pParent) + : m_pSocket(nullptr) + , m_pParent(pParent) + , m_pRegion(nullptr) +{ +} + +GtkSalObject::GtkSalObject(GtkSalFrame* pParent, bool bShow) + : GtkSalObjectBase(pParent) +{ + if (!m_pParent) + return; + + // our plug window + m_pSocket = gtk_grid_new(); + Show( bShow ); + // insert into container + gtk_fixed_put( pParent->getFixedContainer(), + m_pSocket, + 0, 0 ); + + Init(); + + g_signal_connect( G_OBJECT(m_pSocket), "destroy", G_CALLBACK(signalDestroy), this ); + + // #i59255# necessary due to sync effects with java child windows + pParent->Flush(); +} + +void GtkSalObjectBase::Init() +{ + // realize so we can get a window id + gtk_widget_realize( m_pSocket ); + + // system data + // tdf#139609 deliberately defer using m_pParent->GetNativeWindowHandle(m_pSocket)) to set m_aSystemData.aWindow + // unless its explicitly needed + m_aSystemData.aShellWindow = reinterpret_cast(this); + m_aSystemData.pSalFrame = nullptr; + m_aSystemData.pWidget = m_pSocket; + m_aSystemData.nScreen = m_pParent->getXScreenNumber().getXScreen(); + m_aSystemData.toolkit = SystemEnvData::Toolkit::Gtk; +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkScreen* pScreen = gtk_widget_get_screen(m_pParent->getWindow()); + GdkVisual* pVisual = gdk_screen_get_system_visual(pScreen); + +#if defined(GDK_WINDOWING_X11) + GdkDisplay *pDisplay = GtkSalFrame::getGdkDisplay(); + if (DLSYM_GDK_IS_X11_DISPLAY(pDisplay)) + { + m_aSystemData.pDisplay = gdk_x11_display_get_xdisplay(pDisplay); + m_aSystemData.pVisual = gdk_x11_visual_get_xvisual(pVisual); + m_aSystemData.platform = SystemEnvData::Platform::Xcb; + } +#endif +#if defined(GDK_WINDOWING_WAYLAND) + if (DLSYM_GDK_IS_WAYLAND_DISPLAY(pDisplay)) + { + m_aSystemData.pDisplay = gdk_wayland_display_get_wl_display(pDisplay); + m_aSystemData.platform = SystemEnvData::Platform::Wayland; + } +#endif + + g_signal_connect( G_OBJECT(m_pSocket), "button-press-event", G_CALLBACK(signalButton), this ); + g_signal_connect( G_OBJECT(m_pSocket), "button-release-event", G_CALLBACK(signalButton), this ); + g_signal_connect( G_OBJECT(m_pSocket), "focus-in-event", G_CALLBACK(signalFocus), this ); + g_signal_connect( G_OBJECT(m_pSocket), "focus-out-event", G_CALLBACK(signalFocus), this ); +#endif +} + +GtkSalObjectBase::~GtkSalObjectBase() +{ + if( m_pRegion ) + { + cairo_region_destroy( m_pRegion ); + } +} + +GtkSalObject::~GtkSalObject() +{ + if( !m_pSocket ) + return; + +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_unparent(m_pSocket); +#else + // remove socket from parent frame's fixed container + gtk_container_remove( GTK_CONTAINER(gtk_widget_get_parent(m_pSocket)), + m_pSocket ); + // get rid of the socket + // actually the gtk_container_remove should let the ref count + // of the socket sink to 0 and destroy it (see signalDestroy) + // this is just a sanity check + if( m_pSocket ) + gtk_widget_destroy( m_pSocket ); +#endif +} + +void GtkSalObject::ResetClipRegion() +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + if( m_pSocket ) + gdk_window_shape_combine_region( widget_get_surface(m_pSocket), nullptr, 0, 0 ); +#endif +} + +void GtkSalObjectBase::BeginSetClipRegion( sal_uInt32 ) +{ + if (m_pRegion) + cairo_region_destroy(m_pRegion); + m_pRegion = cairo_region_create(); +} + +void GtkSalObjectBase::UnionClipRegion( tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight ) +{ + GdkRectangle aRect; + aRect.x = nX; + aRect.y = nY; + aRect.width = nWidth; + aRect.height = nHeight; + + cairo_region_union_rectangle( m_pRegion, &aRect ); +} + +void GtkSalObject::EndSetClipRegion() +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + if( m_pSocket ) + gdk_window_shape_combine_region( widget_get_surface(m_pSocket), m_pRegion, 0, 0 ); +#endif +} + +void GtkSalObject::SetPosSize(tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight) +{ + if (m_pSocket) + { + GtkFixed* pContainer = GTK_FIXED(gtk_widget_get_parent(m_pSocket)); + gtk_fixed_move( pContainer, m_pSocket, nX, nY ); + gtk_widget_set_size_request( m_pSocket, nWidth, nHeight ); +#if !GTK_CHECK_VERSION(4, 0, 0) + m_pParent->nopaint_container_resize_children(GTK_CONTAINER(pContainer)); +#endif + } +} + +void GtkSalObject::Reparent(SalFrame* pFrame) +{ + GtkSalFrame* pNewParent = static_cast(pFrame); + if (m_pSocket) + { + GtkFixed* pContainer = GTK_FIXED(gtk_widget_get_parent(m_pSocket)); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gint nX(0), nY(0); + gtk_container_child_get(GTK_CONTAINER(pContainer), m_pSocket, + "x", &nX, + "y", &nY, + nullptr); +#else + double nX(0), nY(0); + gtk_fixed_get_child_position(pContainer, m_pSocket, &nX, &nY); +#endif + + g_object_ref(m_pSocket); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_remove(GTK_CONTAINER(pContainer), m_pSocket); +#else + gtk_fixed_remove(pContainer, m_pSocket); +#endif + + gtk_fixed_put(pNewParent->getFixedContainer(), + m_pSocket, + nX, nY); + + g_object_unref(m_pSocket); + } + m_pParent = pNewParent; +} + +void GtkSalObject::Show( bool bVisible ) +{ + if( m_pSocket ) + { + if( bVisible ) + gtk_widget_show(m_pSocket); + else + gtk_widget_hide(m_pSocket); + } +} + +Size GtkSalObjectBase::GetOptimalSize() const +{ + if (m_pSocket) + { + bool bVisible = gtk_widget_get_visible(m_pSocket); + if (!bVisible) + gtk_widget_set_visible(m_pSocket, true); + + // Undo SetPosSize before getting its preferred size + gint width(-1), height(-1); + gtk_widget_get_size_request(m_pSocket, &width, &height); + gtk_widget_set_size_request(m_pSocket, -1, -1); + + GtkRequisition size; + gtk_widget_get_preferred_size(m_pSocket, nullptr, &size); + + // Restore SetPosSize size + gtk_widget_set_size_request(m_pSocket, width, height); + + if (!bVisible) + gtk_widget_set_visible(m_pSocket, false); + return Size(size.width, size.height); + } + return Size(); +} + +const SystemEnvData* GtkSalObjectBase::GetSystemData() const +{ + return &m_aSystemData; +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +gboolean GtkSalObjectBase::signalButton( GtkWidget*, GdkEventButton* pEvent, gpointer object ) +{ + GtkSalObjectBase* pThis = static_cast(object); + + if( pEvent->type == GDK_BUTTON_PRESS ) + { + pThis->CallCallback( SalObjEvent::ToTop ); + } + + return FALSE; +} + +gboolean GtkSalObjectBase::signalFocus( GtkWidget*, GdkEventFocus* pEvent, gpointer object ) +{ + GtkSalObjectBase* pThis = static_cast(object); + + pThis->CallCallback( pEvent->in ? SalObjEvent::GetFocus : SalObjEvent::LoseFocus ); + + return FALSE; +} +#endif + +void GtkSalObject::signalDestroy( GtkWidget* pObj, gpointer object ) +{ + GtkSalObject* pThis = static_cast(object); + if( pObj == pThis->m_pSocket ) + { + pThis->m_pSocket = nullptr; + } +} + +void GtkSalObjectBase::SetForwardKey( bool bEnable ) +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + if( bEnable ) + gtk_widget_add_events( GTK_WIDGET( m_pSocket ), GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK ); + else + gtk_widget_set_events( GTK_WIDGET( m_pSocket ), ~(GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK) & gtk_widget_get_events( GTK_WIDGET( m_pSocket ) ) ); +#else + (void)bEnable; +#endif +} + +GtkSalObjectWidgetClip::GtkSalObjectWidgetClip(GtkSalFrame* pParent, bool bShow) + : GtkSalObjectBase(pParent) + , m_pScrolledWindow(nullptr) + , m_pViewPort(nullptr) + , m_pBgCssProvider(nullptr) +{ + if( !pParent ) + return; + +#if !GTK_CHECK_VERSION(4, 0, 0) + m_pScrolledWindow = gtk_scrolled_window_new(nullptr, nullptr); + g_signal_connect(m_pScrolledWindow, "scroll-event", G_CALLBACK(signalScroll), this); +#else + m_pScrolledWindow = gtk_scrolled_window_new(); + GtkEventController* pScrollController = gtk_event_controller_scroll_new(GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES); + g_signal_connect(pScrollController, "scroll", G_CALLBACK(signalScroll), this); + gtk_widget_add_controller(m_pScrolledWindow, pScrollController); +#endif + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(m_pScrolledWindow), + GTK_POLICY_EXTERNAL, GTK_POLICY_EXTERNAL); + + // insert into container + gtk_fixed_put( pParent->getFixedContainer(), + m_pScrolledWindow, + 0, 0 ); + + // deliberately without adjustments to avoid gtk's auto adjustment on changing focus + m_pViewPort = gtk_viewport_new(nullptr, nullptr); + + // force in a fake background of a suitable color + SetViewPortBackground(); + + ImplGetDefaultWindow()->AddEventListener(LINK(this, GtkSalObjectWidgetClip, SettingsChangedHdl)); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_add(GTK_CONTAINER(m_pScrolledWindow), m_pViewPort); +#else + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(m_pScrolledWindow), m_pViewPort); +#endif + gtk_widget_show(m_pViewPort); + + // our plug window + m_pSocket = gtk_grid_new(); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_add(GTK_CONTAINER(m_pViewPort), m_pSocket); +#else + gtk_viewport_set_child(GTK_VIEWPORT(m_pViewPort), m_pSocket); +#endif + gtk_widget_show(m_pSocket); + + Show(bShow); + + Init(); + + g_signal_connect( G_OBJECT(m_pSocket), "destroy", G_CALLBACK(signalDestroy), this ); +} + +// force in a fake background of a suitable color +void GtkSalObjectWidgetClip::SetViewPortBackground() +{ + GtkStyleContext *pWidgetContext = gtk_widget_get_style_context(m_pViewPort); + if (m_pBgCssProvider) + gtk_style_context_remove_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pBgCssProvider)); + m_pBgCssProvider = gtk_css_provider_new(); + OUString sColor = Application::GetSettings().GetStyleSettings().GetDialogColor().AsRGBHexString(); + OUString aBuffer = "* { background-color: #" + sColor + "; }"; + OString aResult = OUStringToOString(aBuffer, RTL_TEXTENCODING_UTF8); + css_provider_load_from_data(m_pBgCssProvider, aResult.getStr(), aResult.getLength()); + gtk_style_context_add_provider(pWidgetContext, GTK_STYLE_PROVIDER(m_pBgCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +IMPL_LINK(GtkSalObjectWidgetClip, SettingsChangedHdl, VclWindowEvent&, rEvent, void) +{ + if (rEvent.GetId() != VclEventId::WindowDataChanged) + return; + + DataChangedEvent* pData = static_cast(rEvent.GetData()); + if (pData->GetType() == DataChangedEventType::SETTINGS) + SetViewPortBackground(); +} + +GtkSalObjectWidgetClip::~GtkSalObjectWidgetClip() +{ + ImplGetDefaultWindow()->RemoveEventListener(LINK(this, GtkSalObjectWidgetClip, SettingsChangedHdl)); + + if( !m_pSocket ) + return; + + // remove socket from parent frame's fixed container +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_remove( GTK_CONTAINER(gtk_widget_get_parent(m_pScrolledWindow)), + m_pScrolledWindow ); + + // get rid of the socket + // actually the gtk_container_remove should let the ref count + // of the socket sink to 0 and destroy it (see signalDestroy) + // this is just a sanity check + if( m_pScrolledWindow ) + gtk_widget_destroy( m_pScrolledWindow ); +#else + gtk_fixed_remove(GTK_FIXED(gtk_widget_get_parent(m_pScrolledWindow)), + m_pScrolledWindow); +#endif +} + +void GtkSalObjectWidgetClip::ResetClipRegion() +{ + m_aClipRect = tools::Rectangle(); + ApplyClipRegion(); +} + +void GtkSalObjectWidgetClip::EndSetClipRegion() +{ + int nRects = cairo_region_num_rectangles(m_pRegion); + assert(nRects == 0 || nRects == 1); + if (nRects == 0) + m_aClipRect = tools::Rectangle(); + else + { + cairo_rectangle_int_t rectangle; + cairo_region_get_rectangle(m_pRegion, 0, &rectangle); + m_aClipRect = tools::Rectangle(Point(rectangle.x, rectangle.y), Size(rectangle.width, rectangle.height)); + } + ApplyClipRegion(); +} + +void GtkSalObjectWidgetClip::ApplyClipRegion() +{ + if( !m_pSocket ) + return; + + GtkFixed* pContainer = GTK_FIXED(gtk_widget_get_parent(m_pScrolledWindow)); + + GtkAllocation allocation; + allocation.x = m_aRect.Left() + m_aClipRect.Left(); + allocation.y = m_aRect.Top() + m_aClipRect.Top(); + if (m_aClipRect.IsEmpty()) + { + allocation.width = m_aRect.GetWidth(); + allocation.height = m_aRect.GetHeight(); + } + else + { + allocation.width = m_aClipRect.GetWidth(); + allocation.height = m_aClipRect.GetHeight(); + } + + if (AllSettings::GetLayoutRTL()) + { + GtkAllocation aParentAllocation; + gtk_widget_get_allocation(GTK_WIDGET(pContainer), &aParentAllocation); + gtk_fixed_move(pContainer, m_pScrolledWindow, aParentAllocation.width - allocation.width - 1 - allocation.x, allocation.y); + } + else + gtk_fixed_move(pContainer, m_pScrolledWindow, allocation.x, allocation.y); + gtk_widget_set_size_request(m_pScrolledWindow, allocation.width, allocation.height); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_size_allocate(m_pScrolledWindow, &allocation); +#else + gtk_widget_size_allocate(m_pScrolledWindow, &allocation, 0); +#endif + + gtk_adjustment_set_value(gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(m_pScrolledWindow)), m_aClipRect.Left()); + gtk_adjustment_set_value(gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(m_pScrolledWindow)), m_aClipRect.Top()); +} + +void GtkSalObjectWidgetClip::SetPosSize(tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight) +{ + m_aRect = tools::Rectangle(Point(nX, nY), Size(nWidth, nHeight)); + if (m_pSocket) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkFixed* pContainer = GTK_FIXED(gtk_widget_get_parent(m_pScrolledWindow)); +#endif + gtk_widget_set_size_request(m_pSocket, nWidth, nHeight); + ApplyClipRegion(); +#if !GTK_CHECK_VERSION(4, 0, 0) + m_pParent->nopaint_container_resize_children(GTK_CONTAINER(pContainer)); +#endif + } +} + +void GtkSalObjectWidgetClip::Reparent(SalFrame* pFrame) +{ + GtkSalFrame* pNewParent = static_cast(pFrame); + if (m_pSocket) + { + GtkFixed* pContainer = GTK_FIXED(gtk_widget_get_parent(m_pScrolledWindow)); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gint nX(0), nY(0); + gtk_container_child_get(GTK_CONTAINER(pContainer), m_pScrolledWindow, + "x", &nX, + "y", &nY, + nullptr); +#else + double nX(0), nY(0); + gtk_fixed_get_child_position(pContainer, m_pScrolledWindow, &nX, &nY); +#endif + + g_object_ref(m_pScrolledWindow); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_remove(GTK_CONTAINER(pContainer), m_pScrolledWindow); +#else + gtk_fixed_remove(pContainer, m_pScrolledWindow); +#endif + + gtk_fixed_put(pNewParent->getFixedContainer(), + m_pScrolledWindow, + nX, nY); + + g_object_unref(m_pScrolledWindow); + } + m_pParent = pNewParent; +} + +void GtkSalObjectWidgetClip::Show( bool bVisible ) +{ + if (!m_pSocket) + return; + bool bCurrentVis = gtk_widget_get_visible(m_pScrolledWindow); + if (bVisible == bCurrentVis) + return; + if( bVisible ) + { + gtk_widget_show(m_pScrolledWindow); + // tdf#146641 allocations attempted while hidden are discarded by gtk, + // so on transition to visible ApplyClipRegion needs to be called + ApplyClipRegion(); + } + else + { + // on hiding the widget, if a child has focus gtk will want to move the focus out of the widget + // but we want to keep the focus where it is, e.g. writer's comments in margin feature put + // cursor in a sidebar comment and scroll the page so the comment is invisible, we want the focus + // to stay in the invisible widget, so its there when we scroll back or on a keypress the widget + // gets the keystroke and scrolls back to make it visible again + GtkWidget* pTopLevel = widget_get_toplevel(m_pScrolledWindow); + GtkWidget* pOldFocus = GTK_IS_WINDOW(pTopLevel) ? gtk_window_get_focus(GTK_WINDOW(pTopLevel)) : nullptr; + + g_object_set_data(G_OBJECT(pTopLevel), "g-lo-BlockFocusChange", GINT_TO_POINTER(true) ); + + gtk_widget_hide(m_pScrolledWindow); + + GtkWidget* pNewFocus = GTK_IS_WINDOW(pTopLevel) ? gtk_window_get_focus(GTK_WINDOW(pTopLevel)) : nullptr; + if (pOldFocus && pOldFocus != pNewFocus) + gtk_widget_grab_focus(pOldFocus); + + g_object_set_data(G_OBJECT(pTopLevel), "g-lo-BlockFocusChange", GINT_TO_POINTER(false) ); + } +} + +void GtkSalObjectWidgetClip::signalDestroy( GtkWidget* pObj, gpointer object ) +{ + GtkSalObjectWidgetClip* pThis = static_cast(object); + if( pObj == pThis->m_pSocket ) + { + pThis->m_pSocket = nullptr; + pThis->m_pScrolledWindow = nullptr; + } +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +gboolean GtkSalObjectWidgetClip::signalScroll(GtkWidget* pScrolledWindow, GdkEvent* pEvent, gpointer object) +{ + GtkSalObjectWidgetClip* pThis = static_cast(object); + return pThis->signal_scroll(pScrolledWindow, pEvent); +} +#else +gboolean GtkSalObjectWidgetClip::signalScroll(GtkEventControllerScroll* pController, double delta_x, double delta_y, gpointer object) +{ + GtkSalObjectWidgetClip* pThis = static_cast(object); + return pThis->signal_scroll(pController, delta_x, delta_y); +} +#endif + +// forward the wheel scroll events onto the main window instead +#if !GTK_CHECK_VERSION(4, 0, 0) +bool GtkSalObjectWidgetClip::signal_scroll(GtkWidget*, GdkEvent* pEvent) +{ + GtkWidget* pEventWidget = gtk_get_event_widget(pEvent); + + GtkWidget* pMouseEventWidget = m_pParent->getMouseEventWidget(); + + gtk_coord dest_x, dest_y; + gtk_widget_translate_coordinates(pEventWidget, + pMouseEventWidget, + pEvent->scroll.x, + pEvent->scroll.y, + &dest_x, + &dest_y); + pEvent->scroll.x = dest_x; + pEvent->scroll.y = dest_y; + + GtkSalFrame::signalScroll(pMouseEventWidget, pEvent, m_pParent); + return true; +} +#else +bool GtkSalObjectWidgetClip::signal_scroll(GtkEventControllerScroll* pController, double delta_x, double delta_y) +{ + GtkWidget* pEventWidget = m_pScrolledWindow; + + GtkWidget* pMouseEventWidget = m_pParent->getMouseEventWidget(); + + gtk_coord dest_x, dest_y; + gtk_widget_translate_coordinates(pEventWidget, + pMouseEventWidget, + delta_x, + delta_y, + &dest_x, + &dest_y); + delta_x = dest_x; + delta_y = dest_y; + + GtkSalFrame::signalScroll(pController, delta_x, delta_y, m_pParent); + return true; +} +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtksalmenu.cxx b/vcl/unx/gtk3/gtksalmenu.cxx new file mode 100644 index 0000000000..a510473650 --- /dev/null +++ b/vcl/unx/gtk3/gtksalmenu.cxx @@ -0,0 +1,1646 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include // for escapeStringXML + +#include +#include +#include +#include +#include + +static bool bUnityMode = false; + +/* + * This function generates a unique command name for each menu item + */ +static gchar* GetCommandForItem(GtkSalMenu* pParentMenu, sal_uInt16 nItemId) +{ + OString aCommand = "window-" + + OString::number(reinterpret_cast(pParentMenu)) + + "-" + OString::number(nItemId); + return g_strdup(aCommand.getStr()); +} + +static gchar* GetCommandForItem(GtkSalMenuItem* pSalMenuItem) +{ + return GetCommandForItem(pSalMenuItem->mpParentMenu, + pSalMenuItem->mnId); +} + +bool GtkSalMenu::PrepUpdate() const +{ + return mpMenuModel && mpActionGroup; +} + +/* + * Menu updating methods + */ + +static void RemoveSpareItemsFromNativeMenu( GLOMenu* pMenu, GList** pOldCommandList, unsigned nSection, unsigned nValidItems ) +{ + sal_Int32 nSectionItems = g_lo_menu_get_n_items_from_section( pMenu, nSection ); + + while ( nSectionItems > static_cast(nValidItems) ) + { + gchar* aCommand = g_lo_menu_get_command_from_item_in_section( pMenu, nSection, --nSectionItems ); + + if ( aCommand != nullptr && pOldCommandList != nullptr ) + *pOldCommandList = g_list_append( *pOldCommandList, g_strdup( aCommand ) ); + + g_free( aCommand ); + + g_lo_menu_remove_from_section( pMenu, nSection, nSectionItems ); + } +} + +typedef std::pair MenuAndId; + +namespace +{ + MenuAndId decode_command(const gchar *action_name) + { + std::string_view sCommand(action_name); + + sal_Int32 nIndex = 0; + std::string_view sWindow = o3tl::getToken(sCommand, 0, '-', nIndex); + std::string_view sGtkSalMenu = o3tl::getToken(sCommand, 0, '-', nIndex); + std::string_view sItemId = o3tl::getToken(sCommand, 0, '-', nIndex); + + GtkSalMenu* pSalSubMenu = reinterpret_cast(o3tl::toInt64(sGtkSalMenu)); + + assert(sWindow == "window" && pSalSubMenu); + (void) sWindow; + + return MenuAndId(pSalSubMenu, o3tl::toInt32(sItemId)); + } +} + +static void RemoveDisabledItemsFromNativeMenu(GLOMenu* pMenu, GList** pOldCommandList, + sal_Int32 nSection, GActionGroup* pActionGroup) +{ + while (nSection >= 0) + { + sal_Int32 nSectionItems = g_lo_menu_get_n_items_from_section( pMenu, nSection ); + while (nSectionItems--) + { + gchar* pCommand = g_lo_menu_get_command_from_item_in_section(pMenu, nSection, nSectionItems); + // remove disabled entries + bool bRemove = !g_action_group_get_action_enabled(pActionGroup, pCommand); + if (!bRemove) + { + //also remove any empty submenus + GLOMenu* pSubMenuModel = g_lo_menu_get_submenu_from_item_in_section(pMenu, nSection, nSectionItems); + if (pSubMenuModel) + { + gint nSubMenuSections = g_menu_model_get_n_items(G_MENU_MODEL(pSubMenuModel)); + if (nSubMenuSections == 0) + bRemove = true; + else if (nSubMenuSections == 1) + { + gint nItems = g_lo_menu_get_n_items_from_section(pSubMenuModel, 0); + if (nItems == 0) + bRemove = true; + else if (nItems == 1) + { + //If the only entry is the "No Selection Possible" entry, then we are allowed + //to removed it + gchar* pSubCommand = g_lo_menu_get_command_from_item_in_section(pSubMenuModel, 0, 0); + MenuAndId aMenuAndId(decode_command(pSubCommand)); + bRemove = aMenuAndId.second == 0xFFFF; + g_free(pSubCommand); + } + } + } + } + + if (bRemove) + { + //but tdf#86850 Always display clipboard functions + bRemove = g_strcmp0(pCommand, ".uno:Cut") && + g_strcmp0(pCommand, ".uno:Copy") && + g_strcmp0(pCommand, ".uno:Paste"); + } + + if (bRemove) + { + if (pCommand != nullptr && pOldCommandList != nullptr) + *pOldCommandList = g_list_append(*pOldCommandList, g_strdup(pCommand)); + g_lo_menu_remove_from_section(pMenu, nSection, nSectionItems); + } + + g_free(pCommand); + } + --nSection; + } +} + +static void RemoveSpareSectionsFromNativeMenu( GLOMenu* pMenu, GList** pOldCommandList, sal_Int32 nLastSection ) +{ + if ( pMenu == nullptr || pOldCommandList == nullptr ) + return; + + sal_Int32 n = g_menu_model_get_n_items( G_MENU_MODEL( pMenu ) ) - 1; + + for ( ; n > nLastSection; n--) + { + RemoveSpareItemsFromNativeMenu( pMenu, pOldCommandList, n, 0 ); + g_lo_menu_remove( pMenu, n ); + } +} + +static gint CompareStr( gpointer str1, gpointer str2 ) +{ + return g_strcmp0( static_cast(str1), static_cast(str2) ); +} + +static void RemoveUnusedCommands( GLOActionGroup* pActionGroup, GList* pOldCommandList, GList* pNewCommandList ) +{ + if ( pActionGroup == nullptr || pOldCommandList == nullptr ) + { + g_list_free_full( pOldCommandList, g_free ); + g_list_free_full( pNewCommandList, g_free ); + return; + } + + while ( pNewCommandList != nullptr ) + { + GList* pNewCommand = g_list_first( pNewCommandList ); + pNewCommandList = g_list_remove_link( pNewCommandList, pNewCommand ); + + gpointer aCommand = g_list_nth_data( pNewCommand, 0 ); + + GList* pOldCommand = g_list_find_custom( pOldCommandList, aCommand, reinterpret_cast(CompareStr) ); + + if ( pOldCommand != nullptr ) + { + pOldCommandList = g_list_remove_link( pOldCommandList, pOldCommand ); + g_list_free_full( pOldCommand, g_free ); + } + + g_list_free_full( pNewCommand, g_free ); + } + + while ( pOldCommandList != nullptr ) + { + GList* pCommand = g_list_first( pOldCommandList ); + pOldCommandList = g_list_remove_link( pOldCommandList, pCommand ); + + gchar* aCommand = static_cast(g_list_nth_data( pCommand, 0 )); + + g_lo_action_group_remove( pActionGroup, aCommand ); + + g_list_free_full( pCommand, g_free ); + } +} + +void GtkSalMenu::ImplUpdate(bool bRecurse, bool bRemoveDisabledEntries) +{ + SolarMutexGuard aGuard; + + SAL_INFO("vcl.unity", "ImplUpdate pre PrepUpdate"); + if( !PrepUpdate() ) + return; + + if (mbNeedsUpdate) + { + mbNeedsUpdate = false; + if (mbMenuBar && maUpdateMenuBarIdle.IsActive()) + { + maUpdateMenuBarIdle.Stop(); + // tdf#124391 Prevent doubled menus in global menu + if (!bUnityMode) + { + maUpdateMenuBarIdle.Invoke(); + return; + } + } + } + + Menu* pVCLMenu = mpVCLMenu; + GLOMenu* pLOMenu = G_LO_MENU( mpMenuModel ); + GLOActionGroup* pActionGroup = G_LO_ACTION_GROUP( mpActionGroup ); + SAL_INFO("vcl.unity", "Syncing vcl menu " << pVCLMenu << " to menu model " << pLOMenu << " and action group " << pActionGroup); + GList *pOldCommandList = nullptr; + GList *pNewCommandList = nullptr; + + sal_uInt16 nLOMenuSize = g_menu_model_get_n_items( G_MENU_MODEL( pLOMenu ) ); + + if ( nLOMenuSize == 0 ) + g_lo_menu_new_section( pLOMenu, 0, nullptr ); + + sal_Int32 nSection = 0; + sal_Int32 nItemPos = 0; + sal_Int32 validItems = 0; + sal_Int32 nItem; + + for ( nItem = 0; nItem < static_cast(GetItemCount()); nItem++ ) { + if ( !IsItemVisible( nItem ) ) + continue; + + GtkSalMenuItem *pSalMenuItem = GetItemAtPos( nItem ); + sal_uInt16 nId = pSalMenuItem->mnId; + + // PopupMenu::ImplExecute might add entry to top-level + // popup menu, but we have our own implementation below, so skip that one. + if ( nId == 0xFFFF ) + continue; + + if ( pSalMenuItem->mnType == MenuItemType::SEPARATOR ) + { + // Delete extra items from current section. + RemoveSpareItemsFromNativeMenu( pLOMenu, &pOldCommandList, nSection, validItems ); + + nSection++; + nItemPos = 0; + validItems = 0; + + if ( nLOMenuSize <= nSection ) + { + g_lo_menu_new_section( pLOMenu, nSection, nullptr ); + nLOMenuSize++; + } + + continue; + } + + if ( nItemPos >= g_lo_menu_get_n_items_from_section( pLOMenu, nSection ) ) + g_lo_menu_insert_in_section( pLOMenu, nSection, nItemPos, "EMPTY STRING" ); + + // Get internal menu item values. + OUString aText = pVCLMenu->GetItemText( nId ); + Image aImage = pVCLMenu->GetItemImage( nId ); + bool bEnabled = pVCLMenu->IsItemEnabled( nId ); + vcl::KeyCode nAccelKey = pVCLMenu->GetAccelKey( nId ); + bool bChecked = pVCLMenu->IsItemChecked( nId ); + MenuItemBits itemBits = pVCLMenu->GetItemBits( nId ); + + // Store current item command in command list. + gchar *aCurrentCommand = g_lo_menu_get_command_from_item_in_section( pLOMenu, nSection, nItemPos ); + + if ( aCurrentCommand != nullptr ) + pOldCommandList = g_list_append( pOldCommandList, aCurrentCommand ); + + // Get the new command for the item. + gchar* aNativeCommand = GetCommandForItem(pSalMenuItem); + + // Force updating of native menu labels. + NativeSetItemText( nSection, nItemPos, aText ); + NativeSetItemIcon( nSection, nItemPos, aImage ); + NativeSetAccelerator(nSection, nItemPos, nAccelKey, nAccelKey.GetName()); + + if ( g_strcmp0( aNativeCommand, "" ) != 0 && pSalMenuItem->mpSubMenu == nullptr ) + { + NativeSetItemCommand( nSection, nItemPos, nId, aNativeCommand, itemBits, bChecked, false ); + NativeCheckItem( nSection, nItemPos, itemBits, bChecked ); + NativeSetEnableItem( aNativeCommand, bEnabled ); + + pNewCommandList = g_list_append( pNewCommandList, g_strdup( aNativeCommand ) ); + } + + GtkSalMenu* pSubmenu = pSalMenuItem->mpSubMenu; + + if ( pSubmenu && pSubmenu->GetMenu() ) + { + bool bNonMenuChangedToMenu = NativeSetItemCommand( nSection, nItemPos, nId, aNativeCommand, itemBits, false, true ); + pNewCommandList = g_list_append( pNewCommandList, g_strdup( aNativeCommand ) ); + + GLOMenu* pSubMenuModel = g_lo_menu_get_submenu_from_item_in_section( pLOMenu, nSection, nItemPos ); + + if ( pSubMenuModel == nullptr ) + { + g_lo_menu_new_submenu_in_item_in_section( pLOMenu, nSection, nItemPos ); + pSubMenuModel = g_lo_menu_get_submenu_from_item_in_section( pLOMenu, nSection, nItemPos ); + } + + assert(pSubMenuModel); + + if (bRecurse || bNonMenuChangedToMenu) + { + SAL_INFO("vcl.unity", "preparing submenu " << pSubMenuModel << " to menu model " << G_MENU_MODEL(pSubMenuModel) << " and action group " << G_ACTION_GROUP(pActionGroup)); + pSubmenu->SetMenuModel( G_MENU_MODEL( pSubMenuModel ) ); + pSubmenu->SetActionGroup( G_ACTION_GROUP( pActionGroup ) ); + pSubmenu->ImplUpdate(true, bRemoveDisabledEntries); + } + + g_object_unref( pSubMenuModel ); + } + + g_free( aNativeCommand ); + + ++nItemPos; + ++validItems; + } + + if (bRemoveDisabledEntries) + { + // Delete disabled items in last section. + RemoveDisabledItemsFromNativeMenu(pLOMenu, &pOldCommandList, nSection, G_ACTION_GROUP(pActionGroup)); + } + + // Delete extra items in last section. + RemoveSpareItemsFromNativeMenu( pLOMenu, &pOldCommandList, nSection, validItems ); + + // Delete extra sections. + RemoveSpareSectionsFromNativeMenu( pLOMenu, &pOldCommandList, nSection ); + + // Delete unused commands. + RemoveUnusedCommands( pActionGroup, pOldCommandList, pNewCommandList ); + + // Resolves: tdf#103166 if the menu is empty, add a disabled + // placeholder. + sal_Int32 nSectionsCount = g_menu_model_get_n_items(G_MENU_MODEL(pLOMenu)); + gint nItemsCount = 0; + for (nSection = 0; nSection < nSectionsCount; ++nSection) + { + nItemsCount += g_lo_menu_get_n_items_from_section(pLOMenu, nSection); + if (nItemsCount) + break; + } + if (!nItemsCount) + { + gchar* aNativeCommand = GetCommandForItem(this, 0xFFFF); + OUString aPlaceholderText(VclResId(SV_RESID_STRING_NOSELECTIONPOSSIBLE)); + g_lo_menu_insert_in_section(pLOMenu, nSection-1, 0, + OUStringToOString(aPlaceholderText, RTL_TEXTENCODING_UTF8).getStr()); + NativeSetItemCommand(nSection-1, 0, 0xFFFF, aNativeCommand, MenuItemBits::NONE, false, false); + NativeSetEnableItem(aNativeCommand, false); + g_free(aNativeCommand); + } +} + +void GtkSalMenu::Update() +{ + //find out if top level is a menubar or not, if not, then it's a popup menu + //hierarchy and in those we hide (most) disabled entries + const GtkSalMenu* pMenu = this; + while (pMenu->mpParentSalMenu) + pMenu = pMenu->mpParentSalMenu; + + bool bAlwaysShowDisabledEntries; + if (pMenu->mbMenuBar) + bAlwaysShowDisabledEntries = !bool(mpVCLMenu->GetMenuFlags() & MenuFlags::HideDisabledEntries); + else + bAlwaysShowDisabledEntries = bool(mpVCLMenu->GetMenuFlags() & MenuFlags::AlwaysShowDisabledEntries); + + ImplUpdate(false, !bAlwaysShowDisabledEntries); +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +static void MenuPositionFunc(GtkMenu* menu, gint* x, gint* y, gboolean* push_in, gpointer user_data) +{ + Point *pPos = static_cast(user_data); + *x = pPos->X(); + if (gtk_widget_get_default_direction() == GTK_TEXT_DIR_RTL) + { + GtkRequisition natural_size; + gtk_widget_get_preferred_size(GTK_WIDGET(menu), nullptr, &natural_size); + *x -= natural_size.width; + } + *y = pPos->Y(); + *push_in = false; +} +#endif + +static void MenuClosed(GtkPopover* pWidget, GMainLoop* pLoop) +{ + // gtk4 4.4.0: click on an entry in a submenu of a menu crashes without this workaround + gtk_widget_grab_focus(gtk_widget_get_parent(GTK_WIDGET(pWidget))); + g_main_loop_quit(pLoop); +} + +bool GtkSalMenu::ShowNativePopupMenu(FloatingWindow* pWin, const tools::Rectangle& rRect, + FloatWinPopupFlags nFlags) +{ + VclPtr xParent = pWin->ImplGetWindowImpl()->mpRealParent; + mpFrame = static_cast(xParent->ImplGetFrame()); + + GLOActionGroup* pActionGroup = g_lo_action_group_new(); + mpActionGroup = G_ACTION_GROUP(pActionGroup); + mpMenuModel = G_MENU_MODEL(g_lo_menu_new()); + // Generate the main menu structure, populates mpMenuModel + UpdateFull(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + mpMenuWidget = gtk_menu_new_from_model(mpMenuModel); + gtk_menu_attach_to_widget(GTK_MENU(mpMenuWidget), mpFrame->getMouseEventWidget(), nullptr); +#else + mpMenuWidget = gtk_popover_menu_new_from_model(mpMenuModel); + gtk_widget_set_parent(mpMenuWidget, mpFrame->getMouseEventWidget()); + gtk_popover_set_has_arrow(GTK_POPOVER(mpMenuWidget), false); +#endif + gtk_widget_insert_action_group(mpFrame->getMouseEventWidget(), "win", mpActionGroup); + + //run in a sub main loop because we need to keep vcl PopupMenu alive to use + //it during DispatchCommand, returning now to the outer loop causes the + //launching PopupMenu to be destroyed, instead run the subloop here + //until the gtk menu is destroyed + GMainLoop* pLoop = g_main_loop_new(nullptr, true); +#if GTK_CHECK_VERSION(4, 0, 0) + g_signal_connect(G_OBJECT(mpMenuWidget), "closed", G_CALLBACK(MenuClosed), pLoop); +#else + g_signal_connect(G_OBJECT(mpMenuWidget), "deactivate", G_CALLBACK(MenuClosed), pLoop); +#endif + + + // tdf#120764 It isn't allowed under wayland to have two visible popups that share + // the same top level parent. The problem is that since gtk 3.24 tooltips are also + // implemented as popups, which means that we cannot show any popup if there is a + // visible tooltip. + // hide any current tooltip + mpFrame->HideTooltip(); + // don't allow any more to appear until menu is dismissed + mpFrame->BlockTooltip(); + +#if GTK_CHECK_VERSION(4, 0, 0) + AbsoluteScreenPixelRectangle aFloatRect = FloatingWindow::ImplConvertToAbsPos(xParent, rRect); + aFloatRect.Move(-mpFrame->maGeometry.x(), -mpFrame->maGeometry.y()); + GdkRectangle rect {static_cast(aFloatRect.Left()), static_cast(aFloatRect.Top()), + static_cast(aFloatRect.GetWidth()), static_cast(aFloatRect.GetHeight())}; + + gtk_popover_set_pointing_to(GTK_POPOVER(mpMenuWidget), &rect); + + if (nFlags & FloatWinPopupFlags::Left) + gtk_popover_set_position(GTK_POPOVER(mpMenuWidget), GTK_POS_LEFT); + else if (nFlags & FloatWinPopupFlags::Up) + gtk_popover_set_position(GTK_POPOVER(mpMenuWidget), GTK_POS_TOP); + else if (nFlags & FloatWinPopupFlags::Right) + gtk_popover_set_position(GTK_POPOVER(mpMenuWidget), GTK_POS_RIGHT); + else + gtk_popover_set_position(GTK_POPOVER(mpMenuWidget), GTK_POS_BOTTOM); + + gtk_popover_popup(GTK_POPOVER(mpMenuWidget)); +#else +#if GTK_CHECK_VERSION(3,22,0) + if (gtk_check_version(3, 22, 0) == nullptr) + { + AbsoluteScreenPixelRectangle aFloatRect = FloatingWindow::ImplConvertToAbsPos(xParent, rRect); + aFloatRect.Move(-mpFrame->maGeometry.x(), -mpFrame->maGeometry.y()); + GdkRectangle rect {static_cast(aFloatRect.Left()), static_cast(aFloatRect.Top()), + static_cast(aFloatRect.GetWidth()), static_cast(aFloatRect.GetHeight())}; + + GdkGravity rect_anchor = GDK_GRAVITY_SOUTH_WEST, menu_anchor = GDK_GRAVITY_NORTH_WEST; + + if (nFlags & FloatWinPopupFlags::Left) + { + rect_anchor = GDK_GRAVITY_NORTH_WEST; + menu_anchor = GDK_GRAVITY_NORTH_EAST; + } + else if (nFlags & FloatWinPopupFlags::Up) + { + rect_anchor = GDK_GRAVITY_NORTH_WEST; + menu_anchor = GDK_GRAVITY_SOUTH_WEST; + } + else if (nFlags & FloatWinPopupFlags::Right) + { + rect_anchor = GDK_GRAVITY_NORTH_EAST; + } + + GdkSurface* gdkWindow = widget_get_surface(mpFrame->getMouseEventWidget()); + gtk_menu_popup_at_rect(GTK_MENU(mpMenuWidget), gdkWindow, &rect, rect_anchor, menu_anchor, nullptr); + } + else +#endif + { + guint nButton; + guint32 nTime; + + //typically there is an event, and we can then distinguish if this was + //launched from the keyboard (gets auto-mnemoniced) or the mouse (which + //doesn't) + GdkEvent *pEvent = gtk_get_current_event(); + if (pEvent) + { + gdk_event_get_button(pEvent, &nButton); + nTime = gdk_event_get_time(pEvent); + } + else + { + nButton = 0; + nTime = GtkSalFrame::GetLastInputEventTime(); + } + + // Do the same strange semantics as vcl popup windows to arrive at a frame geometry + // in mirrored UI case; best done by actually executing the same code. + // (see code in FloatingWindow::StartPopupMode) + sal_uInt16 nArrangeIndex; + Point aPos = FloatingWindow::ImplCalcPos(pWin, rRect, nFlags, nArrangeIndex); + AbsoluteScreenPixelPoint aPosAbs = FloatingWindow::ImplConvertToAbsPos(xParent, aPos); + + gtk_menu_popup(GTK_MENU(mpMenuWidget), nullptr, nullptr, MenuPositionFunc, + &aPosAbs, nButton, nTime); + } +#endif + + if (g_main_loop_is_running(pLoop)) + main_loop_run(pLoop); + + g_main_loop_unref(pLoop); + + mpVCLMenu->Deactivate(); + + g_object_unref(mpActionGroup); + ClearActionGroupAndMenuModel(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_destroy(mpMenuWidget); +#else + gtk_widget_unparent(mpMenuWidget); +#endif + mpMenuWidget = nullptr; + + gtk_widget_insert_action_group(mpFrame->getMouseEventWidget(), "win", nullptr); + + // undo tooltip blocking + mpFrame->UnblockTooltip(); + + mpFrame = nullptr; + + return true; +} + +/* + * GtkSalMenu + */ + +GtkSalMenu::GtkSalMenu( bool bMenuBar ) : + maUpdateMenuBarIdle("Native Gtk Menu Update Idle"), + mbInActivateCallback( false ), + mbMenuBar( bMenuBar ), + mbNeedsUpdate( false ), + mbReturnFocusToDocument( false ), + mbAddedGrab( false ), + mpMenuBarContainerWidget( nullptr ), + mpMenuAllowShrinkWidget( nullptr ), + mpMenuBarWidget( nullptr ), + mpMenuWidget( nullptr ), + mpMenuBarContainerProvider( nullptr ), + mpMenuBarProvider( nullptr ), + mpCloseButton( nullptr ), + mpVCLMenu( nullptr ), + mpParentSalMenu( nullptr ), + mpFrame( nullptr ), + mpMenuModel( nullptr ), + mpActionGroup( nullptr ) +{ + //typically this only gets called after the menu has been customized on the + //next idle slot, in the normal case of a new menubar SetFrame is called + //directly long before this idle would get called. + maUpdateMenuBarIdle.SetPriority(TaskPriority::HIGHEST); + maUpdateMenuBarIdle.SetInvokeHandler(LINK(this, GtkSalMenu, MenuBarHierarchyChangeHandler)); +} + +IMPL_LINK_NOARG(GtkSalMenu, MenuBarHierarchyChangeHandler, Timer *, void) +{ + SAL_WARN_IF(!mpFrame, "vcl.gtk", "MenuBar layout changed, but no frame for some reason!"); + if (!mpFrame) + return; + SetFrame(mpFrame); +} + +void GtkSalMenu::SetNeedsUpdate() +{ + GtkSalMenu* pMenu = this; + // start that the menu and its parents are in need of an update + // on the next activation + while (pMenu && !pMenu->mbNeedsUpdate) + { + pMenu->mbNeedsUpdate = true; + pMenu = pMenu->mpParentSalMenu; + } + // only if a menubar is directly updated do we force in a full + // structure update + if (mbMenuBar && !maUpdateMenuBarIdle.IsActive()) + maUpdateMenuBarIdle.Start(); +} + +void GtkSalMenu::SetMenuModel(GMenuModel* pMenuModel) +{ + if (mpMenuModel) + g_object_unref(mpMenuModel); + mpMenuModel = pMenuModel; + if (mpMenuModel) + g_object_ref(mpMenuModel); +} + +GtkSalMenu::~GtkSalMenu() +{ + SolarMutexGuard aGuard; + + // tdf#140225 we expect all items to be removed by Menu::dispose + // before this dtor is called + assert(maItems.empty()); + + DestroyMenuBarWidget(); + + if (mpMenuModel) + g_object_unref(mpMenuModel); + + if (mpFrame) + mpFrame->SetMenu(nullptr); +} + +bool GtkSalMenu::VisibleMenuBar() +{ + return mbMenuBar && (bUnityMode || mpMenuBarContainerWidget); +} + +void GtkSalMenu::InsertItem( SalMenuItem* pSalMenuItem, unsigned nPos ) +{ + SolarMutexGuard aGuard; + GtkSalMenuItem *pItem = static_cast( pSalMenuItem ); + + if ( nPos == MENU_APPEND ) + maItems.push_back( pItem ); + else + maItems.insert( maItems.begin() + nPos, pItem ); + + pItem->mpParentMenu = this; + + SetNeedsUpdate(); +} + +void GtkSalMenu::RemoveItem( unsigned nPos ) +{ + SolarMutexGuard aGuard; + + // tdf#140225 clear associated action when the item is removed + if (mpActionGroup) + { + GLOActionGroup* pActionGroup = G_LO_ACTION_GROUP(mpActionGroup); + gchar* pCommand = GetCommandForItem(maItems[nPos]); + g_lo_action_group_remove(pActionGroup, pCommand); + g_free(pCommand); + } + + maItems.erase( maItems.begin() + nPos ); + SetNeedsUpdate(); +} + +void GtkSalMenu::SetSubMenu( SalMenuItem* pSalMenuItem, SalMenu* pSubMenu, unsigned ) +{ + SolarMutexGuard aGuard; + GtkSalMenuItem *pItem = static_cast< GtkSalMenuItem* >( pSalMenuItem ); + GtkSalMenu *pGtkSubMenu = static_cast< GtkSalMenu* >( pSubMenu ); + + if ( pGtkSubMenu == nullptr ) + return; + + pGtkSubMenu->mpParentSalMenu = this; + pItem->mpSubMenu = pGtkSubMenu; + + SetNeedsUpdate(); +} + +static void CloseMenuBar(GtkWidget *, gpointer pMenu) +{ + Application::PostUserEvent(static_cast(pMenu)->GetCloseButtonClickHdl()); +} + +GtkWidget* GtkSalMenu::AddButton(GtkWidget *pImage) +{ + GtkWidget* pButton = gtk_button_new(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_button_set_relief(GTK_BUTTON(pButton), GTK_RELIEF_NONE); + gtk_button_set_focus_on_click(GTK_BUTTON(pButton), false); +#else + gtk_button_set_has_frame(GTK_BUTTON(pButton), false); + gtk_widget_set_focus_on_click(pButton, false); +#endif + + gtk_widget_set_can_focus(pButton, false); + + GtkStyleContext *pButtonContext = gtk_widget_get_style_context(GTK_WIDGET(pButton)); + + gtk_style_context_add_class(pButtonContext, "flat"); + gtk_style_context_add_class(pButtonContext, "small-button"); + + gtk_widget_show(pImage); + + gtk_widget_set_valign(pButton, GTK_ALIGN_CENTER); + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_add(GTK_CONTAINER(pButton), pImage); + gtk_widget_show_all(pButton); +#else + gtk_button_set_child(GTK_BUTTON(pButton), pImage); +#endif + return pButton; +} + +void GtkSalMenu::ShowCloseButton(bool bShow) +{ + assert(mbMenuBar); + if (!mpMenuBarContainerWidget) + return; + + if (!bShow) + { + if (mpCloseButton) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_destroy(mpCloseButton); +#else + g_clear_pointer(&mpCloseButton, gtk_widget_unparent); +#endif + mpCloseButton = nullptr; + } + return; + } + + if (mpCloseButton) + return; + + GIcon* pIcon = g_themed_icon_new_with_default_fallbacks("window-close-symbolic"); +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pImage = gtk_image_new_from_gicon(pIcon, GTK_ICON_SIZE_MENU); +#else + GtkWidget* pImage = gtk_image_new_from_gicon(pIcon); +#endif + g_object_unref(pIcon); + + mpCloseButton = AddButton(pImage); + + gtk_widget_set_margin_end(mpCloseButton, 8); + + OUString sToolTip(VclResId(SV_HELPTEXT_CLOSEDOCUMENT)); + gtk_widget_set_tooltip_text(mpCloseButton, sToolTip.toUtf8().getStr()); + + MenuBar *pVclMenuBar = static_cast(mpVCLMenu.get()); + g_signal_connect(mpCloseButton, "clicked", G_CALLBACK(CloseMenuBar), pVclMenuBar); + + gtk_grid_attach(GTK_GRID(mpMenuBarContainerWidget), mpCloseButton, 1, 0, 1, 1); +} + +namespace +{ + void DestroyMemoryStream(gpointer data) + { + SvMemoryStream* pMemStm = static_cast(data); + delete pMemStm; + } +} + +static void MenuButtonClicked(GtkWidget* pWidget, gpointer pMenu) +{ + OUString aId(get_buildable_id(GTK_BUILDABLE(pWidget))); + static_cast(pMenu)->HandleMenuButtonEvent(aId.toUInt32()); +} + +bool GtkSalMenu::AddMenuBarButton(const SalMenuButtonItem& rNewItem) +{ + if (!mbMenuBar) + return false; + + if (!mpMenuBarContainerWidget) + return false; + + GtkWidget* pImage = nullptr; + if (!!rNewItem.maImage) + { + SvMemoryStream* pMemStm = new SvMemoryStream; + auto aBitmapEx = rNewItem.maImage.GetBitmapEx(); + vcl::PngImageWriter aWriter(*pMemStm); + aWriter.write(aBitmapEx); + + GBytes *pBytes = g_bytes_new_with_free_func(pMemStm->GetData(), + pMemStm->TellEnd(), + DestroyMemoryStream, + pMemStm); + + GIcon *pIcon = g_bytes_icon_new(pBytes); +#if !GTK_CHECK_VERSION(4, 0, 0) + pImage = gtk_image_new_from_gicon(pIcon, GTK_ICON_SIZE_MENU); +#else + pImage = gtk_image_new_from_gicon(pIcon); +#endif + g_object_unref(pIcon); + } + + GtkWidget* pButton = AddButton(pImage); + + maExtraButtons.emplace_back(rNewItem.mnId, pButton); + + set_buildable_id(GTK_BUILDABLE(pButton), OUString::number(rNewItem.mnId)); + + gtk_widget_set_tooltip_text(pButton, rNewItem.maToolTipText.toUtf8().getStr()); + + MenuBar *pVclMenuBar = static_cast(mpVCLMenu.get()); + g_signal_connect(pButton, "clicked", G_CALLBACK(MenuButtonClicked), pVclMenuBar); + + if (mpCloseButton) + { + gtk_grid_insert_next_to(GTK_GRID(mpMenuBarContainerWidget), mpCloseButton, GTK_POS_LEFT); + gtk_grid_attach_next_to(GTK_GRID(mpMenuBarContainerWidget), pButton, mpCloseButton, + GTK_POS_LEFT, 1, 1); + } + else + gtk_grid_attach(GTK_GRID(mpMenuBarContainerWidget), pButton, 1, 0, 1, 1); + + return true; +} + +void GtkSalMenu::RemoveMenuBarButton( sal_uInt16 nId ) +{ + const auto it = std::find_if(maExtraButtons.begin(), maExtraButtons.end(), [&nId](const auto &item) { + return item.first == nId; }); + if (it == maExtraButtons.end()) + return; + + gint nAttach(0); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_child_get(GTK_CONTAINER(mpMenuBarContainerWidget), it->second, "left-attach", &nAttach, nullptr); + gtk_widget_destroy(it->second); +#else + gtk_grid_query_child(GTK_GRID(mpMenuBarContainerWidget), it->second, &nAttach, nullptr, nullptr, nullptr); + g_clear_pointer(&(it->second), gtk_widget_unparent); +#endif + gtk_grid_remove_column(GTK_GRID(mpMenuBarContainerWidget), nAttach); + maExtraButtons.erase(it); +} + +tools::Rectangle GtkSalMenu::GetMenuBarButtonRectPixel(sal_uInt16 nId, SalFrame* pReferenceFrame) +{ + if (!pReferenceFrame) + return tools::Rectangle(); + + const auto it = std::find_if(maExtraButtons.begin(), maExtraButtons.end(), [&nId](const auto &item) { + return item.first == nId; }); + if (it == maExtraButtons.end()) + return tools::Rectangle(); + + GtkWidget* pButton = it->second; + + GtkSalFrame* pFrame = static_cast(pReferenceFrame); + + gtk_coord x, y; + if (!gtk_widget_translate_coordinates(pButton, GTK_WIDGET(pFrame->getMouseEventWidget()), 0, 0, &x, &y)) + return tools::Rectangle(); + + return tools::Rectangle(Point(x, y), Size(gtk_widget_get_allocated_width(pButton), + gtk_widget_get_allocated_height(pButton))); +} + +//Typically when the menubar is deactivated we want the focus to return +//to where it came from. If the menubar was activated because of F6 +//moving focus into the associated VCL menubar then on pressing ESC +//or any other normal reason for deactivation we want focus to return +//to the document, definitely not still stuck in the associated +//VCL menubar. But if F6 is pressed while the menubar is activated +//we want to pass that F6 back to the VCL menubar which will move +//focus to the next pane by itself. +void GtkSalMenu::ReturnFocus() +{ + if (mbAddedGrab) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_grab_remove(mpMenuBarWidget); +#endif + mbAddedGrab = false; + } + if (!mbReturnFocusToDocument) + gtk_widget_grab_focus(mpFrame->getMouseEventWidget()); + else + mpFrame->GetWindow()->GrabFocusToDocument(); + mbReturnFocusToDocument = false; +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +gboolean GtkSalMenu::SignalKey(GdkEventKey const * pEvent) +{ + if (pEvent->keyval == GDK_KEY_F6) + { + mbReturnFocusToDocument = false; + gtk_menu_shell_cancel(GTK_MENU_SHELL(mpMenuBarWidget)); + //because we return false here, the keypress will continue + //to propagate and in the case that vcl focus is in + //the vcl menubar then that will also process F6 and move + //to the next pane + } + return false; +} +#endif + +//The GtkSalMenu is owned by a Vcl Menu/MenuBar. In the menubar +//case the vcl menubar is present and "visible", but with a 0 height +//so it not apparent. Normally it acts as though it is not there when +//a Native menubar is active. If we return true here, then for keyboard +//activation and traversal with F6 through panes then the vcl menubar +//acts as though it *is* present and we translate its take focus and F6 +//traversal key events into the gtk menubar equivalents. +bool GtkSalMenu::CanGetFocus() const +{ + return mpMenuBarWidget != nullptr; +} + +bool GtkSalMenu::TakeFocus() +{ + if (!mpMenuBarWidget) + return false; + +#if !GTK_CHECK_VERSION(4, 0, 0) + //Send a keyboard event to the gtk menubar to let it know it has been + //activated via the keyboard. Doesn't do anything except cause the gtk + //menubar "keyboard_mode" member to get set to true, so typically mnemonics + //are shown which will serve as indication that the menubar has focus + //(given that we want to show it with no menus popped down) + GdkEvent *event = GtkSalFrame::makeFakeKeyPress(mpMenuBarWidget); + gtk_widget_event(mpMenuBarWidget, event); + gdk_event_free(event); + + //this pairing results in a menubar with keyboard focus with no menus + //auto-popped down + gtk_grab_add(mpMenuBarWidget); + + mbAddedGrab = true; + gtk_menu_shell_select_first(GTK_MENU_SHELL(mpMenuBarWidget), false); + gtk_menu_shell_deselect(GTK_MENU_SHELL(mpMenuBarWidget)); +#endif + mbReturnFocusToDocument = true; + return true; +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +static void MenuBarReturnFocus(GtkMenuShell*, gpointer menu) +{ + GtkSalFrame::UpdateLastInputEventTime(gtk_get_current_event_time()); + GtkSalMenu* pMenu = static_cast(menu); + pMenu->ReturnFocus(); +} + +static gboolean MenuBarSignalKey(GtkWidget*, GdkEventKey* pEvent, gpointer menu) +{ + GtkSalMenu* pMenu = static_cast(menu); + return pMenu->SignalKey(pEvent); +} +#endif + +void GtkSalMenu::CreateMenuBarWidget() +{ + if (mpMenuBarContainerWidget) + return; + + GtkGrid* pGrid = mpFrame->getTopLevelGridWidget(); + mpMenuBarContainerWidget = gtk_grid_new(); + + gtk_widget_set_hexpand(GTK_WIDGET(mpMenuBarContainerWidget), true); + gtk_grid_insert_row(pGrid, 0); + gtk_grid_attach(pGrid, mpMenuBarContainerWidget, 0, 0, 1, 1); + +#if !GTK_CHECK_VERSION(4, 0, 0) + mpMenuAllowShrinkWidget = gtk_scrolled_window_new(nullptr, nullptr); + gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(mpMenuAllowShrinkWidget), GTK_SHADOW_NONE); + // tdf#129634 don't allow this scrolled window as a candidate to tab into + gtk_widget_set_can_focus(GTK_WIDGET(mpMenuAllowShrinkWidget), false); +#else + mpMenuAllowShrinkWidget = gtk_scrolled_window_new(); + gtk_scrolled_window_set_has_frame(GTK_SCROLLED_WINDOW(mpMenuAllowShrinkWidget), false); +#endif + // tdf#116290 external policy on scrolledwindow will not show a scrollbar, + // but still allow scrolled window to not be sized to the child content. + // So the menubar can be shrunk past its nominal smallest width. + // Unlike a hack using GtkFixed/GtkLayout the correct placement of the menubar occurs under RTL + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(mpMenuAllowShrinkWidget), GTK_POLICY_EXTERNAL, GTK_POLICY_NEVER); + gtk_grid_attach(GTK_GRID(mpMenuBarContainerWidget), mpMenuAllowShrinkWidget, 0, 0, 1, 1); + +#if !GTK_CHECK_VERSION(4, 0, 0) + mpMenuBarWidget = gtk_menu_bar_new_from_model(mpMenuModel); +#else + mpMenuBarWidget = gtk_popover_menu_bar_new_from_model(mpMenuModel); +#endif + + gtk_widget_insert_action_group(mpMenuBarWidget, "win", mpActionGroup); + gtk_widget_set_hexpand(GTK_WIDGET(mpMenuBarWidget), true); + gtk_widget_set_hexpand(mpMenuAllowShrinkWidget, true); +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_container_add(GTK_CONTAINER(mpMenuAllowShrinkWidget), mpMenuBarWidget); +#else + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(mpMenuAllowShrinkWidget), mpMenuBarWidget); +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + g_signal_connect(G_OBJECT(mpMenuBarWidget), "deactivate", G_CALLBACK(MenuBarReturnFocus), this); + g_signal_connect(G_OBJECT(mpMenuBarWidget), "key-press-event", G_CALLBACK(MenuBarSignalKey), this); +#endif + + gtk_widget_show(mpMenuBarWidget); + gtk_widget_show(mpMenuAllowShrinkWidget); + gtk_widget_show(mpMenuBarContainerWidget); + + ShowCloseButton( static_cast(mpVCLMenu.get())->HasCloseButton() ); + + ApplyPersona(); +} + +void GtkSalMenu::ApplyPersona() +{ + if (!mpMenuBarContainerWidget) + return; + assert(mbMenuBar); + // I'm dubious about the persona theming feature, but as it exists, lets try and support + // it, apply the image to the mpMenuBarContainerWidget + const BitmapEx& rPersonaBitmap = Application::GetSettings().GetStyleSettings().GetPersonaHeader(); + + GtkStyleContext *pMenuBarContainerContext = gtk_widget_get_style_context(GTK_WIDGET(mpMenuBarContainerWidget)); + if (mpMenuBarContainerProvider) + { + gtk_style_context_remove_provider(pMenuBarContainerContext, GTK_STYLE_PROVIDER(mpMenuBarContainerProvider)); + mpMenuBarContainerProvider = nullptr; + } + GtkStyleContext *pMenuBarContext = gtk_widget_get_style_context(GTK_WIDGET(mpMenuBarWidget)); + if (mpMenuBarProvider) + { + gtk_style_context_remove_provider(pMenuBarContext, GTK_STYLE_PROVIDER(mpMenuBarProvider)); + mpMenuBarProvider = nullptr; + } + + if (!rPersonaBitmap.IsEmpty()) + { + if (maPersonaBitmap != rPersonaBitmap) + { + mxPersonaImage.reset(new utl::TempFileNamed); + mxPersonaImage->EnableKillingFile(true); + SvStream* pStream = mxPersonaImage->GetStream(StreamMode::WRITE); + vcl::PngImageWriter aPNGWriter(*pStream); + aPNGWriter.write(rPersonaBitmap); + mxPersonaImage->CloseStream(); + } + + mpMenuBarContainerProvider = gtk_css_provider_new(); + OUString aBuffer = "* { background-image: url(\"" + mxPersonaImage->GetURL() + "\"); background-position: top right; }"; + OString aResult = OUStringToOString(aBuffer, RTL_TEXTENCODING_UTF8); + css_provider_load_from_data(mpMenuBarContainerProvider, aResult.getStr(), aResult.getLength()); + gtk_style_context_add_provider(pMenuBarContainerContext, GTK_STYLE_PROVIDER(mpMenuBarContainerProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + + // force the menubar to be transparent when persona is active otherwise for + // me the menubar becomes gray when its in the backdrop + mpMenuBarProvider = gtk_css_provider_new(); + static const gchar data[] = "* { " + "background-image: none;" + "background-color: transparent;" + "}"; + css_provider_load_from_data(mpMenuBarProvider, data, -1); + gtk_style_context_add_provider(pMenuBarContext, + GTK_STYLE_PROVIDER(mpMenuBarProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + maPersonaBitmap = rPersonaBitmap; +} + +void GtkSalMenu::DestroyMenuBarWidget() +{ + if (!mpMenuBarContainerWidget) + return; + +#if !GTK_CHECK_VERSION(4, 0, 0) + // tdf#140225 call cancel before destroying it in case there are some + // active menus popped open + gtk_menu_shell_cancel(GTK_MENU_SHELL(mpMenuBarWidget)); + + gtk_widget_destroy(mpMenuBarContainerWidget); +#else + g_clear_pointer(&mpMenuBarContainerWidget, gtk_widget_unparent); +#endif + mpMenuBarContainerWidget = nullptr; + mpMenuBarWidget = nullptr; + mpCloseButton = nullptr; +} + +void GtkSalMenu::SetFrame(const SalFrame* pFrame) +{ + SolarMutexGuard aGuard; + assert(mbMenuBar); + SAL_INFO("vcl.unity", "GtkSalMenu set to frame"); + mpFrame = const_cast(static_cast(pFrame)); + + // if we had a menu on the GtkSalMenu we have to free it as we generate a + // full menu anyway and we might need to reuse an existing model and + // actiongroup + mpFrame->SetMenu( this ); + mpFrame->EnsureAppMenuWatch(); + + // Clean menu model and action group if needed. + GtkWidget* pWidget = mpFrame->getWindow(); + GdkSurface* gdkWindow = widget_get_surface(pWidget); + + GLOMenu* pMenuModel = G_LO_MENU( g_object_get_data( G_OBJECT( gdkWindow ), "g-lo-menubar" ) ); + GLOActionGroup* pActionGroup = G_LO_ACTION_GROUP( g_object_get_data( G_OBJECT( gdkWindow ), "g-lo-action-group" ) ); + SAL_INFO("vcl.unity", "Found menu model: " << pMenuModel << " and action group: " << pActionGroup); + + if ( pMenuModel ) + { + if ( g_menu_model_get_n_items( G_MENU_MODEL( pMenuModel ) ) > 0 ) + g_lo_menu_remove( pMenuModel, 0 ); + + mpMenuModel = G_MENU_MODEL( g_lo_menu_new() ); + } + + if ( pActionGroup ) + { + g_lo_action_group_clear( pActionGroup ); + mpActionGroup = G_ACTION_GROUP( pActionGroup ); + } + + // Generate the main menu structure. + if ( PrepUpdate() ) + UpdateFull(); + + g_lo_menu_insert_section( pMenuModel, 0, nullptr, mpMenuModel ); + + if (!bUnityMode && static_cast(mpVCLMenu.get())->IsDisplayable()) + { + DestroyMenuBarWidget(); + CreateMenuBarWidget(); + } +} + +const GtkSalFrame* GtkSalMenu::GetFrame() const +{ + SolarMutexGuard aGuard; + const GtkSalMenu* pMenu = this; + while( pMenu && ! pMenu->mpFrame ) + pMenu = pMenu->mpParentSalMenu; + return pMenu ? pMenu->mpFrame : nullptr; +} + +void GtkSalMenu::NativeCheckItem( unsigned nSection, unsigned nItemPos, MenuItemBits bits, gboolean bCheck ) +{ + SolarMutexGuard aGuard; + + if ( mpActionGroup == nullptr ) + return; + + gchar* aCommand = g_lo_menu_get_command_from_item_in_section( G_LO_MENU( mpMenuModel ), nSection, nItemPos ); + + if ( aCommand != nullptr || g_strcmp0( aCommand, "" ) != 0 ) + { + GVariant *pCheckValue = nullptr; + GVariant *pCurrentState = g_action_group_get_action_state( mpActionGroup, aCommand ); + + if ( bits & MenuItemBits::RADIOCHECK ) + pCheckValue = bCheck ? g_variant_new_string( aCommand ) : g_variant_new_string( "" ); + else + { + // By default, all checked items are checkmark buttons. + if (bCheck || pCurrentState != nullptr) + pCheckValue = g_variant_new_boolean( bCheck ); + } + + if ( pCheckValue != nullptr ) + { + if ( pCurrentState == nullptr || g_variant_equal( pCurrentState, pCheckValue ) == FALSE ) + { + g_action_group_change_action_state( mpActionGroup, aCommand, pCheckValue ); + } + else + { + g_variant_unref (pCheckValue); + } + } + + if ( pCurrentState != nullptr ) + g_variant_unref( pCurrentState ); + } + + if ( aCommand ) + g_free( aCommand ); +} + +void GtkSalMenu::NativeSetEnableItem( gchar const * aCommand, gboolean bEnable ) +{ + SolarMutexGuard aGuard; + GLOActionGroup* pActionGroup = G_LO_ACTION_GROUP( mpActionGroup ); + + if ( g_action_group_get_action_enabled( G_ACTION_GROUP( pActionGroup ), aCommand ) != bEnable ) + g_lo_action_group_set_action_enabled( pActionGroup, aCommand, bEnable ); +} + +void GtkSalMenu::NativeSetItemText( unsigned nSection, unsigned nItemPos, const OUString& rText ) +{ + SolarMutexGuard aGuard; + // Escape all underscores so that they don't get interpreted as hotkeys + OUString aText = rText.replaceAll( "_", "__" ); + // Replace the LibreOffice hotkey identifier with an underscore + aText = aText.replace( '~', '_' ); + OString aConvertedText = OUStringToOString( aText, RTL_TEXTENCODING_UTF8 ); + + // Update item text only when necessary. + gchar* aLabel = g_lo_menu_get_label_from_item_in_section( G_LO_MENU( mpMenuModel ), nSection, nItemPos ); + + if ( aLabel == nullptr || g_strcmp0( aLabel, aConvertedText.getStr() ) != 0 ) + g_lo_menu_set_label_to_item_in_section( G_LO_MENU( mpMenuModel ), nSection, nItemPos, aConvertedText.getStr() ); + + if ( aLabel ) + g_free( aLabel ); +} + +void GtkSalMenu::NativeSetItemIcon( unsigned nSection, unsigned nItemPos, const Image& rImage ) +{ +#if GLIB_CHECK_VERSION(2,38,0) + if (!rImage && mbHasNullItemIcon) + return; + + SolarMutexGuard aGuard; + + if (!!rImage) + { + SvMemoryStream* pMemStm = new SvMemoryStream; + auto aBitmapEx = rImage.GetBitmapEx(); + vcl::PngImageWriter aWriter(*pMemStm); + aWriter.write(aBitmapEx); + + GBytes *pBytes = g_bytes_new_with_free_func(pMemStm->GetData(), + pMemStm->TellEnd(), + DestroyMemoryStream, + pMemStm); + + GIcon *pIcon = g_bytes_icon_new(pBytes); + + g_lo_menu_set_icon_to_item_in_section( G_LO_MENU( mpMenuModel ), nSection, nItemPos, pIcon ); + g_object_unref(pIcon); + g_bytes_unref(pBytes); + mbHasNullItemIcon = false; + } + else + { + g_lo_menu_set_icon_to_item_in_section( G_LO_MENU( mpMenuModel ), nSection, nItemPos, nullptr ); + mbHasNullItemIcon = true; + } +#else + (void)nSection; + (void)nItemPos; + (void)rImage; +#endif +} + +void GtkSalMenu::NativeSetAccelerator( unsigned nSection, unsigned nItemPos, const vcl::KeyCode& rKeyCode, std::u16string_view rKeyName ) +{ + SolarMutexGuard aGuard; + + if ( rKeyName.empty() ) + return; + + guint nKeyCode; + GdkModifierType nModifiers; + GtkSalFrame::KeyCodeToGdkKey(rKeyCode, &nKeyCode, &nModifiers); + + gchar* aAccelerator = gtk_accelerator_name( nKeyCode, nModifiers ); + + gchar* aCurrentAccel = g_lo_menu_get_accelerator_from_item_in_section( G_LO_MENU( mpMenuModel ), nSection, nItemPos ); + + if ( aCurrentAccel == nullptr && g_strcmp0( aCurrentAccel, aAccelerator ) != 0 ) + g_lo_menu_set_accelerator_to_item_in_section ( G_LO_MENU( mpMenuModel ), nSection, nItemPos, aAccelerator ); + + g_free( aAccelerator ); + g_free( aCurrentAccel ); +} + +bool GtkSalMenu::NativeSetItemCommand( unsigned nSection, + unsigned nItemPos, + sal_uInt16 nId, + const gchar* aCommand, + MenuItemBits nBits, + bool bChecked, + bool bIsSubmenu ) +{ + bool bSubMenuAddedOrRemoved = false; + + SolarMutexGuard aGuard; + GLOActionGroup* pActionGroup = G_LO_ACTION_GROUP( mpActionGroup ); + + GVariant *pTarget = nullptr; + + if (g_action_group_has_action(mpActionGroup, aCommand)) + g_lo_action_group_remove(pActionGroup, aCommand); + + if ( ( nBits & MenuItemBits::CHECKABLE ) || bIsSubmenu ) + { + // Item is a checkmark button. + GVariantType* pStateType = g_variant_type_new( reinterpret_cast(G_VARIANT_TYPE_BOOLEAN) ); + GVariant* pState = g_variant_new_boolean( bChecked ); + + g_lo_action_group_insert_stateful( pActionGroup, aCommand, nId, bIsSubmenu, nullptr, pStateType, nullptr, pState ); + } + else if ( nBits & MenuItemBits::RADIOCHECK ) + { + // Item is a radio button. + GVariantType* pParameterType = g_variant_type_new( reinterpret_cast(G_VARIANT_TYPE_STRING) ); + GVariantType* pStateType = g_variant_type_new( reinterpret_cast(G_VARIANT_TYPE_STRING) ); + GVariant* pState = g_variant_new_string( "" ); + pTarget = g_variant_new_string( aCommand ); + + g_lo_action_group_insert_stateful( pActionGroup, aCommand, nId, FALSE, pParameterType, pStateType, nullptr, pState ); + } + else + { + // Item is not special, so insert a stateless action. + g_lo_action_group_insert( pActionGroup, aCommand, nId, FALSE ); + } + + GLOMenu* pMenu = G_LO_MENU( mpMenuModel ); + + // Menu item is not updated unless it's necessary. + gchar* aCurrentCommand = g_lo_menu_get_command_from_item_in_section( pMenu, nSection, nItemPos ); + + if ( aCurrentCommand == nullptr || g_strcmp0( aCurrentCommand, aCommand ) != 0 ) + { + bool bOldHasSubmenu = g_lo_menu_get_submenu_from_item_in_section(pMenu, nSection, nItemPos) != nullptr; + bSubMenuAddedOrRemoved = bOldHasSubmenu != bIsSubmenu; + if (bSubMenuAddedOrRemoved) + { + //tdf#98636 it's not good enough to unset the "submenu-action" attribute to change something + //from a submenu to a non-submenu item, so remove the old one entirely and re-add it to + //support achieving that + gchar* pLabel = g_lo_menu_get_label_from_item_in_section(pMenu, nSection, nItemPos); + g_lo_menu_remove_from_section(pMenu, nSection, nItemPos); + g_lo_menu_insert_in_section(pMenu, nSection, nItemPos, pLabel); + g_free(pLabel); + } + + g_lo_menu_set_command_to_item_in_section( pMenu, nSection, nItemPos, aCommand ); + + gchar* aItemCommand = g_strconcat("win.", aCommand, nullptr ); + + if ( bIsSubmenu ) + g_lo_menu_set_submenu_action_to_item_in_section( pMenu, nSection, nItemPos, aItemCommand ); + else + { + g_lo_menu_set_action_and_target_value_to_item_in_section( pMenu, nSection, nItemPos, aItemCommand, pTarget ); + pTarget = nullptr; + } + + g_free( aItemCommand ); + } + + if ( aCurrentCommand ) + g_free( aCurrentCommand ); + + if (pTarget) + g_variant_unref(pTarget); + + return bSubMenuAddedOrRemoved; +} + +GtkSalMenu* GtkSalMenu::GetTopLevel() +{ + GtkSalMenu *pMenu = this; + while (pMenu->mpParentSalMenu) + pMenu = pMenu->mpParentSalMenu; + return pMenu; +} + +void GtkSalMenu::DispatchCommand(const gchar *pCommand) +{ + SolarMutexGuard aGuard; + MenuAndId aMenuAndId = decode_command(pCommand); + GtkSalMenu* pSalSubMenu = aMenuAndId.first; + GtkSalMenu* pTopLevel = pSalSubMenu->GetTopLevel(); + + // tdf#125803 spacebar will toggle radios and checkbuttons without automatically + // closing the menu. To handle this properly I imagine we need to set groups for the + // radiobuttons so the others visually untoggle when the active one is toggled and + // we would further need to teach vcl that the state can change more than once. + // + // or we could unconditionally deactivate the menus if regardless of what particular + // type of menu item got activated + if (pTopLevel->mpMenuBarWidget) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_menu_shell_deactivate(GTK_MENU_SHELL(pTopLevel->mpMenuBarWidget)); +#endif + } + if (pTopLevel->mpMenuWidget) + { +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_popover_popdown(GTK_POPOVER(pTopLevel->mpMenuWidget)); +#else + gtk_menu_shell_deactivate(GTK_MENU_SHELL(pTopLevel->mpMenuWidget)); +#endif + } + + pTopLevel->GetMenu()->HandleMenuCommandEvent(pSalSubMenu->GetMenu(), aMenuAndId.second); +} + +void GtkSalMenu::ActivateAllSubmenus(Menu* pMenuBar) +{ + // We can re-enter this method via the new event loop that gets created + // in GtkClipboardTransferable::getTransferDataFlavorsAsVector, so use the InActivateCallback + // flag to detect that and skip some startup work. + if (mbInActivateCallback) + return; + + mbInActivateCallback = true; + pMenuBar->HandleMenuActivateEvent(GetMenu()); + mbInActivateCallback = false; + for (GtkSalMenuItem* pSalItem : maItems) + { + if ( pSalItem->mpSubMenu != nullptr ) + { + pSalItem->mpSubMenu->ActivateAllSubmenus(pMenuBar); + } + } + Update(); + pMenuBar->HandleMenuDeActivateEvent(GetMenu()); +} + +void GtkSalMenu::ClearActionGroupAndMenuModel() +{ + SetMenuModel(nullptr); + mpActionGroup = nullptr; + for (GtkSalMenuItem* pSalItem : maItems) + { + if ( pSalItem->mpSubMenu != nullptr ) + { + pSalItem->mpSubMenu->ClearActionGroupAndMenuModel(); + } + } +} + +void GtkSalMenu::Activate(const gchar* pCommand) +{ + MenuAndId aMenuAndId = decode_command(pCommand); + GtkSalMenu* pSalMenu = aMenuAndId.first; + Menu* pVclMenu = pSalMenu->GetMenu(); + if (pVclMenu->isDisposed()) + return; + GtkSalMenu* pTopLevel = pSalMenu->GetTopLevel(); + Menu* pVclSubMenu = pVclMenu->GetPopupMenu(aMenuAndId.second); + GtkSalMenu* pSubMenu = pSalMenu->GetItemAtPos(pVclMenu->GetItemPos(aMenuAndId.second))->mpSubMenu; + + pSubMenu->mbInActivateCallback = true; + pTopLevel->GetMenu()->HandleMenuActivateEvent(pVclSubMenu); + pSubMenu->mbInActivateCallback = false; + pVclSubMenu->UpdateNativeMenu(); +} + +void GtkSalMenu::Deactivate(const gchar* pCommand) +{ + MenuAndId aMenuAndId = decode_command(pCommand); + GtkSalMenu* pSalMenu = aMenuAndId.first; + Menu* pVclMenu = pSalMenu->GetMenu(); + if (pVclMenu->isDisposed()) + return; + GtkSalMenu* pTopLevel = pSalMenu->GetTopLevel(); + Menu* pVclSubMenu = pVclMenu->GetPopupMenu(aMenuAndId.second); + pTopLevel->GetMenu()->HandleMenuDeActivateEvent(pVclSubMenu); +} + +void GtkSalMenu::EnableUnity(bool bEnable) +{ + bUnityMode = bEnable; + + MenuBar* pMenuBar(static_cast(mpVCLMenu.get())); + bool bDisplayable(pMenuBar->IsDisplayable()); + + if (bEnable) + { + DestroyMenuBarWidget(); + UpdateFull(); + if (!bDisplayable) + ShowMenuBar(false); + } + else + { + Update(); + ShowMenuBar(bDisplayable); + } + + pMenuBar->LayoutChanged(); +} + +void GtkSalMenu::ShowMenuBar( bool bVisible ) +{ + // Unity tdf#106271: Can't hide global menu, so empty it instead when user wants to hide menubar, + if (bUnityMode) + { + if (bVisible) + Update(); + else if (mpMenuModel && g_menu_model_get_n_items(G_MENU_MODEL(mpMenuModel)) > 0) + g_lo_menu_remove(G_LO_MENU(mpMenuModel), 0); + } + else if (bVisible) + CreateMenuBarWidget(); + else + DestroyMenuBarWidget(); +} + +bool GtkSalMenu::IsItemVisible( unsigned nPos ) +{ + SolarMutexGuard aGuard; + bool bVisible = false; + + if ( nPos < maItems.size() ) + bVisible = maItems[ nPos ]->mbVisible; + + return bVisible; +} + +void GtkSalMenu::CheckItem( unsigned, bool ) +{ +} + +void GtkSalMenu::EnableItem( unsigned nPos, bool bEnable ) +{ + SolarMutexGuard aGuard; + if ( bUnityMode && !mbInActivateCallback && !mbNeedsUpdate && GetTopLevel()->mbMenuBar && ( nPos < maItems.size() ) ) + { + gchar* pCommand = GetCommandForItem( GetItemAtPos( nPos ) ); + NativeSetEnableItem( pCommand, bEnable ); + g_free( pCommand ); + } +} + +void GtkSalMenu::ShowItem( unsigned nPos, bool bShow ) +{ + SolarMutexGuard aGuard; + if ( nPos < maItems.size() ) + { + maItems[ nPos ]->mbVisible = bShow; + if ( bUnityMode && !mbInActivateCallback && !mbNeedsUpdate && GetTopLevel()->mbMenuBar ) + Update(); + } +} + +void GtkSalMenu::SetItemText( unsigned nPos, SalMenuItem* pSalMenuItem, const OUString& rText ) +{ + SolarMutexGuard aGuard; + if ( !bUnityMode || mbInActivateCallback || mbNeedsUpdate || !GetTopLevel()->mbMenuBar || ( nPos >= maItems.size() ) ) + return; + + gchar* pCommand = GetCommandForItem( static_cast< GtkSalMenuItem* >( pSalMenuItem ) ); + + gint nSectionsCount = g_menu_model_get_n_items( mpMenuModel ); + for ( gint nSection = 0; nSection < nSectionsCount; ++nSection ) + { + gint nItemsCount = g_lo_menu_get_n_items_from_section( G_LO_MENU( mpMenuModel ), nSection ); + for ( gint nItem = 0; nItem < nItemsCount; ++nItem ) + { + gchar* pCommandFromModel = g_lo_menu_get_command_from_item_in_section( G_LO_MENU( mpMenuModel ), nSection, nItem ); + + if ( !g_strcmp0( pCommandFromModel, pCommand ) ) + { + NativeSetItemText( nSection, nItem, rText ); + g_free( pCommandFromModel ); + g_free( pCommand ); + return; + } + + g_free( pCommandFromModel ); + } + } + + g_free( pCommand ); +} + +void GtkSalMenu::SetItemImage( unsigned, SalMenuItem*, const Image& ) +{ +} + +void GtkSalMenu::SetAccelerator( unsigned, SalMenuItem*, const vcl::KeyCode&, const OUString& ) +{ +} + +void GtkSalMenu::GetSystemMenuData( SystemMenuData* ) +{ +} + +int GtkSalMenu::GetMenuBarHeight() const +{ + return mpMenuBarWidget ? gtk_widget_get_allocated_height(mpMenuBarWidget) : 0; +} + +/* + * GtkSalMenuItem + */ + +GtkSalMenuItem::GtkSalMenuItem( const SalItemParams* pItemData ) : + mpParentMenu( nullptr ), + mpSubMenu( nullptr ), + mnType( pItemData->eType ), + mnId( pItemData->nId ), + mbVisible( true ) +{ +} + +GtkSalMenuItem::~GtkSalMenuItem() +{ +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/gtksys.cxx b/vcl/unx/gtk3/gtksys.cxx new file mode 100644 index 0000000000..4dc191bd67 --- /dev/null +++ b/vcl/unx/gtk3/gtksys.cxx @@ -0,0 +1,330 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include +#include +#include +#include + +GtkSalSystem *GtkSalSystem::GetSingleton() +{ + static GtkSalSystem *pSingleton = new GtkSalSystem(); + return pSingleton; +} + +SalSystem *GtkInstance::CreateSalSystem() +{ + return GtkSalSystem::GetSingleton(); +} + +GtkSalSystem::GtkSalSystem() +{ + mpDisplay = gdk_display_get_default(); +#if !GTK_CHECK_VERSION(4, 0, 0) + countScreenMonitors(); +#endif + // rhbz#1285356, native look will be gtk2, which crashes + // when gtk3 is already loaded. Until there is a solution + // java-side force look and feel to something that doesn't + // crash when we are using gtk3 + setenv("STOC_FORCE_SYSTEM_LAF", "true", 1); +} + +GtkSalSystem::~GtkSalSystem() +{ +} + +namespace +{ + +struct GdkRectangleCoincidentLess +{ + // fdo#78799 - detect and elide overlaying monitors of different sizes + bool operator()(GdkRectangle const& rLeft, GdkRectangle const& rRight) + { + return + rLeft.x < rRight.x + || rLeft.y < rRight.y + ; + } +}; +struct GdkRectangleCoincident +{ + // fdo#78799 - detect and elide overlaying monitors of different sizes + bool operator()(GdkRectangle const& rLeft, GdkRectangle const& rRight) + { + return + rLeft.x == rRight.x + && rLeft.y == rRight.y + ; + } +}; + +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +/** + * GtkSalSystem::countScreenMonitors() + * + * This method builds the vector which allows us to map from VCL's + * idea of linear integer ScreenNumber to gtk+'s rather more + * complicated screen + monitor concept. + */ +void +GtkSalSystem::countScreenMonitors() +{ + maScreenMonitors.clear(); + for (gint i = 0; i < gdk_display_get_n_screens(mpDisplay); i++) + { + GdkScreen* const pScreen(gdk_display_get_screen(mpDisplay, i)); + gint nMonitors(pScreen ? gdk_screen_get_n_monitors(pScreen) : 0); + if (nMonitors > 1) + { + std::vector aGeometries; + aGeometries.reserve(nMonitors); + for (gint j(0); j != nMonitors; ++j) + { + GdkRectangle aGeometry; + gdk_screen_get_monitor_geometry(pScreen, j, &aGeometry); + aGeometries.push_back(aGeometry); + } + std::sort(aGeometries.begin(), aGeometries.end(), + GdkRectangleCoincidentLess()); + const std::vector::iterator aUniqueEnd( + std::unique(aGeometries.begin(), aGeometries.end(), + GdkRectangleCoincident())); + nMonitors = std::distance(aGeometries.begin(), aUniqueEnd); + } + maScreenMonitors.emplace_back(pScreen, nMonitors); + } +} +#endif + +SalX11Screen +GtkSalSystem::getXScreenFromDisplayScreen(unsigned int nScreen) +{ + if (!DLSYM_GDK_IS_X11_DISPLAY(mpDisplay)) + return SalX11Screen (0); + +#if GTK_CHECK_VERSION(4, 0, 0) + GdkX11Screen *pScreen = gdk_x11_display_get_screen(mpDisplay); + (void)nScreen; +#else + gint nMonitor; + GdkScreen *pScreen = getScreenMonitorFromIdx (nScreen, nMonitor); + if (!pScreen) + return SalX11Screen (0); +#endif + return SalX11Screen(gdk_x11_screen_get_screen_number(pScreen)); +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +GdkScreen * +GtkSalSystem::getScreenMonitorFromIdx (int nIdx, gint &nMonitor) +{ + GdkScreen *pScreen = nullptr; + for (auto const& screenMonitor : maScreenMonitors) + { + pScreen = screenMonitor.first; + if (!pScreen) + break; + if (nIdx >= screenMonitor.second) + nIdx -= screenMonitor.second; + else + break; + } + nMonitor = nIdx; + + // handle invalid monitor indexes as non-existent screens + if (nMonitor < 0 || (pScreen && nMonitor >= gdk_screen_get_n_monitors (pScreen))) + pScreen = nullptr; + + return pScreen; +} + +int +GtkSalSystem::getScreenIdxFromPtr (GdkScreen *pScreen) +{ + int nIdx = 0; + for (auto const& screenMonitor : maScreenMonitors) + { + if (screenMonitor.first == pScreen) + return nIdx; + nIdx += screenMonitor.second; + } + g_warning ("failed to find screen %p", pScreen); + return 0; +} + +int GtkSalSystem::getScreenMonitorIdx (GdkScreen *pScreen, + int nX, int nY) +{ + // TODO: this will fail horribly for exotic combinations like two + // monitors in mirror mode and one extra. Hopefully such + // abominations are not used (or, even better, not possible) in + // practice .-) + return getScreenIdxFromPtr (pScreen) + + gdk_screen_get_monitor_at_point (pScreen, nX, nY); +} +#endif + +unsigned int GtkSalSystem::GetDisplayScreenCount() +{ +#if GTK_CHECK_VERSION(4, 0, 0) + return g_list_model_get_n_items(gdk_display_get_monitors(mpDisplay)); +#else + gint nMonitor; + (void)getScreenMonitorFromIdx (G_MAXINT, nMonitor); + return G_MAXINT - nMonitor; +#endif +} + +unsigned int GtkSalSystem::GetDisplayBuiltInScreen() +{ +#if GTK_CHECK_VERSION(4, 0, 0) +#if defined(GDK_WINDOWING_X11) + if (DLSYM_GDK_IS_X11_DISPLAY(mpDisplay)) + { + GdkMonitor* pPrimary = gdk_x11_display_get_primary_monitor(mpDisplay); + GListModel* pList = gdk_display_get_monitors(mpDisplay); + int nIndex = 0; + while (gpointer pElem = g_list_model_get_item(pList, nIndex)) + { + if (pElem == pPrimary) + return nIndex; + ++nIndex; + } + } +#endif + // nothing for wayland ?, hope for the best that its at index 0 + return 0; +#else // !GTK_CHECK_VERSION(4, 0, 0) + GdkScreen *pDefault = gdk_display_get_default_screen (mpDisplay); + int idx = getScreenIdxFromPtr (pDefault); + return idx + gdk_screen_get_primary_monitor(pDefault); +#endif +} + +AbsoluteScreenPixelRectangle GtkSalSystem::GetDisplayScreenPosSizePixel(unsigned int nScreen) +{ + GdkRectangle aRect; +#if GTK_CHECK_VERSION(4, 0, 0) + GListModel* pList = gdk_display_get_monitors(mpDisplay); + GdkMonitor* pMonitor = static_cast(g_list_model_get_item(pList, nScreen)); + if (!pMonitor) + return AbsoluteScreenPixelRectangle(); + gdk_monitor_get_geometry(pMonitor, &aRect); +#else + gint nMonitor; + GdkScreen *pScreen; + pScreen = getScreenMonitorFromIdx (nScreen, nMonitor); + if (!pScreen) + return AbsoluteScreenPixelRectangle(); + gdk_screen_get_monitor_geometry (pScreen, nMonitor, &aRect); +#endif + return AbsoluteScreenPixelRectangle(AbsoluteScreenPixelPoint(aRect.x, aRect.y), AbsoluteScreenPixelSize(aRect.width, aRect.height)); +} + +// convert ~ to indicate mnemonic to '_' +static OString MapToGtkAccelerator(const OUString &rStr) +{ + return OUStringToOString(rStr.replaceFirst("~", "_"), RTL_TEXTENCODING_UTF8); +} + +#if GTK_CHECK_VERSION(4, 0, 0) + +namespace +{ + struct DialogLoop + { + GMainLoop* pLoop = nullptr; + gint nResponseId = GTK_RESPONSE_NONE; + gulong nSignalResponseId = 0; + gulong nSignalCloseRequestId= 0; + + static gboolean DialogClose(GtkWindow* pDialog, gpointer /*data*/) + { + gtk_dialog_response(GTK_DIALOG(pDialog), GTK_RESPONSE_CANCEL); + return true; + } + + static void DialogResponse(GtkDialog* pDialog, gint nResponseId, gpointer data) + { + DialogLoop* pDialogLoop = static_cast(data); + g_signal_handler_disconnect(pDialog, pDialogLoop->nSignalResponseId); + g_signal_handler_disconnect(pDialog, pDialogLoop->nSignalCloseRequestId); + pDialogLoop->nResponseId = nResponseId; + g_main_loop_quit(pDialogLoop->pLoop); + } + + int run(GtkDialog *pDialog) + { + nSignalResponseId = g_signal_connect(pDialog, "response", G_CALLBACK(DialogResponse), this); + nSignalCloseRequestId = g_signal_connect(pDialog, "close-request", G_CALLBACK(DialogClose), this); + gtk_window_present(GTK_WINDOW(pDialog)); + pLoop = g_main_loop_new(nullptr, false); + main_loop_run(pLoop); + g_main_loop_unref(pLoop); + return nResponseId; + } + + }; +} + +gint gtk_dialog_run(GtkDialog* pDialog) +{ + DialogLoop aDialogLoop; + return aDialogLoop.run(pDialog); +} + +#endif + +int GtkSalSystem::ShowNativeDialog (const OUString& rTitle, const OUString& rMessage, + const std::vector< OUString >& rButtonNames) +{ + OString aTitle (OUStringToOString (rTitle, RTL_TEXTENCODING_UTF8)); + OString aMessage (OUStringToOString (rMessage, RTL_TEXTENCODING_UTF8)); + + GtkDialog *pDialog = GTK_DIALOG ( + g_object_new (GTK_TYPE_MESSAGE_DIALOG, + "title", aTitle.getStr(), + "message-type", int(GTK_MESSAGE_WARNING), + "text", aMessage.getStr(), + nullptr)); + int nButton = 0; + for (auto const& buttonName : rButtonNames) + gtk_dialog_add_button (pDialog, MapToGtkAccelerator(buttonName).getStr(), nButton++); + gtk_dialog_set_default_response (pDialog, 0/*nDefaultButton*/); + + nButton = gtk_dialog_run (pDialog); + if (nButton < 0) + nButton = -1; + +#if !GTK_CHECK_VERSION(4, 0, 0) + gtk_widget_destroy(GTK_WIDGET(pDialog)); +#else + gtk_window_destroy(GTK_WINDOW(pDialog)); +#endif + + return nButton; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/hudawareness.cxx b/vcl/unx/gtk3/hudawareness.cxx new file mode 100644 index 0000000000..ebcbaf747f --- /dev/null +++ b/vcl/unx/gtk3/hudawareness.cxx @@ -0,0 +1,110 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include + +#include + +namespace { + +struct HudAwarenessHandle +{ + gpointer connection; + HudAwarenessCallback callback; + gpointer user_data; + GDestroyNotify notify; +}; + +} + +static void +hud_awareness_method_call (GDBusConnection * /* connection */, + const gchar * /* sender */, + const gchar * /* object_path */, + const gchar * /* interface_name */, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + HudAwarenessHandle *handle = static_cast(user_data); + + if (g_str_equal (method_name, "HudActiveChanged")) + { + gboolean active; + + g_variant_get (parameters, "(b)", &active); + + (* handle->callback) (active, handle->user_data); + } + + g_dbus_method_invocation_return_value (invocation, nullptr); +} + +guint +hud_awareness_register (GDBusConnection *connection, + const gchar *object_path, + HudAwarenessCallback callback, + gpointer user_data, + GDestroyNotify notify, + GError **error) +{ + static GDBusInterfaceInfo *iface; + static GDBusNodeInfo *info; + GDBusInterfaceVTable vtable; + HudAwarenessHandle *handle; + guint object_id; + + memset (static_cast(&vtable), 0, sizeof (vtable)); + vtable.method_call = hud_awareness_method_call; + + if G_UNLIKELY (iface == nullptr) + { + GError *local_error = nullptr; + + info = g_dbus_node_info_new_for_xml ("" + "" + "" + "" + "" + "" + "" + "", + &local_error); + g_assert_no_error (local_error); + iface = g_dbus_node_info_lookup_interface (info, "com.canonical.hud.Awareness"); + g_assert (iface != nullptr); + } + + handle = static_cast(g_malloc (sizeof (HudAwarenessHandle))); + + object_id = g_dbus_connection_register_object (connection, object_path, iface, &vtable, handle, &g_free, error); + + if (object_id == 0) + { + g_free (handle); + return 0; + } + + handle->connection = g_object_ref(connection); + handle->callback = callback; + handle->user_data = user_data; + handle->notify = notify; + + return object_id; +} + +void +hud_awareness_unregister (GDBusConnection *connection, + guint subscription_id) +{ + g_dbus_connection_unregister_object (connection, subscription_id); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3/salnativewidgets-gtk.cxx b/vcl/unx/gtk3/salnativewidgets-gtk.cxx new file mode 100644 index 0000000000..a3a82edaa1 --- /dev/null +++ b/vcl/unx/gtk3/salnativewidgets-gtk.cxx @@ -0,0 +1,3082 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gtkcairo.hxx" +#include + +GtkStyleContext* GtkSalGraphics::mpWindowStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpLinkButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpEntryStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpTextViewStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpVScrollbarStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpVScrollbarContentsStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpVScrollbarTroughStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpVScrollbarSliderStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpVScrollbarButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpHScrollbarStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpHScrollbarContentsStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpHScrollbarTroughStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpHScrollbarSliderStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpHScrollbarButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpToolbarStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpToolButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpToolbarSeparatorStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpCheckButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpCheckButtonCheckStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpRadioButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpRadioButtonRadioStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpSpinStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpSpinUpStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpSpinDownStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpComboboxStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpComboboxBoxStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpComboboxEntryStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpComboboxButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpComboboxButtonBoxStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpComboboxButtonArrowStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpListboxStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpListboxBoxStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpListboxButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpListboxButtonBoxStyle= nullptr; +GtkStyleContext* GtkSalGraphics::mpListboxButtonArrowStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpFrameInStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpFrameOutStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpFixedHoriLineStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpFixedVertLineStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpTreeHeaderButtonStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpProgressBarStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpProgressBarTroughStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpProgressBarProgressStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpNotebookStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpNotebookStackStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpNotebookHeaderStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpNotebookHeaderTabsStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpNotebookHeaderTabsTabStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpNotebookHeaderTabsTabLabelStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpNotebookHeaderTabsTabActiveLabelStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpNotebookHeaderTabsTabHoverLabelStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpMenuBarStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpMenuBarItemStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpMenuWindowStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpMenuStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpMenuItemStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpMenuItemArrowStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpMenuItemLabelStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpCheckMenuItemStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpCheckMenuItemCheckStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpRadioMenuItemStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpRadioMenuItemRadioStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpSeparatorMenuItemStyle = nullptr; +GtkStyleContext* GtkSalGraphics::mpSeparatorMenuItemSeparatorStyle = nullptr; +gint GtkSalGraphics::mnVerticalSeparatorMinWidth = 0; + +#if !GTK_CHECK_VERSION(4, 0, 0) +static void style_context_get_margin(GtkStyleContext *pContext, GtkBorder *pMargin) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_style_context_get_margin(pContext, pMargin); +#else + gtk_style_context_get_margin(pContext, gtk_style_context_get_state(pContext), pMargin); +#endif +} +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) +static void style_context_get_border(GtkStyleContext* pContext, GtkBorder* pBorder) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_style_context_get_border(pContext, pBorder); +#else + gtk_style_context_get_border(pContext, gtk_style_context_get_state(pContext), pBorder); +#endif +} +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) +static void style_context_get_padding(GtkStyleContext* pContext, GtkBorder* pPadding) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_style_context_get_padding(pContext, pPadding); +#else + gtk_style_context_get_padding(pContext, gtk_style_context_get_state(pContext), pPadding); +#endif +} +#endif + +bool GtkSalGraphics::style_loaded = false; +/************************************************************************ + * State conversion + ************************************************************************/ +#if !GTK_CHECK_VERSION(4, 0, 0) +static GtkStateFlags NWConvertVCLStateToGTKState(ControlState nVCLState) +{ + GtkStateFlags nGTKState = GTK_STATE_FLAG_NORMAL; + + if (!( nVCLState & ControlState::ENABLED )) + { + nGTKState = GTK_STATE_FLAG_INSENSITIVE; + } + + if ( nVCLState & ControlState::PRESSED ) + { + nGTKState = static_cast(nGTKState | GTK_STATE_FLAG_ACTIVE); + } + + if ( nVCLState & ControlState::ROLLOVER ) + { + nGTKState = static_cast(nGTKState | GTK_STATE_FLAG_PRELIGHT); + } + + if ( nVCLState & ControlState::SELECTED ) + nGTKState = static_cast(nGTKState | GTK_STATE_FLAG_SELECTED); + + if ( nVCLState & ControlState::FOCUSED ) + nGTKState = static_cast(nGTKState | GTK_STATE_FLAG_FOCUSED); + + if (AllSettings::GetLayoutRTL()) + { + nGTKState = static_cast(nGTKState | GTK_STATE_FLAG_DIR_RTL); + } + else + { + nGTKState = static_cast(nGTKState | GTK_STATE_FLAG_DIR_LTR); + } + + return nGTKState; +} + +namespace { + +enum class RenderType { + BackgroundAndFrame = 1, + Check, + Background, + MenuSeparator, + ToolbarSeparator, + Separator, + Arrow, + Radio, + Scrollbar, + Spinbutton, + Combobox, + Expander, + Icon, + Progress, + TabItem, + Focus +}; + +} + +static void NWCalcArrowRect( const tools::Rectangle& rButton, tools::Rectangle& rArrow ) +{ + // Size the arrow appropriately + Size aSize( rButton.GetWidth()/2, rButton.GetHeight()/2 ); + rArrow.SetSize( aSize ); + + rArrow.SetPos( Point( + rButton.Left() + ( rButton.GetWidth() - rArrow.GetWidth() ) / 2, + rButton.Top() + ( rButton.GetHeight() - rArrow.GetHeight() ) / 2 + ) ); +} + +tools::Rectangle GtkSalGraphics::NWGetSpinButtonRect( ControlPart nPart, tools::Rectangle aAreaRect) +{ + gint w, h; + gtk_icon_size_lookup (GTK_ICON_SIZE_MENU, &w, &h); + gint icon_size = std::max(w, h); + + GtkBorder padding, border; + style_context_get_padding(mpSpinUpStyle, &padding); + style_context_get_border(mpSpinUpStyle, &border); + + gint buttonWidth = icon_size + padding.left + padding.right + + border.left + border.right; + + tools::Rectangle buttonRect(Point(0, aAreaRect.Top()), Size(buttonWidth, 0)); + buttonRect.setHeight(aAreaRect.GetHeight()); + tools::Rectangle partRect(buttonRect); + if ( nPart == ControlPart::ButtonUp ) + { + if (AllSettings::GetLayoutRTL()) + partRect.SetPosX(aAreaRect.Left()); + else + partRect.SetPosX(aAreaRect.Left() + (aAreaRect.GetWidth() - buttonRect.GetWidth())); + } + else if( nPart == ControlPart::ButtonDown ) + { + if (AllSettings::GetLayoutRTL()) + partRect.SetPosX(aAreaRect.Left() + buttonRect.GetWidth()); + else + partRect.SetPosX(aAreaRect.Left() + (aAreaRect.GetWidth() - 2 * buttonRect.GetWidth())); + } + else + { + if (AllSettings::GetLayoutRTL()) + { + partRect.SetRight( aAreaRect.Left() + aAreaRect.GetWidth() ); + partRect.SetLeft( aAreaRect.Left() + (2 * buttonRect.GetWidth()) - 1 ); + } + else + { + partRect.SetRight( (aAreaRect.Left() + (aAreaRect.GetWidth() - 2 * buttonRect.GetWidth())) - 1 ); + partRect.SetLeft( aAreaRect.Left() ); + } + partRect.SetTop( aAreaRect.Top() ); + partRect.SetBottom( aAreaRect.Bottom() ); + } + + return partRect; +} +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) +namespace +{ + void QuerySize(GtkStyleContext *pContext, Size &rSize) + { + GtkBorder margin, border, padding; + + style_context_get_margin(pContext, &margin); + style_context_get_border(pContext, &border); + style_context_get_padding(pContext, &padding); + + int nMinWidth(0), nMinHeight(0); + GtkStateFlags stateflags = gtk_style_context_get_state (pContext); + gtk_style_context_get(pContext, stateflags, + "min-width", &nMinWidth, "min-height", &nMinHeight, nullptr); + nMinWidth += margin.left + margin.right + border.left + border.right + padding.left + padding.right; + nMinHeight += margin.top + margin.bottom + border.top + border.bottom + padding.top + padding.bottom; + + rSize = Size(std::max(rSize.Width(), nMinWidth), std::max(rSize.Height(), nMinHeight)); + } +} +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) +tools::Rectangle GtkSalGraphics::NWGetScrollButtonRect( ControlPart nPart, tools::Rectangle aAreaRect ) +{ + tools::Rectangle buttonRect; + + gboolean has_forward; + gboolean has_forward2; + gboolean has_backward; + gboolean has_backward2; + + GtkStyleContext* pScrollbarStyle = nullptr; + if ((nPart == ControlPart::ButtonLeft) || (nPart == ControlPart::ButtonRight)) + pScrollbarStyle = mpHScrollbarStyle; + else // (nPart == ControlPart::ButtonUp) || (nPart == ControlPart::ButtonDown) + pScrollbarStyle = mpVScrollbarStyle; + + gtk_style_context_get_style( pScrollbarStyle, + "has-forward-stepper", &has_forward, + "has-secondary-forward-stepper", &has_forward2, + "has-backward-stepper", &has_backward, + "has-secondary-backward-stepper", &has_backward2, nullptr ); + + gint nFirst = 0; + gint nSecond = 0; + + if ( has_forward ) nSecond += 1; + if ( has_forward2 ) nFirst += 1; + if ( has_backward ) nFirst += 1; + if ( has_backward2 ) nSecond += 1; + + Size aSize; + if (nPart == ControlPart::ButtonLeft || nPart == ControlPart::ButtonRight) + { + QuerySize(mpHScrollbarStyle, aSize); + QuerySize(mpHScrollbarContentsStyle, aSize); + QuerySize(mpHScrollbarButtonStyle, aSize); + } + else + { + QuerySize(mpVScrollbarStyle, aSize); + QuerySize(mpVScrollbarContentsStyle, aSize); + QuerySize(mpVScrollbarButtonStyle, aSize); + } + + if (nPart == ControlPart::ButtonUp) + { + aSize.setHeight( aSize.Height() * nFirst ); + buttonRect.SetLeft(aAreaRect.Left()); + buttonRect.SetTop(aAreaRect.Top()); + } + else if (nPart == ControlPart::ButtonLeft) + { + aSize.setWidth( aSize.Width() * nFirst ); + buttonRect.SetLeft(aAreaRect.Left()); + buttonRect.SetTop(aAreaRect.Top()); + } + else if (nPart == ControlPart::ButtonDown) + { + aSize.setHeight( aSize.Height() * nSecond ); + buttonRect.SetLeft(aAreaRect.Left()); + buttonRect.SetTop(aAreaRect.Top() + aAreaRect.GetHeight() - aSize.Height()); + } + else if (nPart == ControlPart::ButtonRight) + { + aSize.setWidth( aSize.Width() * nSecond ); + buttonRect.SetLeft(aAreaRect.Left() + aAreaRect.GetWidth() - aSize.Width()); + buttonRect.SetTop(aAreaRect.Top()); + } + + buttonRect.SetSize(aSize); + + return buttonRect; +} +#endif + +static GtkWidget* gCacheWindow; +static GtkWidget* gDumbContainer; +#if GTK_CHECK_VERSION(4, 0, 0) +static GtkWidget* gVScrollbar; +static GtkWidget* gHScrollbar; +static GtkWidget* gTextView; +#else +static GtkWidget* gComboBox; +static GtkWidget* gListBox; +static GtkWidget* gSpinBox; +static GtkWidget* gTreeViewWidget; +#endif +static GtkWidget* gEntryBox; + +namespace +{ + void style_context_set_state(GtkStyleContext* context, GtkStateFlags flags) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + do + { + gtk_style_context_set_state(context, flags); + } + while ((context = gtk_style_context_get_parent(context))); +#else + gtk_style_context_set_state(context, flags); +#endif + } + + class StyleContextSave + { + private: + std::vector> m_aStates; + public: + void save(GtkStyleContext* context) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + do + { + m_aStates.emplace_back(context, gtk_style_context_get_state(context)); + } + while ((context = gtk_style_context_get_parent(context))); +#else + m_aStates.emplace_back(context, gtk_style_context_get_state(context)); +#endif + } + void restore() + { + for (auto a = m_aStates.rbegin(); a != m_aStates.rend(); ++a) + { + gtk_style_context_set_state(a->first, a->second); + } + m_aStates.clear(); + } + }; + +#if !GTK_CHECK_VERSION(4, 0, 0) + tools::Rectangle render_common(GtkStyleContext *pContext, cairo_t *cr, const tools::Rectangle &rIn, GtkStateFlags flags) + { + if (!pContext) + return rIn; + + gtk_style_context_set_state(pContext, flags); + + tools::Rectangle aRect(rIn); + GtkBorder margin; + style_context_get_margin(pContext, &margin); + + aRect.AdjustLeft(margin.left ); + aRect.AdjustTop(margin.top ); + aRect.AdjustRight( -(margin.right) ); + aRect.AdjustBottom( -(margin.bottom) ); + + gtk_render_background(pContext, cr, aRect.Left(), aRect.Top(), + aRect.GetWidth(), aRect.GetHeight()); + gtk_render_frame(pContext, cr, aRect.Left(), aRect.Top(), + aRect.GetWidth(), aRect.GetHeight()); + + GtkBorder border, padding; + style_context_get_border(pContext, &border); + style_context_get_padding(pContext, &padding); + + aRect.AdjustLeft(border.left + padding.left ); + aRect.AdjustTop(border.top + padding.top ); + aRect.AdjustRight( -(border.right + padding.right) ); + aRect.AdjustBottom( -(border.bottom + padding.bottom) ); + + return aRect; + } +#endif +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +void GtkSalGraphics::PaintScrollbar(GtkStyleContext *context, + cairo_t *cr, + const tools::Rectangle& rControlRectangle, + ControlPart nPart, + const ImplControlValue& rValue ) +{ + assert(rValue.getType() == ControlType::Scrollbar); + const ScrollbarValue& rScrollbarVal = static_cast(rValue); + tools::Rectangle scrollbarRect; + GtkStateFlags stateFlags; + GtkOrientation scrollbarOrientation; + tools::Rectangle thumbRect = rScrollbarVal.maThumbRect; + tools::Rectangle button11BoundRect = rScrollbarVal.maButton1Rect; // backward + tools::Rectangle button22BoundRect = rScrollbarVal.maButton2Rect; // forward + tools::Rectangle button12BoundRect = rScrollbarVal.maButton1Rect; // secondary forward + tools::Rectangle button21BoundRect = rScrollbarVal.maButton2Rect; // secondary backward + gdouble arrow1Angle; // backward + gdouble arrow2Angle; // forward + tools::Rectangle arrowRect; + gint slider_width = 0; + gint stepper_size = 0; + + // make controlvalue rectangles relative to area + thumbRect.Move( -rControlRectangle.Left(), -rControlRectangle.Top() ); + button11BoundRect.Move( -rControlRectangle.Left(), -rControlRectangle.Top() ); + button22BoundRect.Move( -rControlRectangle.Left(), -rControlRectangle.Top() ); + button12BoundRect.Move( -rControlRectangle.Left(), -rControlRectangle.Top() ); + button21BoundRect.Move( -rControlRectangle.Left(), -rControlRectangle.Top() ); + + // Find the overall bounding rect of the control + scrollbarRect = rControlRectangle; + if (scrollbarRect.IsEmpty()) + return; + + gint slider_side; + Size aSize; + if (nPart == ControlPart::DrawBackgroundHorz) + { + QuerySize(mpHScrollbarStyle, aSize); + QuerySize(mpHScrollbarContentsStyle, aSize); + QuerySize(mpHScrollbarTroughStyle, aSize); + QuerySize(mpHScrollbarSliderStyle, aSize); + slider_side = aSize.Height(); + gtk_style_context_get(mpHScrollbarButtonStyle, + gtk_style_context_get_state(mpHScrollbarButtonStyle), + "min-height", &slider_width, + "min-width", &stepper_size, nullptr); + } + else + { + QuerySize(mpVScrollbarStyle, aSize); + QuerySize(mpVScrollbarContentsStyle, aSize); + QuerySize(mpVScrollbarTroughStyle, aSize); + QuerySize(mpVScrollbarSliderStyle, aSize); + slider_side = aSize.Width(); + gtk_style_context_get(mpVScrollbarButtonStyle, + gtk_style_context_get_state(mpVScrollbarButtonStyle), + "min-width", &slider_width, + "min-height", &stepper_size, nullptr); + } + + gboolean has_forward; + gboolean has_forward2; + gboolean has_backward; + gboolean has_backward2; + + gtk_style_context_get_style( context, + "has-forward-stepper", &has_forward, + "has-secondary-forward-stepper", &has_forward2, + "has-backward-stepper", &has_backward, + "has-secondary-backward-stepper", &has_backward2, nullptr ); + + if ( nPart == ControlPart::DrawBackgroundHorz ) + { + // Center vertically in the track + scrollbarRect.Move( 0, (scrollbarRect.GetHeight() - slider_side) / 2 ); + scrollbarRect.SetSize( Size( scrollbarRect.GetWidth(), slider_side ) ); + thumbRect.Move( 0, (scrollbarRect.GetHeight() - slider_side) / 2 ); + thumbRect.SetSize( Size( thumbRect.GetWidth(), slider_side ) ); + + scrollbarOrientation = GTK_ORIENTATION_HORIZONTAL; + arrow1Angle = G_PI * 3 / 2; + arrow2Angle = G_PI / 2; + + if ( has_backward ) + { + button12BoundRect.Move( stepper_size, + (scrollbarRect.GetHeight() - slider_width) / 2 ); + } + + button11BoundRect.Move( 0, (scrollbarRect.GetHeight() - slider_width) / 2 ); + button11BoundRect.SetSize( Size( stepper_size, slider_width ) ); + button12BoundRect.SetSize( Size( stepper_size, slider_width ) ); + + if ( has_backward2 ) + { + button22BoundRect.Move( stepper_size, (scrollbarRect.GetHeight() - slider_width) / 2 ); + button21BoundRect.Move( 0, (scrollbarRect.GetHeight() - slider_width) / 2 ); + } + else + { + button22BoundRect.Move( 0, (scrollbarRect.GetHeight() - slider_width) / 2 ); + } + + button21BoundRect.SetSize( Size( stepper_size, slider_width ) ); + button22BoundRect.SetSize( Size( stepper_size, slider_width ) ); + } + else + { + // Center horizontally in the track + scrollbarRect.Move( (scrollbarRect.GetWidth() - slider_side) / 2, 0 ); + scrollbarRect.SetSize( Size( slider_side, scrollbarRect.GetHeight() ) ); + thumbRect.Move( (scrollbarRect.GetWidth() - slider_side) / 2, 0 ); + thumbRect.SetSize( Size( slider_side, thumbRect.GetHeight() ) ); + + scrollbarOrientation = GTK_ORIENTATION_VERTICAL; + arrow1Angle = 0; + arrow2Angle = G_PI; + + if ( has_backward ) + { + button12BoundRect.Move( (scrollbarRect.GetWidth() - slider_width) / 2, + stepper_size ); + } + button11BoundRect.Move( (scrollbarRect.GetWidth() - slider_width) / 2, 0 ); + button11BoundRect.SetSize( Size( slider_width, stepper_size ) ); + button12BoundRect.SetSize( Size( slider_width, stepper_size ) ); + + if ( has_backward2 ) + { + button22BoundRect.Move( (scrollbarRect.GetWidth() - slider_width) / 2, stepper_size ); + button21BoundRect.Move( (scrollbarRect.GetWidth() - slider_width) / 2, 0 ); + } + else + { + button22BoundRect.Move( (scrollbarRect.GetWidth() - slider_width) / 2, 0 ); + } + + button21BoundRect.SetSize( Size( slider_width, stepper_size ) ); + button22BoundRect.SetSize( Size( slider_width, stepper_size ) ); + } + + bool has_slider = !thumbRect.IsEmpty(); + + // ----------------- CONTENTS + GtkStyleContext* pScrollbarContentsStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarContentsStyle : mpHScrollbarContentsStyle; + + gtk_render_background(gtk_widget_get_style_context(gCacheWindow), cr, 0, 0, + scrollbarRect.GetWidth(), scrollbarRect.GetHeight() ); + + gtk_render_background(context, cr, 0, 0, + scrollbarRect.GetWidth(), scrollbarRect.GetHeight() ); + gtk_render_frame(context, cr, 0, 0, + scrollbarRect.GetWidth(), scrollbarRect.GetHeight() ); + + gtk_render_background(pScrollbarContentsStyle, cr, 0, 0, + scrollbarRect.GetWidth(), scrollbarRect.GetHeight() ); + gtk_render_frame(pScrollbarContentsStyle, cr, 0, 0, + scrollbarRect.GetWidth(), scrollbarRect.GetHeight() ); + + bool backwardButtonInsensitive = + rScrollbarVal.mnCur == rScrollbarVal.mnMin; + bool forwardButtonInsensitive = rScrollbarVal.mnMax == 0 || + rScrollbarVal.mnCur + rScrollbarVal.mnVisibleSize >= rScrollbarVal.mnMax; + + // ----------------- BUTTON 1 + if ( has_backward ) + { + stateFlags = NWConvertVCLStateToGTKState(rScrollbarVal.mnButton1State); + if ( backwardButtonInsensitive ) + stateFlags = GTK_STATE_FLAG_INSENSITIVE; + + GtkStyleContext* pScrollbarButtonStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarButtonStyle : mpHScrollbarButtonStyle; + + gtk_style_context_set_state(pScrollbarButtonStyle, stateFlags); + + gtk_render_background(pScrollbarButtonStyle, cr, + button11BoundRect.Left(), button11BoundRect.Top(), + button11BoundRect.GetWidth(), button11BoundRect.GetHeight() ); + gtk_render_frame(pScrollbarButtonStyle, cr, + button11BoundRect.Left(), button11BoundRect.Top(), + button11BoundRect.GetWidth(), button11BoundRect.GetHeight() ); + + // ----------------- ARROW 1 + NWCalcArrowRect( button11BoundRect, arrowRect ); + gtk_render_arrow(pScrollbarButtonStyle, cr, + arrow1Angle, + arrowRect.Left(), arrowRect.Top(), + MIN(arrowRect.GetWidth(), arrowRect.GetHeight()) ); + } + if ( has_forward2 ) + { + stateFlags = NWConvertVCLStateToGTKState(rScrollbarVal.mnButton2State); + if ( forwardButtonInsensitive ) + stateFlags = GTK_STATE_FLAG_INSENSITIVE; + + GtkStyleContext* pScrollbarButtonStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarButtonStyle : mpHScrollbarButtonStyle; + + gtk_style_context_set_state(pScrollbarButtonStyle, stateFlags); + + gtk_render_background(pScrollbarButtonStyle, cr, + button12BoundRect.Left(), button12BoundRect.Top(), + button12BoundRect.GetWidth(), button12BoundRect.GetHeight() ); + gtk_render_frame(pScrollbarButtonStyle, cr, + button12BoundRect.Left(), button12BoundRect.Top(), + button12BoundRect.GetWidth(), button12BoundRect.GetHeight() ); + + // ----------------- ARROW 1 + NWCalcArrowRect( button12BoundRect, arrowRect ); + gtk_render_arrow(pScrollbarButtonStyle, cr, + arrow2Angle, + arrowRect.Left(), arrowRect.Top(), + MIN(arrowRect.GetWidth(), arrowRect.GetHeight()) ); + } + // ----------------- BUTTON 2 + + if ( has_forward ) + { + stateFlags = NWConvertVCLStateToGTKState(rScrollbarVal.mnButton2State); + if ( forwardButtonInsensitive ) + stateFlags = GTK_STATE_FLAG_INSENSITIVE; + + GtkStyleContext* pScrollbarButtonStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarButtonStyle : mpHScrollbarButtonStyle; + + gtk_style_context_set_state(pScrollbarButtonStyle, stateFlags); + + gtk_render_background(pScrollbarButtonStyle, cr, + button22BoundRect.Left(), button22BoundRect.Top(), + button22BoundRect.GetWidth(), button22BoundRect.GetHeight() ); + gtk_render_frame(pScrollbarButtonStyle, cr, + button22BoundRect.Left(), button22BoundRect.Top(), + button22BoundRect.GetWidth(), button22BoundRect.GetHeight() ); + + // ----------------- ARROW 2 + NWCalcArrowRect( button22BoundRect, arrowRect ); + gtk_render_arrow(pScrollbarButtonStyle, cr, + arrow2Angle, + arrowRect.Left(), arrowRect.Top(), + MIN(arrowRect.GetWidth(), arrowRect.GetHeight()) ); + } + + if ( has_backward2 ) + { + stateFlags = NWConvertVCLStateToGTKState(rScrollbarVal.mnButton1State); + if ( backwardButtonInsensitive ) + stateFlags = GTK_STATE_FLAG_INSENSITIVE; + + GtkStyleContext* pScrollbarButtonStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarButtonStyle : mpHScrollbarButtonStyle; + + gtk_style_context_set_state(pScrollbarButtonStyle, stateFlags); + + gtk_render_background(pScrollbarButtonStyle, cr, + button21BoundRect.Left(), button21BoundRect.Top(), + button21BoundRect.GetWidth(), button21BoundRect.GetHeight() ); + gtk_render_frame(pScrollbarButtonStyle, cr, + button21BoundRect.Left(), button21BoundRect.Top(), + button21BoundRect.GetWidth(), button21BoundRect.GetHeight() ); + + // ----------------- ARROW 2 + NWCalcArrowRect( button21BoundRect, arrowRect ); + gtk_render_arrow(pScrollbarButtonStyle, cr, + arrow1Angle, + arrowRect.Left(), arrowRect.Top(), + MIN(arrowRect.GetWidth(), arrowRect.GetHeight()) ); + } + + // ----------------- TROUGH + // trackrect matches that of ScrollBar::ImplCalc + tools::Rectangle aTrackRect(Point(0, 0), scrollbarRect.GetSize()); + if (nPart == ControlPart::DrawBackgroundHorz) + { + tools::Rectangle aBtn1Rect = NWGetScrollButtonRect(ControlPart::ButtonLeft, aTrackRect); + tools::Rectangle aBtn2Rect = NWGetScrollButtonRect(ControlPart::ButtonRight, aTrackRect); + if (!aBtn1Rect.IsWidthEmpty()) + aTrackRect.SetLeft( aBtn1Rect.Right() ); + if (!aBtn2Rect.IsWidthEmpty()) + aTrackRect.SetRight( aBtn2Rect.Left() ); + } + else + { + tools::Rectangle aBtn1Rect = NWGetScrollButtonRect(ControlPart::ButtonUp, aTrackRect); + tools::Rectangle aBtn2Rect = NWGetScrollButtonRect(ControlPart::ButtonDown, aTrackRect); + if (!aBtn1Rect.IsHeightEmpty()) + aTrackRect.SetTop( aBtn1Rect.Bottom() + 1 ); + if (!aBtn2Rect.IsHeightEmpty()) + aTrackRect.SetBottom( aBtn2Rect.Top() ); + } + + GtkStyleContext* pScrollbarTroughStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarTroughStyle : mpHScrollbarTroughStyle; + gtk_render_background(pScrollbarTroughStyle, cr, aTrackRect.Left(), aTrackRect.Top(), + aTrackRect.GetWidth(), aTrackRect.GetHeight() ); + gtk_render_frame(pScrollbarTroughStyle, cr, aTrackRect.Left(), aTrackRect.Top(), + aTrackRect.GetWidth(), aTrackRect.GetHeight() ); + + // ----------------- THUMB + if ( !has_slider ) + return; + + stateFlags = NWConvertVCLStateToGTKState(rScrollbarVal.mnThumbState); + if ( rScrollbarVal.mnThumbState & ControlState::PRESSED ) + stateFlags = static_cast(stateFlags | GTK_STATE_FLAG_PRELIGHT); + + GtkStyleContext* pScrollbarSliderStyle = scrollbarOrientation == GTK_ORIENTATION_VERTICAL ? + mpVScrollbarSliderStyle : mpHScrollbarSliderStyle; + + gtk_style_context_set_state(pScrollbarSliderStyle, stateFlags); + + GtkBorder margin; + style_context_get_margin(pScrollbarSliderStyle, &margin); + + gtk_render_background(pScrollbarSliderStyle, cr, + thumbRect.Left() + margin.left, thumbRect.Top() + margin.top, + thumbRect.GetWidth() - margin.left - margin.right, + thumbRect.GetHeight() - margin.top - margin.bottom); + + gtk_render_frame(pScrollbarSliderStyle, cr, + thumbRect.Left() + margin.left, thumbRect.Top() + margin.top, + thumbRect.GetWidth() - margin.left - margin.right, + thumbRect.GetHeight() - margin.top - margin.bottom); +} + +void GtkSalGraphics::PaintOneSpinButton( GtkStyleContext *context, + cairo_t *cr, + ControlPart nPart, + tools::Rectangle aAreaRect, + ControlState nState ) +{ + GtkBorder padding, border; + + GtkStateFlags stateFlags = NWConvertVCLStateToGTKState(nState); + tools::Rectangle buttonRect = NWGetSpinButtonRect( nPart, aAreaRect ); + + gtk_style_context_set_state(context, stateFlags); + + style_context_get_padding(context, &padding); + style_context_get_border(context, &border); + + gtk_render_background(context, cr, + buttonRect.Left(), buttonRect.Top(), + buttonRect.GetWidth(), buttonRect.GetHeight() ); + + gint iconWidth = buttonRect.GetWidth() - padding.left - padding.right - border.left - border.right; + gint iconHeight = buttonRect.GetHeight() - padding.top - padding.bottom - border.top - border.bottom; + + const char* icon = (nPart == ControlPart::ButtonUp) ? "list-add-symbolic" : "list-remove-symbolic"; + GtkIconTheme *pIconTheme = gtk_icon_theme_get_for_screen(gtk_widget_get_screen(mpWindow)); + + gint scale = gtk_style_context_get_scale (context); + GtkIconInfo *info = gtk_icon_theme_lookup_icon_for_scale(pIconTheme, icon, std::min(iconWidth, iconHeight), scale, + static_cast(0)); + + GdkPixbuf *pixbuf = gtk_icon_info_load_symbolic_for_context(info, context, nullptr, nullptr); + g_object_unref(info); + + iconWidth = gdk_pixbuf_get_width(pixbuf)/scale; + iconHeight = gdk_pixbuf_get_height(pixbuf)/scale; + tools::Rectangle arrowRect(buttonRect.Center() - Point(iconWidth / 2, iconHeight / 2), + Size(iconWidth, iconHeight)); + + gtk_style_context_save (context); + gtk_style_context_set_scale (context, 1); + gtk_render_icon(context, cr, pixbuf, arrowRect.Left(), arrowRect.Top()); + gtk_style_context_restore (context); + g_object_unref(pixbuf); + + gtk_render_frame(context, cr, + buttonRect.Left(), buttonRect.Top(), + buttonRect.GetWidth(), buttonRect.GetHeight() ); +} + +void GtkSalGraphics::PaintSpinButton(GtkStateFlags flags, + cairo_t *cr, + const tools::Rectangle& rControlRectangle, + ControlPart nPart, + const ImplControlValue& rValue ) +{ + const SpinbuttonValue *pSpinVal = (rValue.getType() == ControlType::SpinButtons) ? static_cast(&rValue) : nullptr; + ControlPart upBtnPart = ControlPart::ButtonUp; + ControlState upBtnState = ControlState::NONE; + ControlPart downBtnPart = ControlPart::ButtonDown; + ControlState downBtnState = ControlState::NONE; + + if ( pSpinVal ) + { + upBtnPart = pSpinVal->mnUpperPart; + upBtnState = pSpinVal->mnUpperState; + + downBtnPart = pSpinVal->mnLowerPart; + downBtnState = pSpinVal->mnLowerState; + } + + if (nPart == ControlPart::Entire) + { + gtk_style_context_set_state(mpWindowStyle, flags); + + gtk_render_background(mpWindowStyle, cr, + 0, 0, + rControlRectangle.GetWidth(), rControlRectangle.GetHeight()); + + gtk_style_context_set_state(mpSpinStyle, flags); + + gtk_render_background(mpSpinStyle, cr, + 0, 0, + rControlRectangle.GetWidth(), rControlRectangle.GetHeight()); + } + + cairo_translate(cr, -rControlRectangle.Left(), -rControlRectangle.Top()); + PaintOneSpinButton(mpSpinUpStyle, cr, upBtnPart, rControlRectangle, upBtnState ); + PaintOneSpinButton(mpSpinDownStyle, cr, downBtnPart, rControlRectangle, downBtnState ); + cairo_translate(cr, rControlRectangle.Left(), rControlRectangle.Top()); + + if (nPart == ControlPart::Entire) + { + gtk_render_frame(mpSpinStyle, cr, + 0, 0, + rControlRectangle.GetWidth(), rControlRectangle.GetHeight() ); + } +} + +#define FALLBACK_ARROW_SIZE gint(11 * 0.85) + +tools::Rectangle GtkSalGraphics::NWGetComboBoxButtonRect(ControlType nType, + ControlPart nPart, + tools::Rectangle aAreaRect ) +{ + tools::Rectangle aButtonRect; + + GtkBorder padding; + if (nType == ControlType::Listbox) + style_context_get_padding(mpListboxButtonStyle, &padding); + else + style_context_get_padding(mpButtonStyle, &padding); + + gint nArrowWidth = FALLBACK_ARROW_SIZE; + gtk_style_context_get(mpComboboxButtonArrowStyle, + gtk_style_context_get_state(mpComboboxButtonArrowStyle), + "min-width", &nArrowWidth, nullptr); + + gint nButtonWidth = nArrowWidth + padding.left + padding.right; + if( nPart == ControlPart::ButtonDown ) + { + Point aPos(aAreaRect.Left() + aAreaRect.GetWidth() - nButtonWidth, aAreaRect.Top()); + if (AllSettings::GetLayoutRTL()) + aPos.setX( aAreaRect.Left() ); + aButtonRect.SetSize( Size( nButtonWidth, aAreaRect.GetHeight() ) ); + aButtonRect.SetPos(aPos); + } + else if( nPart == ControlPart::SubEdit ) + { + gint adjust_left = padding.left; + gint adjust_top = padding.top; + gint adjust_right = padding.right; + gint adjust_bottom = padding.bottom; + + aButtonRect.SetSize( Size( aAreaRect.GetWidth() - nButtonWidth - (adjust_left + adjust_right), + aAreaRect.GetHeight() - (adjust_top + adjust_bottom)) ); + Point aEditPos = aAreaRect.TopLeft(); + if (AllSettings::GetLayoutRTL()) + aEditPos.AdjustX(nButtonWidth ); + else + aEditPos.AdjustX(adjust_left ); + aEditPos.AdjustY(adjust_top ); + aButtonRect.SetPos( aEditPos ); + } + + return aButtonRect; +} + +void GtkSalGraphics::PaintCombobox( GtkStateFlags flags, cairo_t *cr, + const tools::Rectangle& rControlRectangle, + ControlType nType, + ControlPart nPart ) +{ + tools::Rectangle areaRect; + tools::Rectangle buttonRect; + tools::Rectangle arrowRect; + + // Find the overall bounding rect of the buttons's drawing area, + // plus its actual draw rect excluding adornment + areaRect = rControlRectangle; + + buttonRect = NWGetComboBoxButtonRect(ControlType::Combobox, ControlPart::ButtonDown, areaRect); + + tools::Rectangle aEditBoxRect( areaRect ); + aEditBoxRect.SetSize( Size( areaRect.GetWidth() - buttonRect.GetWidth(), aEditBoxRect.GetHeight() ) ); + if (AllSettings::GetLayoutRTL()) + aEditBoxRect.SetPos( Point( areaRect.Left() + buttonRect.GetWidth(), areaRect.Top() ) ); + + gint arrow_width = FALLBACK_ARROW_SIZE, arrow_height = FALLBACK_ARROW_SIZE; + if (nType == ControlType::Combobox) + { + gtk_style_context_get(mpComboboxButtonArrowStyle, + gtk_style_context_get_state(mpComboboxButtonArrowStyle), + "min-width", &arrow_width, "min-height", &arrow_height, nullptr); + } + else if (nType == ControlType::Listbox) + { + gtk_style_context_get(mpListboxButtonArrowStyle, + gtk_style_context_get_state(mpListboxButtonArrowStyle), + "min-width", &arrow_width, "min-height", &arrow_height, nullptr); + } + + arrowRect.SetSize(Size(arrow_width, arrow_height)); + arrowRect.SetPos( Point( buttonRect.Left() + static_cast((buttonRect.GetWidth() - arrowRect.GetWidth()) / 2), + buttonRect.Top() + static_cast((buttonRect.GetHeight() - arrowRect.GetHeight()) / 2) ) ); + + + tools::Rectangle aRect(Point(0, 0), Size(areaRect.GetWidth(), areaRect.GetHeight())); + + if (nType == ControlType::Combobox) + { + if( nPart == ControlPart::Entire ) + { + render_common(mpComboboxStyle, cr, aRect, flags); + render_common(mpComboboxBoxStyle, cr, aRect, flags); + tools::Rectangle aEntryRect(Point(aEditBoxRect.Left() - areaRect.Left(), + aEditBoxRect.Top() - areaRect.Top()), + Size(aEditBoxRect.GetWidth(), aEditBoxRect.GetHeight())); + + GtkJunctionSides eJuncSides = gtk_style_context_get_junction_sides(mpComboboxEntryStyle); + if (AllSettings::GetLayoutRTL()) + gtk_style_context_set_junction_sides(mpComboboxEntryStyle, GTK_JUNCTION_LEFT); + else + gtk_style_context_set_junction_sides(mpComboboxEntryStyle, GTK_JUNCTION_RIGHT); + render_common(mpComboboxEntryStyle, cr, aEntryRect, flags); + gtk_style_context_set_junction_sides(mpComboboxEntryStyle, eJuncSides); + } + + tools::Rectangle aButtonRect(Point(buttonRect.Left() - areaRect.Left(), buttonRect.Top() - areaRect.Top()), + Size(buttonRect.GetWidth(), buttonRect.GetHeight())); + GtkJunctionSides eJuncSides = gtk_style_context_get_junction_sides(mpComboboxButtonStyle); + if (AllSettings::GetLayoutRTL()) + gtk_style_context_set_junction_sides(mpComboboxButtonStyle, GTK_JUNCTION_RIGHT); + else + gtk_style_context_set_junction_sides(mpComboboxButtonStyle, GTK_JUNCTION_LEFT); + render_common(mpComboboxButtonStyle, cr, aButtonRect, flags); + gtk_style_context_set_junction_sides(mpComboboxButtonStyle, eJuncSides); + + gtk_render_arrow(mpComboboxButtonArrowStyle, cr, + G_PI, + (arrowRect.Left() - areaRect.Left()), (arrowRect.Top() - areaRect.Top()), + arrowRect.GetWidth() ); + } + else if (nType == ControlType::Listbox) + { + if( nPart == ControlPart::ListboxWindow ) + { + /* render the popup window with the menu style */ + gtk_render_frame(mpMenuStyle, cr, + 0, 0, + areaRect.GetWidth(), areaRect.GetHeight()); + } + else + { + render_common(mpListboxStyle, cr, aRect, flags); + render_common(mpListboxButtonStyle, cr, aRect, flags); + render_common(mpListboxBoxStyle, cr, aRect, flags); + + gtk_render_arrow(mpListboxButtonArrowStyle, cr, + G_PI, + (arrowRect.Left() - areaRect.Left()), (arrowRect.Top() - areaRect.Top()), + arrowRect.GetWidth() ); + } + } +} + +static void appendComboEntry(GtkWidgetPath* pSiblingsPath) +{ + gtk_widget_path_append_type(pSiblingsPath, GTK_TYPE_ENTRY); + gtk_widget_path_iter_set_object_name(pSiblingsPath, -1, "entry"); + gtk_widget_path_iter_add_class(pSiblingsPath, -1, "combo"); +} + +static void appendComboButton(GtkWidgetPath* pSiblingsPath) +{ + gtk_widget_path_append_type(pSiblingsPath, GTK_TYPE_BUTTON); + gtk_widget_path_iter_set_object_name(pSiblingsPath, -1, "button"); + gtk_widget_path_iter_add_class(pSiblingsPath, -1, "combo"); +} + +static GtkWidgetPath* buildLTRComboSiblingsPath() +{ + GtkWidgetPath* pSiblingsPath = gtk_widget_path_new(); + + appendComboEntry(pSiblingsPath); + appendComboButton(pSiblingsPath); + + return pSiblingsPath; +} + +static GtkWidgetPath* buildRTLComboSiblingsPath() +{ + GtkWidgetPath* pSiblingsPath = gtk_widget_path_new(); + + appendComboButton(pSiblingsPath); + appendComboEntry(pSiblingsPath); + + return pSiblingsPath; +} + + +GtkStyleContext* GtkSalGraphics::makeContext(GtkWidgetPath *pPath, GtkStyleContext *pParent) +{ + GtkStyleContext* context = gtk_style_context_new(); + gtk_style_context_set_screen(context, gtk_widget_get_screen(mpWindow)); + gtk_style_context_set_path(context, pPath); + if (pParent == nullptr) + { + GtkWidget* pTopLevel = widget_get_toplevel(mpWindow); + GtkStyleContext* pStyle = gtk_widget_get_style_context(pTopLevel); + gtk_style_context_set_parent(context, pStyle); + gtk_style_context_set_scale (context, gtk_style_context_get_scale (pStyle)); + } + else + { + gtk_style_context_set_parent(context, pParent); + gtk_style_context_set_scale (context, gtk_style_context_get_scale (pParent)); + } + gtk_widget_path_unref(pPath); + return context; +} + +GtkStyleContext* GtkSalGraphics::createStyleContext(GtkControlPart ePart) +{ + switch (ePart) + { + case GtkControlPart::ToplevelWindow: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, G_TYPE_NONE); + gtk_widget_path_iter_set_object_name(path, -1, "window"); + gtk_widget_path_iter_add_class(path, -1, "background"); + return makeContext(path, nullptr); + } + case GtkControlPart::Button: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_BUTTON); + gtk_widget_path_iter_set_object_name(path, -1, "button"); + return makeContext(path, nullptr); + } + case GtkControlPart::LinkButton: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_BUTTON); + gtk_widget_path_iter_set_object_name(path, -1, "button"); + gtk_widget_path_iter_add_class(path, -1, "link"); + return makeContext(path, nullptr); + } + case GtkControlPart::CheckButton: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_CHECK_BUTTON); + gtk_widget_path_iter_set_object_name(path, -1, "checkbutton"); + return makeContext(path, nullptr); + } + case GtkControlPart::CheckButtonCheck: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpCheckButtonStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_CHECK_BUTTON); + gtk_widget_path_iter_set_object_name(path, -1, "check"); + return makeContext(path, mpCheckButtonStyle); + } + case GtkControlPart::RadioButton: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_RADIO_BUTTON); + gtk_widget_path_iter_set_object_name(path, -1, "radiobutton"); + return makeContext(path, nullptr); + } + case GtkControlPart::RadioButtonRadio: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpRadioButtonStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_RADIO_BUTTON); + gtk_widget_path_iter_set_object_name(path, -1, "radio"); + return makeContext(path, mpRadioButtonStyle); + } + case GtkControlPart::ComboboxBoxButtonBoxArrow: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpComboboxButtonBoxStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_RADIO_BUTTON); + gtk_widget_path_append_type(path, GTK_TYPE_BUTTON); + gtk_widget_path_iter_set_object_name(path, -1, "arrow"); + return makeContext(path, mpComboboxButtonBoxStyle); + } + case GtkControlPart::ListboxBoxButtonBoxArrow: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpListboxButtonBoxStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_RADIO_BUTTON); + gtk_widget_path_append_type(path, GTK_TYPE_BUTTON); + gtk_widget_path_iter_set_object_name(path, -1, "arrow"); + return makeContext(path, mpListboxButtonBoxStyle); + } + case GtkControlPart::Entry: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_ENTRY); + gtk_widget_path_iter_set_object_name(path, -1, "entry"); + return makeContext(path, nullptr); + } + case GtkControlPart::Combobox: + case GtkControlPart::Listbox: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, G_TYPE_NONE); + gtk_widget_path_iter_set_object_name(path, -1, "combobox"); + return makeContext(path, nullptr); + } + case GtkControlPart::ComboboxBox: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpComboboxStyle)); + gtk_widget_path_append_type(path, G_TYPE_NONE); + gtk_widget_path_iter_set_object_name(path, -1, "box"); + gtk_widget_path_iter_add_class(path, -1, "horizontal"); + gtk_widget_path_iter_add_class(path, -1, "linked"); + return makeContext(path, mpComboboxStyle); + } + case GtkControlPart::ListboxBox: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpListboxStyle)); + gtk_widget_path_append_type(path, G_TYPE_NONE); + gtk_widget_path_iter_set_object_name(path, -1, "box"); + gtk_widget_path_iter_add_class(path, -1, "horizontal"); + gtk_widget_path_iter_add_class(path, -1, "linked"); + return makeContext(path, mpListboxStyle); + } + case GtkControlPart::ComboboxBoxEntry: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpComboboxBoxStyle)); + GtkWidgetPath* pSiblingsPath; + if (AllSettings::GetLayoutRTL()) + { + pSiblingsPath = buildRTLComboSiblingsPath(); + gtk_widget_path_append_with_siblings(path, pSiblingsPath, 1); + } + else + { + pSiblingsPath = buildLTRComboSiblingsPath(); + gtk_widget_path_append_with_siblings(path, pSiblingsPath, 0); + } + gtk_widget_path_unref(pSiblingsPath); + return makeContext(path, mpComboboxBoxStyle); + } + case GtkControlPart::ComboboxBoxButton: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpComboboxBoxStyle)); + GtkWidgetPath* pSiblingsPath; + if (AllSettings::GetLayoutRTL()) + { + pSiblingsPath = buildRTLComboSiblingsPath(); + gtk_widget_path_append_with_siblings(path, pSiblingsPath, 0); + } + else + { + pSiblingsPath = buildLTRComboSiblingsPath(); + gtk_widget_path_append_with_siblings(path, pSiblingsPath, 1); + } + gtk_widget_path_unref(pSiblingsPath); + return makeContext(path, mpComboboxBoxStyle); + } + case GtkControlPart::ListboxBoxButton: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpListboxBoxStyle)); + GtkWidgetPath* pSiblingsPath = gtk_widget_path_new(); + + gtk_widget_path_append_type(pSiblingsPath, GTK_TYPE_BUTTON); + gtk_widget_path_iter_set_object_name(pSiblingsPath, -1, "button"); + gtk_widget_path_iter_add_class(pSiblingsPath, -1, "combo"); + + gtk_widget_path_append_with_siblings(path, pSiblingsPath, 0); + gtk_widget_path_unref(pSiblingsPath); + return makeContext(path, mpListboxBoxStyle); + } + case GtkControlPart::ComboboxBoxButtonBox: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpComboboxButtonStyle)); + gtk_widget_path_append_type(path, G_TYPE_NONE); + gtk_widget_path_iter_set_object_name(path, -1, "box"); + gtk_widget_path_iter_add_class(path, -1, "horizontal"); + return makeContext(path, mpComboboxButtonStyle); + } + case GtkControlPart::ListboxBoxButtonBox: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpListboxButtonStyle)); + gtk_widget_path_append_type(path, G_TYPE_NONE); + gtk_widget_path_iter_set_object_name(path, -1, "box"); + gtk_widget_path_iter_add_class(path, -1, "horizontal"); + return makeContext(path, mpListboxButtonStyle); + } + case GtkControlPart::SpinButton: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpWindowStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_SPIN_BUTTON); + gtk_widget_path_iter_set_object_name(path, -1, "spinbutton"); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_HORIZONTAL); + return makeContext(path, mpWindowStyle); + } + case GtkControlPart::SpinButtonUpButton: + case GtkControlPart::SpinButtonDownButton: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpSpinStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_SPIN_BUTTON); + gtk_widget_path_iter_set_object_name(path, -1, "button"); + gtk_widget_path_iter_add_class(path, -1, ePart == GtkControlPart::SpinButtonUpButton ? "up" : "down"); + return makeContext(path, mpSpinStyle); + } + case GtkControlPart::ScrollbarVertical: + case GtkControlPart::ScrollbarHorizontal: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_SCROLLBAR); + gtk_widget_path_iter_set_object_name(path, -1, "scrollbar"); + gtk_widget_path_iter_add_class(path, -1, ePart == GtkControlPart::ScrollbarVertical ? "vertical" : "horizontal"); + return makeContext(path, nullptr); + } + case GtkControlPart::ScrollbarVerticalContents: + case GtkControlPart::ScrollbarHorizontalContents: + { + GtkStyleContext *pParent = + (ePart == GtkControlPart::ScrollbarVerticalContents) ? mpVScrollbarStyle : mpHScrollbarStyle; + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(pParent)); + gtk_widget_path_append_type(path, GTK_TYPE_SCROLLBAR); + gtk_widget_path_iter_set_object_name(path, -1, "contents"); + return makeContext(path, pParent); + } + case GtkControlPart::ScrollbarVerticalTrough: + case GtkControlPart::ScrollbarHorizontalTrough: + { + GtkStyleContext *pParent = + (ePart == GtkControlPart::ScrollbarVerticalTrough) ? mpVScrollbarContentsStyle : mpHScrollbarContentsStyle; + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(pParent)); + gtk_widget_path_append_type(path, GTK_TYPE_SCROLLBAR); + gtk_widget_path_iter_set_object_name(path, -1, "trough"); + return makeContext(path, pParent); + } + case GtkControlPart::ScrollbarVerticalSlider: + case GtkControlPart::ScrollbarHorizontalSlider: + { + GtkStyleContext *pParent = + (ePart == GtkControlPart::ScrollbarVerticalSlider) ? mpVScrollbarTroughStyle : mpHScrollbarTroughStyle; + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(pParent)); + gtk_widget_path_append_type(path, GTK_TYPE_SCROLLBAR); + gtk_widget_path_iter_set_object_name(path, -1, "slider"); + return makeContext(path, pParent); + } + case GtkControlPart::ScrollbarVerticalButton: + case GtkControlPart::ScrollbarHorizontalButton: + { + GtkStyleContext *pParent = + (ePart == GtkControlPart::ScrollbarVerticalButton) ? mpVScrollbarStyle : mpHScrollbarStyle; + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(pParent)); + gtk_widget_path_append_type(path, GTK_TYPE_SCROLLBAR); + gtk_widget_path_iter_set_object_name(path, -1, "button"); + return makeContext(path, pParent); + } + case GtkControlPart::ProgressBar: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_PROGRESS_BAR); + gtk_widget_path_iter_set_object_name(path, -1, "progressbar"); + gtk_widget_path_iter_add_class(path, -1, GTK_STYLE_CLASS_HORIZONTAL); + return makeContext(path, nullptr); + } + case GtkControlPart::ProgressBarTrough: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpProgressBarStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_PROGRESS_BAR); + gtk_widget_path_iter_set_object_name(path, -1, "trough"); + return makeContext(path, mpProgressBarStyle); + } + case GtkControlPart::ProgressBarProgress: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpProgressBarTroughStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_PROGRESS_BAR); + gtk_widget_path_iter_set_object_name(path, -1, "progress"); + return makeContext(path, mpProgressBarTroughStyle); + } + case GtkControlPart::Notebook: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpWindowStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_NOTEBOOK); + gtk_widget_path_iter_set_object_name(path, -1, "notebook"); + return makeContext(path, mpWindowStyle); + } + case GtkControlPart::NotebookStack: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpNotebookStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_NOTEBOOK); + gtk_widget_path_iter_set_object_name(path, -1, "stack"); + return makeContext(path, mpNotebookStyle); + } + case GtkControlPart::NotebookHeader: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpNotebookStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_NOTEBOOK); + gtk_widget_path_iter_set_object_name(path, -1, "header"); + gtk_widget_path_iter_add_class(path, -1, "frame"); + gtk_widget_path_iter_add_class(path, -1, "top"); + return makeContext(path, mpNotebookStyle); + } + case GtkControlPart::NotebookHeaderTabs: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpNotebookHeaderStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_NOTEBOOK); + gtk_widget_path_iter_set_object_name(path, -1, "tabs"); + gtk_widget_path_iter_add_class(path, -1, "top"); + return makeContext(path, mpNotebookHeaderStyle); + } + case GtkControlPart::NotebookHeaderTabsTab: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpNotebookHeaderTabsStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_NOTEBOOK); + gtk_widget_path_iter_set_object_name(path, -1, "tab"); + gtk_widget_path_iter_add_class(path, -1, "top"); + return makeContext(path, mpNotebookHeaderTabsStyle); + } + case GtkControlPart::NotebookHeaderTabsTabLabel: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpNotebookHeaderTabsTabStyle)); + gtk_widget_path_append_type(path, G_TYPE_NONE); + gtk_widget_path_iter_set_object_name(path, -1, "label"); + return makeContext(path, mpNotebookHeaderTabsTabStyle); + } + case GtkControlPart::NotebookHeaderTabsTabActiveLabel: + case GtkControlPart::NotebookHeaderTabsTabHoverLabel: + return mpNotebookHeaderTabsTabLabelStyle; + case GtkControlPart::FrameBorder: + { + GtkWidgetPath *path = gtk_widget_path_new(); + gtk_widget_path_append_type(path, GTK_TYPE_FRAME); + gtk_widget_path_iter_set_object_name(path, -1, "frame"); + gtk_widget_path_iter_add_class(path, -1, "frame"); + return makeContext(path, nullptr); + } + case GtkControlPart::MenuBar: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpWindowStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_MENU_BAR); + gtk_widget_path_iter_set_object_name(path, -1, "menubar"); + return makeContext(path, mpWindowStyle); + } + case GtkControlPart::MenuBarItem: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuBarStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_MENU_ITEM); + gtk_widget_path_iter_set_object_name(path, -1, "menuitem"); + return makeContext(path, mpMenuBarStyle); + } + case GtkControlPart::MenuWindow: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuBarItemStyle)); + gtk_widget_path_append_type(path, G_TYPE_NONE); + gtk_widget_path_iter_set_object_name(path, -1, "window"); + gtk_widget_path_iter_add_class(path, -1, "background"); + gtk_widget_path_iter_add_class(path, -1, "popup"); + return makeContext(path, mpMenuBarItemStyle); + } + case GtkControlPart::Menu: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuWindowStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_MENU); + gtk_widget_path_iter_set_object_name(path, -1, "menu"); + return makeContext(path, mpMenuWindowStyle); + } + case GtkControlPart::MenuItem: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_MENU_ITEM); + gtk_widget_path_iter_set_object_name(path, -1, "menuitem"); + return makeContext(path, mpMenuStyle); + } + case GtkControlPart::MenuItemLabel: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuItemStyle)); + gtk_widget_path_append_type(path, G_TYPE_NONE); + gtk_widget_path_iter_set_object_name(path, -1, "label"); + return makeContext(path, mpMenuItemStyle); + } + case GtkControlPart::MenuItemArrow: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuItemStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_MENU_ITEM); + gtk_widget_path_iter_set_object_name(path, -1, "arrow"); + return makeContext(path, mpMenuItemStyle); + } + case GtkControlPart::CheckMenuItem: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_CHECK_MENU_ITEM); + gtk_widget_path_iter_set_object_name(path, -1, "menuitem"); + return makeContext(path, mpMenuStyle); + } + case GtkControlPart::CheckMenuItemCheck: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpCheckMenuItemStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_CHECK_MENU_ITEM); + gtk_widget_path_iter_set_object_name(path, -1, "check"); + return makeContext(path, mpCheckMenuItemStyle); + } + case GtkControlPart::RadioMenuItem: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_RADIO_MENU_ITEM); + gtk_widget_path_iter_set_object_name(path, -1, "menuitem"); + return makeContext(path, mpMenuStyle); + } + case GtkControlPart::RadioMenuItemRadio: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpRadioMenuItemStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_RADIO_MENU_ITEM); + gtk_widget_path_iter_set_object_name(path, -1, "radio"); + return makeContext(path, mpRadioMenuItemStyle); + } + case GtkControlPart::SeparatorMenuItem: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpMenuStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_SEPARATOR_MENU_ITEM); + gtk_widget_path_iter_set_object_name(path, -1, "menuitem"); + return makeContext(path, mpMenuStyle); + } + case GtkControlPart::SeparatorMenuItemSeparator: + { + GtkWidgetPath *path = gtk_widget_path_copy(gtk_style_context_get_path(mpSeparatorMenuItemStyle)); + gtk_widget_path_append_type(path, GTK_TYPE_SEPARATOR_MENU_ITEM); + gtk_widget_path_iter_set_object_name(path, -1, "separator"); + return makeContext(path, mpSeparatorMenuItemStyle); + } + } + + return nullptr; +} + +#ifndef GTK_STYLE_CLASS_POPUP +constexpr OUStringLiteral GTK_STYLE_CLASS_POPUP = u"popup"; +#endif +#ifndef GTK_STYLE_CLASS_LABEL +constexpr OUStringLiteral GTK_STYLE_CLASS_LABEL = u"label"; +#endif + +void GtkSalGraphics::PaintCheckOrRadio(cairo_t *cr, GtkStyleContext *context, + const tools::Rectangle& rControlRectangle, bool bIsCheck, bool bInMenu) +{ + gint indicator_size; + gtk_style_context_get_style(context, "indicator-size", &indicator_size, nullptr); + + gint x = (rControlRectangle.GetWidth() - indicator_size) / 2; + gint y = (rControlRectangle.GetHeight() - indicator_size) / 2; + + if (!bInMenu) + gtk_render_background(context, cr, x, y, indicator_size, indicator_size); + + if (bIsCheck) + gtk_render_check(context, cr, x, y, indicator_size, indicator_size); + else + gtk_render_option(context, cr, x, y, indicator_size, indicator_size); + + gtk_render_frame(context, cr, x, y, indicator_size, indicator_size); +} + +void GtkSalGraphics::PaintCheck(cairo_t *cr, GtkStyleContext *context, + const tools::Rectangle& rControlRectangle, bool bInMenu) +{ + PaintCheckOrRadio(cr, context, rControlRectangle, true, bInMenu); +} + +void GtkSalGraphics::PaintRadio(cairo_t *cr, GtkStyleContext *context, + const tools::Rectangle& rControlRectangle, bool bInMenu) +{ + PaintCheckOrRadio(cr, context, rControlRectangle, false, bInMenu); +} + +static gfloat getArrowSize(GtkStyleContext* context) +{ + gint min_width, min_weight; + gtk_style_context_get_style(context, "min-width", &min_width, nullptr); + gtk_style_context_get_style(context, "min-height", &min_weight, nullptr); + gfloat arrow_size = 11 * MAX (min_width, min_weight); + return arrow_size; +} + +namespace +{ + void draw_vertical_separator(GtkStyleContext *context, cairo_t *cr, const tools::Rectangle& rControlRegion, gint nSeparatorWidth) + { + tools::Long nX = 0; + tools::Long nY = 0; + + gint nHalfSeparatorWidth = nSeparatorWidth / 2; + gint nHalfRegionWidth = rControlRegion.GetWidth() / 2; + + nX = nX + nHalfRegionWidth - nHalfSeparatorWidth; + nY = rControlRegion.GetHeight() > 5 ? 1 : 0; + int nHeight = rControlRegion.GetHeight() - (2 * nY); + + gtk_render_background(context, cr, nX, nY, nSeparatorWidth, nHeight); + gtk_render_frame(context, cr, nX, nY, nSeparatorWidth, nHeight); + } + + void draw_horizontal_separator(GtkStyleContext *context, cairo_t *cr, const tools::Rectangle& rControlRegion) + { + tools::Long nX = 0; + tools::Long nY = 0; + + gint nSeparatorHeight = 1; + + gtk_style_context_get(context, + gtk_style_context_get_state(context), + "min-height", &nSeparatorHeight, nullptr); + + gint nHalfSeparatorHeight = nSeparatorHeight / 2; + gint nHalfRegionHeight = rControlRegion.GetHeight() / 2; + + nY = nY + nHalfRegionHeight - nHalfSeparatorHeight; + nX = rControlRegion.GetWidth() > 5 ? 1 : 0; + int nWidth = rControlRegion.GetWidth() - (2 * nX); + + gtk_render_background(context, cr, nX, nY, nWidth, nSeparatorHeight); + gtk_render_frame(context, cr, nX, nY, nWidth, nSeparatorHeight); + } +} +#endif + +void GtkSalGraphics::handleDamage(const tools::Rectangle& rDamagedRegion) +{ + assert(m_pWidgetDraw); + assert(!rDamagedRegion.IsEmpty()); + mpFrame->damaged(rDamagedRegion.Left(), rDamagedRegion.Top(), rDamagedRegion.GetWidth(), rDamagedRegion.GetHeight()); +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +bool GtkSalGraphics::drawNativeControl( ControlType nType, ControlPart nPart, const tools::Rectangle& rControlRegion, + ControlState nState, const ImplControlValue& rValue, + const OUString&, const Color& rBackgroundColor) +{ + RenderType renderType = nPart == ControlPart::Focus ? RenderType::Focus : RenderType::BackgroundAndFrame; + GtkStyleContext *context = nullptr; + GdkPixbuf *pixbuf = nullptr; + bool bInMenu = false; + + GtkStateFlags flags = NWConvertVCLStateToGTKState(nState); + + switch(nType) + { + case ControlType::Spinbox: + case ControlType::SpinButtons: + context = mpSpinStyle; + renderType = RenderType::Spinbutton; + break; + case ControlType::Editbox: + context = mpEntryStyle; + break; + case ControlType::MultilineEditbox: + context = mpTextViewStyle; + break; + case ControlType::Combobox: + context = mpComboboxStyle; + renderType = RenderType::Combobox; + break; + case ControlType::Listbox: + if (nPart == ControlPart::Focus) + { + renderType = RenderType::Focus; + context = mpListboxButtonStyle; + } + else + { + renderType = RenderType::Combobox; + context = mpListboxStyle; + } + break; + case ControlType::MenuPopup: + bInMenu = true; + + // map selected menu entries in vcl parlance to gtk prelight + if (nPart >= ControlPart::MenuItem && nPart <= ControlPart::SubmenuArrow && (nState & ControlState::SELECTED)) + flags = static_cast(flags | GTK_STATE_FLAG_PRELIGHT); + flags = static_cast(flags & ~GTK_STATE_FLAG_ACTIVE); + switch(nPart) + { + case ControlPart::MenuItem: + context = mpMenuItemStyle; + renderType = RenderType::BackgroundAndFrame; + break; + case ControlPart::MenuItemCheckMark: + context = mpCheckMenuItemCheckStyle; + renderType = RenderType::Check; + nType = ControlType::Checkbox; + if (nState & ControlState::PRESSED) + { + flags = static_cast(flags | GTK_STATE_FLAG_CHECKED); + } + break; + case ControlPart::MenuItemRadioMark: + context = mpRadioMenuItemRadioStyle; + renderType = RenderType::Radio; + nType = ControlType::Radiobutton; + if (nState & ControlState::PRESSED) + { + flags = static_cast(flags | GTK_STATE_FLAG_CHECKED); + } + break; + case ControlPart::Separator: + context = mpSeparatorMenuItemSeparatorStyle; + flags = GtkStateFlags(GTK_STATE_FLAG_BACKDROP | GTK_STATE_FLAG_INSENSITIVE); //GTK_STATE_FLAG_BACKDROP hack ? + renderType = RenderType::MenuSeparator; + break; + case ControlPart::SubmenuArrow: + context = mpMenuItemArrowStyle; + renderType = RenderType::Arrow; + break; + case ControlPart::Entire: + context = mpMenuStyle; + renderType = RenderType::Background; + break; + default: break; + } + break; + case ControlType::Toolbar: + switch(nPart) + { + case ControlPart::DrawBackgroundHorz: + case ControlPart::DrawBackgroundVert: + context = mpToolbarStyle; + break; + case ControlPart::Button: + /* For all checkbuttons in the toolbars */ + flags = static_cast(flags | + ( (rValue.getTristateVal() == ButtonValue::On) ? GTK_STATE_FLAG_CHECKED : GTK_STATE_FLAG_NORMAL)); + context = mpToolButtonStyle; + break; + case ControlPart::SeparatorVert: + context = mpToolbarSeparatorStyle; + renderType = RenderType::ToolbarSeparator; + break; + default: + return false; + } + break; + case ControlType::Radiobutton: + flags = static_cast(flags | + ( (rValue.getTristateVal() == ButtonValue::On) ? GTK_STATE_FLAG_CHECKED : GTK_STATE_FLAG_NORMAL)); + context = mpRadioButtonRadioStyle; + renderType = nPart == ControlPart::Focus ? RenderType::Focus : RenderType::Radio; + break; + case ControlType::Checkbox: + flags = static_cast(flags | + ( (rValue.getTristateVal() == ButtonValue::On) ? GTK_STATE_FLAG_CHECKED : + (rValue.getTristateVal() == ButtonValue::Mixed) ? GTK_STATE_FLAG_INCONSISTENT : + GTK_STATE_FLAG_NORMAL)); + context = mpCheckButtonCheckStyle; + renderType = nPart == ControlPart::Focus ? RenderType::Focus : RenderType::Check; + break; + case ControlType::Pushbutton: + context = mpButtonStyle; + break; + case ControlType::Scrollbar: + switch(nPart) + { + case ControlPart::DrawBackgroundVert: + case ControlPart::DrawBackgroundHorz: + context = (nPart == ControlPart::DrawBackgroundVert) + ? mpVScrollbarStyle : mpHScrollbarStyle; + renderType = RenderType::Scrollbar; + break; + default: break; + } + break; + case ControlType::ListNet: + return true; + case ControlType::TabPane: + context = mpNotebookStyle; + break; + case ControlType::TabBody: + context = mpNotebookStackStyle; + break; + case ControlType::TabHeader: + context = mpNotebookHeaderStyle; + break; + case ControlType::TabItem: + context = mpNotebookHeaderTabsTabStyle; + if (nState & ControlState::SELECTED) + flags = static_cast(flags | GTK_STATE_FLAG_CHECKED); + renderType = RenderType::TabItem; + break; + case ControlType::WindowBackground: + context = gtk_widget_get_style_context(widget_get_toplevel(mpWindow)); + break; + case ControlType::Frame: + { + DrawFrameStyle nStyle = static_cast(rValue.getNumericVal() & 0x0f); + if (nStyle == DrawFrameStyle::In) + context = mpFrameOutStyle; + else + context = mpFrameInStyle; + break; + } + case ControlType::Menubar: + if (nPart == ControlPart::MenuItem) + { + context = mpMenuBarItemStyle; + + flags = (!(nState & ControlState::ENABLED)) ? GTK_STATE_FLAG_INSENSITIVE : GTK_STATE_FLAG_NORMAL; + if (nState & ControlState::SELECTED) + flags = static_cast(flags | GTK_STATE_FLAG_PRELIGHT); + } + else + { + context = mpMenuBarStyle; + } + break; + case ControlType::Fixedline: + context = nPart == ControlPart::SeparatorHorz ? mpFixedHoriLineStyle : mpFixedVertLineStyle; + renderType = RenderType::Separator; + break; + case ControlType::ListNode: + { + context = mpTreeHeaderButtonStyle; + ButtonValue aButtonValue = rValue.getTristateVal(); + if (aButtonValue == ButtonValue::On) + flags = static_cast(flags | GTK_STATE_FLAG_CHECKED); + renderType = RenderType::Expander; + break; + } + case ControlType::ListHeader: + context = mpTreeHeaderButtonStyle; + if (nPart == ControlPart::Arrow) + { + const char* icon = (rValue.getNumericVal() & 1) ? "pan-down-symbolic" : "pan-up-symbolic"; + GtkIconTheme *pIconTheme = gtk_icon_theme_get_for_screen(gtk_widget_get_screen(mpWindow)); + pixbuf = gtk_icon_theme_load_icon_for_scale(pIconTheme, icon, + std::max(rControlRegion.GetWidth(), rControlRegion.GetHeight()), + gtk_style_context_get_scale (context), + static_cast(0), nullptr); + flags = GTK_STATE_FLAG_SELECTED; + renderType = RenderType::Icon; + } + break; + case ControlType::Progress: + context = mpProgressBarProgressStyle; + renderType = RenderType::Progress; + break; + default: + return false; + } + + cairo_t *cr = getCairoContext(); + clipRegion(cr); + cairo_translate(cr, rControlRegion.Left(), rControlRegion.Top()); + + tools::Long nX = 0; + tools::Long nY = 0; + tools::Long nWidth = rControlRegion.GetWidth(); + tools::Long nHeight = rControlRegion.GetHeight(); + + StyleContextSave aContextState; + aContextState.save(context); + style_context_set_state(context, flags); + + // apply background in style, if explicitly set + // note: for more complex controls that use multiple styles for their elements, + // background may have to be applied for more of those as well (s. case RenderType::Combobox below) + GtkCssProvider* pBgCssProvider = nullptr; + if (rBackgroundColor != COL_AUTO) + { + const OUString sColorCss = "* { background-color: #" + rBackgroundColor.AsRGBHexString() + "; }"; + const OString aResult = OUStringToOString(sColorCss, RTL_TEXTENCODING_UTF8); + pBgCssProvider = gtk_css_provider_new(); + css_provider_load_from_data(pBgCssProvider, aResult.getStr(), aResult.getLength()); + gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(pBgCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + + switch(renderType) + { + case RenderType::Background: + case RenderType::BackgroundAndFrame: + gtk_render_background(context, cr, nX, nY, nWidth, nHeight); + if (renderType == RenderType::BackgroundAndFrame) + { + gtk_render_frame(context, cr, nX, nY, nWidth, nHeight); + } + break; + case RenderType::Check: + { + PaintCheck(cr, context, rControlRegion, bInMenu); + break; + } + case RenderType::Radio: + { + PaintRadio(cr, context, rControlRegion, bInMenu); + break; + } + case RenderType::MenuSeparator: + gtk_render_line(context, cr, + 0, rControlRegion.GetHeight() / 2, + rControlRegion.GetWidth() - 1, rControlRegion.GetHeight() / 2); + break; + case RenderType::ToolbarSeparator: + { + draw_vertical_separator(context, cr, rControlRegion, mnVerticalSeparatorMinWidth); + break; + } + case RenderType::Separator: + if (nPart == ControlPart::SeparatorHorz) + draw_horizontal_separator(context, cr, rControlRegion); + else + draw_vertical_separator(context, cr, rControlRegion, mnVerticalSeparatorMinWidth); + break; + case RenderType::Arrow: + gtk_render_arrow(context, cr, + G_PI / 2, 0, 0, + MIN(rControlRegion.GetWidth(), 1 + rControlRegion.GetHeight())); + break; + case RenderType::Expander: + gtk_render_expander(context, cr, -2, -2, nWidth+4, nHeight+4); + break; + case RenderType::Scrollbar: + PaintScrollbar(context, cr, rControlRegion, nPart, rValue); + break; + case RenderType::Spinbutton: + PaintSpinButton(flags, cr, rControlRegion, nPart, rValue); + break; + case RenderType::Combobox: + if (pBgCssProvider) + { + if (nType == ControlType::Combobox) + { + gtk_style_context_add_provider(mpComboboxEntryStyle, GTK_STYLE_PROVIDER(pBgCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + else if (nType == ControlType::Listbox) + { + gtk_style_context_add_provider(mpListboxBoxStyle, GTK_STYLE_PROVIDER(pBgCssProvider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + } + PaintCombobox(flags, cr, rControlRegion, nType, nPart); + if (pBgCssProvider) + { + if (nType == ControlType::Combobox) + gtk_style_context_remove_provider(mpComboboxEntryStyle, GTK_STYLE_PROVIDER(pBgCssProvider)); + else if (nType == ControlType::Listbox) + gtk_style_context_remove_provider(mpListboxBoxStyle, GTK_STYLE_PROVIDER(pBgCssProvider)); + } + break; + case RenderType::Icon: + gtk_style_context_save (context); + gtk_style_context_set_scale (context, 1); + gtk_render_icon(context, cr, pixbuf, nX, nY); + gtk_style_context_restore (context); + g_object_unref(pixbuf); + break; + case RenderType::Focus: + { + if (nType == ControlType::Checkbox || + nType == ControlType::Radiobutton) + { + nX -= 2; nY -=2; + nHeight += 4; nWidth += 4; + } + else + { + GtkBorder border; + + style_context_get_border(context, &border); + + nX += border.left; + nY += border.top; + nWidth -= border.left + border.right; + nHeight -= border.top + border.bottom; + } + + gtk_render_focus(context, cr, nX, nY, nWidth, nHeight); + + break; + } + case RenderType::Progress: + { + gtk_render_background(mpProgressBarTroughStyle, cr, nX, nY, nWidth, nHeight); + + tools::Long nProgressWidth = rValue.getNumericVal(); + if (nProgressWidth) + { + GtkBorder padding; + style_context_get_padding(context, &padding); + + nX += padding.left; + nY += padding.top; + nHeight -= (padding.top + padding.bottom); + nProgressWidth -= (padding.left + padding.right); + gtk_render_background(context, cr, nX, nY, nProgressWidth, nHeight); + gtk_render_frame(context, cr, nX, nY, nProgressWidth, nHeight); + } + + gtk_render_frame(mpProgressBarTroughStyle, cr, nX, nY, nWidth, nHeight); + + break; + } + case RenderType::TabItem: + { + gint initial_gap(0); + gtk_style_context_get_style(mpNotebookStyle, + "initial-gap", &initial_gap, + nullptr); + + nX += initial_gap/2; + nWidth -= initial_gap; + tools::Rectangle aRect(Point(nX, nY), Size(nWidth, nHeight)); + render_common(mpNotebookHeaderTabsTabStyle, cr, aRect, flags); + break; + } + default: + break; + } + + if (pBgCssProvider) + { + gtk_style_context_remove_provider(context, GTK_STYLE_PROVIDER(pBgCssProvider)); + } + aContextState.restore(); + + cairo_destroy(cr); // unref + + if (!rControlRegion.IsEmpty()) + mpFrame->damaged(rControlRegion.Left(), rControlRegion.Top(), rControlRegion.GetWidth(), rControlRegion.GetHeight()); + + return true; +} + +static tools::Rectangle GetWidgetSize(const tools::Rectangle& rControlRegion, GtkWidget* widget) +{ + GtkRequisition aReq; + gtk_widget_get_preferred_size(widget, nullptr, &aReq); + tools::Long nHeight = std::max(rControlRegion.GetHeight(), aReq.height); + return tools::Rectangle(rControlRegion.TopLeft(), Size(rControlRegion.GetWidth(), nHeight)); +} + +static tools::Rectangle AdjustRectForTextBordersPadding(GtkStyleContext* pStyle, tools::Long nContentWidth, tools::Long nContentHeight, const tools::Rectangle& rControlRegion) +{ + GtkBorder border; + style_context_get_border(pStyle, &border); + + GtkBorder padding; + style_context_get_padding(pStyle, &padding); + + gint nWidgetHeight = nContentHeight + padding.top + padding.bottom + border.top + border.bottom; + nWidgetHeight = std::max(std::max(nWidgetHeight, rControlRegion.GetHeight()), 34); + + gint nWidgetWidth = nContentWidth + padding.left + padding.right + border.left + border.right; + nWidgetWidth = std::max(nWidgetWidth, rControlRegion.GetWidth()); + + tools::Rectangle aEditRect(rControlRegion.TopLeft(), Size(nWidgetWidth, nWidgetHeight)); + + return aEditRect; +} + +bool GtkSalGraphics::getNativeControlRegion( ControlType nType, ControlPart nPart, const tools::Rectangle& rControlRegion, ControlState, + const ImplControlValue& rValue, const OUString&, + tools::Rectangle &rNativeBoundingRegion, tools::Rectangle &rNativeContentRegion ) +{ + /* TODO: all this functions needs improvements */ + tools::Rectangle aEditRect = rControlRegion; + gint indicator_size, indicator_spacing; + + if(((nType == ControlType::Checkbox) || (nType == ControlType::Radiobutton)) && + nPart == ControlPart::Entire) + { + rNativeBoundingRegion = rControlRegion; + + GtkStyleContext *pButtonStyle = (nType == ControlType::Checkbox) ? mpCheckButtonCheckStyle : mpRadioButtonRadioStyle; + + + gtk_style_context_get_style( pButtonStyle, + "indicator-size", &indicator_size, + "indicator-spacing", &indicator_spacing, + nullptr ); + + GtkBorder border; + style_context_get_border(pButtonStyle, &border); + + GtkBorder padding; + style_context_get_padding(pButtonStyle, &padding); + + + indicator_size += 2*indicator_spacing + border.left + padding.left + border.right + padding.right; + tools::Rectangle aIndicatorRect( Point( 0, + (rControlRegion.GetHeight()-indicator_size)/2), + Size( indicator_size, indicator_size ) ); + rNativeContentRegion = aIndicatorRect; + + return true; + } + else if( nType == ControlType::MenuPopup) + { + if ((nPart == ControlPart::MenuItemCheckMark) || + (nPart == ControlPart::MenuItemRadioMark) ) + { + indicator_size = 0; + + GtkStyleContext *pMenuItemStyle = (nPart == ControlPart::MenuItemCheckMark ) ? mpCheckMenuItemCheckStyle + : mpRadioMenuItemRadioStyle; + + gtk_style_context_get_style( pMenuItemStyle, + "indicator-size", &indicator_size, + nullptr ); + + gint point = MAX(0, rControlRegion.GetHeight() - indicator_size); + aEditRect = tools::Rectangle( Point( 0, point / 2), + Size( indicator_size, indicator_size ) ); + } + else if (nPart == ControlPart::Separator) + { + gint separator_height, separator_width, wide_separators; + + gtk_style_context_get_style (mpSeparatorMenuItemSeparatorStyle, + "wide-separators", &wide_separators, + "separator-width", &separator_width, + "separator-height", &separator_height, + nullptr); + + aEditRect = tools::Rectangle( aEditRect.TopLeft(), + Size( aEditRect.GetWidth(), wide_separators ? separator_height : 1 ) ); + } + else if (nPart == ControlPart::SubmenuArrow) + { + gfloat arrow_size = getArrowSize(mpMenuItemArrowStyle); + aEditRect = tools::Rectangle( aEditRect.TopLeft(), + Size( arrow_size, arrow_size ) ); + } + } + else if ( (nType==ControlType::Scrollbar) && + ((nPart==ControlPart::ButtonLeft) || (nPart==ControlPart::ButtonRight) || + (nPart==ControlPart::ButtonUp) || (nPart==ControlPart::ButtonDown) ) ) + { + rNativeBoundingRegion = NWGetScrollButtonRect( nPart, rControlRegion ); + rNativeContentRegion = rNativeBoundingRegion; + + if (!rNativeContentRegion.GetWidth()) + rNativeContentRegion.SetRight( rNativeContentRegion.Left() + 1 ); + if (!rNativeContentRegion.GetHeight()) + rNativeContentRegion.SetBottom( rNativeContentRegion.Top() + 1 ); + + return true; + } + else if ( (nType==ControlType::Spinbox) && + ((nPart==ControlPart::ButtonUp) || (nPart==ControlPart::ButtonDown) || + (nPart==ControlPart::SubEdit)) ) + { + tools::Rectangle aControlRegion(GetWidgetSize(rControlRegion, gSpinBox)); + aEditRect = NWGetSpinButtonRect(nPart, aControlRegion); + } + else if ( (nType==ControlType::Combobox) && + ((nPart==ControlPart::ButtonDown) || (nPart==ControlPart::SubEdit)) ) + { + aEditRect = NWGetComboBoxButtonRect(nType, nPart, rControlRegion); + } + else if ( (nType==ControlType::Listbox) && + ((nPart==ControlPart::ButtonDown) || (nPart==ControlPart::SubEdit)) ) + { + aEditRect = NWGetComboBoxButtonRect(nType, nPart, rControlRegion); + } + else if (nType == ControlType::Editbox && nPart == ControlPart::Entire) + { + aEditRect = GetWidgetSize(rControlRegion, gEntryBox); + } + else if (nType == ControlType::Listbox && nPart == ControlPart::Entire) + { + aEditRect = GetWidgetSize(rControlRegion, gListBox); + } + else if (nType == ControlType::Combobox && nPart == ControlPart::Entire) + { + aEditRect = GetWidgetSize(rControlRegion, gComboBox); + } + else if (nType == ControlType::Spinbox && nPart == ControlPart::Entire) + { + aEditRect = GetWidgetSize(rControlRegion, gSpinBox); + } + else if (nType == ControlType::TabItem && nPart == ControlPart::Entire) + { + const TabitemValue& rTabitemValue = static_cast(rValue); + const tools::Rectangle& rTabitemRect = rTabitemValue.getContentRect(); + + aEditRect = AdjustRectForTextBordersPadding(mpNotebookHeaderTabsTabStyle, rTabitemRect.GetWidth(), + rTabitemRect.GetHeight(), rControlRegion); + } + else if (nType == ControlType::Frame && nPart == ControlPart::Border) + { + aEditRect = rControlRegion; + + GtkBorder padding; + style_context_get_padding(mpFrameInStyle, &padding); + + GtkBorder border; + style_context_get_border(mpFrameInStyle, &border); + + int x1 = aEditRect.Left(); + int y1 = aEditRect.Top(); + int x2 = aEditRect.Right(); + int y2 = aEditRect.Bottom(); + + rNativeBoundingRegion = aEditRect; + rNativeContentRegion = tools::Rectangle(x1 + (padding.left + border.left), + y1 + (padding.top + border.top), + x2 - (padding.right + border.right), + y2 - (padding.bottom + border.bottom)); + + return true; + } + else + { + return false; + } + + rNativeBoundingRegion = aEditRect; + rNativeContentRegion = rNativeBoundingRegion; + + return true; +} +#endif + +/************************************************************************ + * helper for GtkSalFrame + ************************************************************************/ +static ::Color getColor( const GdkRGBA& rCol ) +{ + return ::Color( static_cast(rCol.red * 0xFFFF) >> 8, static_cast(rCol.green * 0xFFFF) >> 8, static_cast(rCol.blue * 0xFFFF) >> 8 ); +} + +static ::Color style_context_get_background_color(GtkStyleContext* pStyle) +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkRGBA background_color; + gtk_style_context_get_background_color(pStyle, gtk_style_context_get_state(pStyle), &background_color); + return getColor(background_color); +#else + cairo_surface_t *target = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1); + cairo_t* cr = cairo_create(target); + gtk_render_background(pStyle, cr, 0, 0, 1, 1); + cairo_destroy(cr); + + cairo_surface_flush(target); + vcl::bitmap::lookup_table const & unpremultiply_table = vcl::bitmap::get_unpremultiply_table(); + unsigned char *data = cairo_image_surface_get_data(target); + sal_uInt8 a = data[SVP_CAIRO_ALPHA]; + sal_uInt8 b = unpremultiply_table[a][data[SVP_CAIRO_BLUE]]; + sal_uInt8 g = unpremultiply_table[a][data[SVP_CAIRO_GREEN]]; + sal_uInt8 r = unpremultiply_table[a][data[SVP_CAIRO_RED]]; + Color aColor(r, g, b); + cairo_surface_destroy(target); + + return aColor; +#endif +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +static vcl::Font getFont(GtkStyleContext* pStyle, const css::lang::Locale& rLocale) +{ + const PangoFontDescription* font = gtk_style_context_get_font(pStyle, gtk_style_context_get_state(pStyle)); + return pango_to_vcl(font, rLocale); +} +#endif + +vcl::Font pango_to_vcl(const PangoFontDescription* font, const css::lang::Locale& rLocale) +{ + OString aFamily = pango_font_description_get_family( font ); + PangoStyle eStyle = pango_font_description_get_style( font ); + PangoWeight eWeight = pango_font_description_get_weight( font ); + PangoStretch eStretch = pango_font_description_get_stretch( font ); + + FontAttributes aDFA; + + // set family name + aDFA.SetFamilyName(OStringToOUString(aFamily, RTL_TEXTENCODING_UTF8)); + + // set italic + switch( eStyle ) + { + case PANGO_STYLE_NORMAL: aDFA.SetItalic(ITALIC_NONE);break; + case PANGO_STYLE_ITALIC: aDFA.SetItalic(ITALIC_NORMAL);break; + case PANGO_STYLE_OBLIQUE: aDFA.SetItalic(ITALIC_OBLIQUE);break; + } + + // set weight + if( eWeight <= PANGO_WEIGHT_ULTRALIGHT ) + aDFA.SetWeight(WEIGHT_ULTRALIGHT); + else if( eWeight <= PANGO_WEIGHT_LIGHT ) + aDFA.SetWeight(WEIGHT_LIGHT); + else if( eWeight <= PANGO_WEIGHT_NORMAL ) + aDFA.SetWeight(WEIGHT_NORMAL); + else if( eWeight <= PANGO_WEIGHT_BOLD ) + aDFA.SetWeight(WEIGHT_BOLD); + else + aDFA.SetWeight(WEIGHT_ULTRABOLD); + + // set width + switch( eStretch ) + { + case PANGO_STRETCH_ULTRA_CONDENSED: aDFA.SetWidthType(WIDTH_ULTRA_CONDENSED);break; + case PANGO_STRETCH_EXTRA_CONDENSED: aDFA.SetWidthType(WIDTH_EXTRA_CONDENSED);break; + case PANGO_STRETCH_CONDENSED: aDFA.SetWidthType(WIDTH_CONDENSED);break; + case PANGO_STRETCH_SEMI_CONDENSED: aDFA.SetWidthType(WIDTH_SEMI_CONDENSED);break; + case PANGO_STRETCH_NORMAL: aDFA.SetWidthType(WIDTH_NORMAL);break; + case PANGO_STRETCH_SEMI_EXPANDED: aDFA.SetWidthType(WIDTH_SEMI_EXPANDED);break; + case PANGO_STRETCH_EXPANDED: aDFA.SetWidthType(WIDTH_EXPANDED);break; + case PANGO_STRETCH_EXTRA_EXPANDED: aDFA.SetWidthType(WIDTH_EXTRA_EXPANDED);break; + case PANGO_STRETCH_ULTRA_EXPANDED: aDFA.SetWidthType(WIDTH_ULTRA_EXPANDED);break; + } + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.gtk3", "font name BEFORE system match: \"" + << aFamily << "\"."); +#endif + + // match font to e.g. resolve "Sans" + bool bFound = psp::PrintFontManager::get().matchFont(aDFA, rLocale); + +#if OSL_DEBUG_LEVEL > 1 + SAL_INFO("vcl.gtk3", "font match " + << (bFound ? "succeeded" : "failed") + << ", name AFTER: \"" + << aDFA.GetFamilyName() + << "\"."); +#else + (void) bFound; +#endif + + int nPangoHeight = pango_font_description_get_size(font) / PANGO_SCALE; + + if (pango_font_description_get_size_is_absolute(font)) + { + const sal_Int32 nDPIY = 96; + nPangoHeight = nPangoHeight * 72; + nPangoHeight = nPangoHeight + nDPIY / 2; + nPangoHeight = nPangoHeight / nDPIY; + } + + vcl::Font aFont(aDFA.GetFamilyName(), Size(0, nPangoHeight)); + if (aDFA.GetWeight() != WEIGHT_DONTKNOW) + aFont.SetWeight(aDFA.GetWeight()); + if (aDFA.GetWidthType() != WIDTH_DONTKNOW) + aFont.SetWidthType(aDFA.GetWidthType()); + if (aDFA.GetItalic() != ITALIC_DONTKNOW) + aFont.SetItalic(aDFA.GetItalic()); + if (aDFA.GetPitch() != PITCH_DONTKNOW) + aFont.SetPitch(aDFA.GetPitch()); + return aFont; +} + +bool GtkSalGraphics::updateSettings(AllSettings& rSettings) +{ + GtkWidget* pTopLevel = widget_get_toplevel(mpWindow); + GtkStyleContext* pStyle = gtk_widget_get_style_context(pTopLevel); + StyleContextSave aContextState; + aContextState.save(pStyle); + GtkSettings* pSettings = gtk_widget_get_settings(pTopLevel); + StyleSettings aStyleSet = rSettings.GetStyleSettings(); + + // text colors + GdkRGBA text_color; + style_context_set_state(pStyle, GTK_STATE_FLAG_NORMAL); + style_context_get_color(pStyle, &text_color); + ::Color aTextColor = getColor( text_color ); + aStyleSet.SetDialogTextColor( aTextColor ); + aStyleSet.SetButtonTextColor( aTextColor ); + aStyleSet.SetDefaultActionButtonTextColor(aTextColor); + aStyleSet.SetActionButtonTextColor(aTextColor); + aStyleSet.SetListBoxWindowTextColor( aTextColor ); + aStyleSet.SetRadioCheckTextColor( aTextColor ); + aStyleSet.SetGroupTextColor( aTextColor ); + aStyleSet.SetLabelTextColor( aTextColor ); + aStyleSet.SetWindowTextColor( aTextColor ); + aStyleSet.SetFieldTextColor( aTextColor ); + + // background colors + ::Color aBackColor = style_context_get_background_color(pStyle); + aStyleSet.BatchSetBackgrounds( aBackColor ); + + // UI font +#if GTK_CHECK_VERSION(4, 0, 0) + gchar* pFontname = nullptr; + g_object_get(pSettings, "gtk-font-name", &pFontname, nullptr); + PangoFontDescription* pFontDesc = pango_font_description_from_string(pFontname); + vcl::Font aFont(pango_to_vcl(pFontDesc, rSettings.GetUILanguageTag().getLocale())); + pango_font_description_free(pFontDesc); +#else + vcl::Font aFont(getFont(pStyle, rSettings.GetUILanguageTag().getLocale())); +#endif + + aStyleSet.BatchSetFonts( aFont, aFont); + + aFont.SetWeight( WEIGHT_BOLD ); + aStyleSet.SetTitleFont( aFont ); + aStyleSet.SetFloatTitleFont( aFont ); + + // mouse over text colors + style_context_set_state(pStyle, GTK_STATE_FLAG_PRELIGHT); + style_context_get_color(pStyle, &text_color); + aTextColor = getColor(text_color); + aStyleSet.SetDefaultButtonTextColor(aTextColor); + aStyleSet.SetDefaultButtonRolloverTextColor(aTextColor); + aStyleSet.SetDefaultButtonPressedRolloverTextColor(aTextColor); + aStyleSet.SetButtonRolloverTextColor(aTextColor); + aStyleSet.SetDefaultActionButtonRolloverTextColor(aTextColor); + aStyleSet.SetDefaultActionButtonPressedRolloverTextColor(aTextColor); + aStyleSet.SetActionButtonRolloverTextColor(aTextColor); + aStyleSet.SetActionButtonPressedRolloverTextColor(aTextColor); + aStyleSet.SetFlatButtonTextColor(aTextColor); + aStyleSet.SetFlatButtonPressedRolloverTextColor(aTextColor); + aStyleSet.SetFlatButtonRolloverTextColor(aTextColor); + aStyleSet.SetFieldRolloverTextColor(aTextColor); + + aContextState.restore(); + + // button mouse over colors + { + GdkRGBA normal_button_rollover_text_color, pressed_button_rollover_text_color; + aContextState.save(mpButtonStyle); + style_context_set_state(mpButtonStyle, GTK_STATE_FLAG_PRELIGHT); + style_context_get_color(mpButtonStyle, &normal_button_rollover_text_color); + aTextColor = getColor(normal_button_rollover_text_color); + aStyleSet.SetButtonRolloverTextColor( aTextColor ); + style_context_set_state(mpButtonStyle, static_cast(GTK_STATE_FLAG_PRELIGHT | GTK_STATE_FLAG_ACTIVE)); + style_context_get_color(mpButtonStyle, &pressed_button_rollover_text_color); + aTextColor = getColor(pressed_button_rollover_text_color); + style_context_set_state(mpButtonStyle, GTK_STATE_FLAG_NORMAL); + aStyleSet.SetButtonPressedRolloverTextColor( aTextColor ); + aContextState.restore(); + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + // tooltip colors + { + GtkWidgetPath *pCPath = gtk_widget_path_new(); + guint pos = gtk_widget_path_append_type(pCPath, GTK_TYPE_WINDOW); + gtk_widget_path_iter_add_class(pCPath, pos, GTK_STYLE_CLASS_TOOLTIP); + pos = gtk_widget_path_append_type (pCPath, GTK_TYPE_LABEL); + gtk_widget_path_iter_add_class(pCPath, pos, GTK_STYLE_CLASS_LABEL); + GtkStyleContext *pCStyle = makeContext (pCPath, nullptr); + aContextState.save(pCStyle); + + GdkRGBA tooltip_fg_color; + style_context_set_state(pCStyle, GTK_STATE_FLAG_NORMAL); + style_context_get_color(pCStyle, &tooltip_fg_color); + ::Color aTooltipBgColor = style_context_get_background_color(pCStyle); + + aContextState.restore(); + g_object_unref( pCStyle ); + + aStyleSet.SetHelpColor(aTooltipBgColor); + aStyleSet.SetHelpTextColor( getColor( tooltip_fg_color )); + } +#endif + + GdkRGBA color; + { +#if !GTK_CHECK_VERSION(4, 0, 0) + // construct style context for text view + GtkWidgetPath *pCPath = gtk_widget_path_new(); + gtk_widget_path_append_type( pCPath, GTK_TYPE_TEXT_VIEW ); + gtk_widget_path_iter_add_class( pCPath, -1, GTK_STYLE_CLASS_VIEW ); + GtkStyleContext *pCStyle = makeContext( pCPath, nullptr ); +#else + GtkStyleContext *pCStyle = gtk_widget_get_style_context(gTextView); +#endif + aContextState.save(pCStyle); + + // highlighting colors + style_context_set_state(pCStyle, GTK_STATE_FLAG_SELECTED); + ::Color aHighlightColor = style_context_get_background_color(pCStyle); + style_context_get_color(pCStyle, &text_color); + ::Color aHighlightTextColor = getColor( text_color ); + aStyleSet.SetAccentColor( aHighlightColor ); // https://debugpointnews.com/gnome-native-accent-colour-announcement/ + aStyleSet.SetHighlightColor( aHighlightColor ); + aStyleSet.SetHighlightTextColor( aHighlightTextColor ); + aStyleSet.SetListBoxWindowHighlightColor( aHighlightColor ); + aStyleSet.SetListBoxWindowHighlightTextColor( aHighlightTextColor ); + // make active like highlight, except with a small contrast. Note, see + // a GtkListBoxRow in a GtkStackSidebar for a gtk widget with a + // difference between highlighted and highlighted with focus. + aHighlightColor.IncreaseLuminance(16); + aStyleSet.SetActiveColor( aHighlightColor ); + aStyleSet.SetActiveTextColor( aHighlightTextColor ); + + // warning color + GdkRGBA warning_color; + if (gtk_style_context_lookup_color(pCStyle, "warning_color", &warning_color)) + aStyleSet.SetWarningColor(getColor(warning_color)); + + // field background color + style_context_set_state(pCStyle, GTK_STATE_FLAG_NORMAL); + ::Color aBackFieldColor = style_context_get_background_color(pCStyle); + aStyleSet.SetFieldColor( aBackFieldColor ); + // This baby is the default page/paper color + aStyleSet.SetWindowColor( aBackFieldColor ); + // listbox background color + aStyleSet.SetListBoxWindowBackgroundColor( aBackFieldColor ); + +#if GTK_CHECK_VERSION(4, 0, 0) + double caretAspectRatio = 0.04f; + g_object_get(pSettings, "gtk-cursor-aspect-ratio", &caretAspectRatio, nullptr); +#else + // Cursor width + gfloat caretAspectRatio = 0.04f; + gtk_style_context_get_style( pCStyle, "cursor-aspect-ratio", &caretAspectRatio, nullptr ); +#endif + // Assume 20px tall for the ratio computation, which should give reasonable results + aStyleSet.SetCursorSize(20 * caretAspectRatio + 1); + + // Dark shadow color + style_context_set_state(pCStyle, GTK_STATE_FLAG_INSENSITIVE); + style_context_get_color(pCStyle, &color); + ::Color aDarkShadowColor = getColor( color ); + aStyleSet.SetDarkShadowColor( aDarkShadowColor ); + + ::Color aShadowColor(aBackColor); + if (aDarkShadowColor.GetLuminance() > aBackColor.GetLuminance()) + aShadowColor.IncreaseLuminance(64); + else + aShadowColor.DecreaseLuminance(64); + aStyleSet.SetShadowColor(aShadowColor); + + aContextState.restore(); +#if !GTK_CHECK_VERSION(4, 0, 0) + g_object_unref( pCStyle ); +#endif + + // Tab colors + aStyleSet.SetActiveTabColor( aBackFieldColor ); // same as the window color. + aStyleSet.SetInactiveTabColor( aBackColor ); + } + + // menu disabled entries handling + aStyleSet.SetSkipDisabledInMenus( true ); + aStyleSet.SetPreferredContextMenuShortcuts( false ); + +#if !GTK_CHECK_VERSION(4, 0, 0) + aContextState.save(mpMenuItemLabelStyle); + + // menu colors + style_context_set_state(mpMenuStyle, GTK_STATE_FLAG_NORMAL); + aBackColor = style_context_get_background_color(mpMenuStyle); + aStyleSet.SetMenuColor( aBackColor ); + + // menu bar + style_context_set_state(mpMenuBarStyle, GTK_STATE_FLAG_NORMAL); + aBackColor = style_context_get_background_color(mpMenuBarStyle); + aStyleSet.SetMenuBarColor( aBackColor ); + aStyleSet.SetMenuBarRolloverColor( aBackColor ); + + style_context_set_state(mpMenuBarItemStyle, GTK_STATE_FLAG_NORMAL); + style_context_get_color(mpMenuBarItemStyle, &text_color); + aTextColor = aStyleSet.GetPersonaMenuBarTextColor().value_or( getColor( text_color ) ); + aStyleSet.SetMenuBarTextColor( aTextColor ); + aStyleSet.SetMenuBarRolloverTextColor( aTextColor ); + + style_context_set_state(mpMenuBarItemStyle, GTK_STATE_FLAG_PRELIGHT); + style_context_get_color(mpMenuBarItemStyle, &text_color); + aTextColor = aStyleSet.GetPersonaMenuBarTextColor().value_or( getColor( text_color ) ); + aStyleSet.SetMenuBarHighlightTextColor( aTextColor ); + + // menu items + style_context_set_state(mpMenuItemLabelStyle, GTK_STATE_FLAG_NORMAL); + style_context_get_color(mpMenuItemLabelStyle, &color); + aTextColor = getColor(color); + aStyleSet.SetMenuTextColor(aTextColor); + + style_context_set_state(mpMenuItemLabelStyle, GTK_STATE_FLAG_PRELIGHT); + ::Color aHighlightColor = style_context_get_background_color(mpMenuItemLabelStyle); + aStyleSet.SetMenuHighlightColor( aHighlightColor ); + + style_context_get_color(mpMenuItemLabelStyle, &color); + ::Color aHighlightTextColor = getColor( color ); + aStyleSet.SetMenuHighlightTextColor( aHighlightTextColor ); + + aContextState.restore(); +#endif + + // hyperlink colors + aContextState.save(mpLinkButtonStyle); + style_context_set_state(mpLinkButtonStyle, GTK_STATE_FLAG_LINK); + style_context_get_color(mpLinkButtonStyle, &text_color); + aStyleSet.SetLinkColor(getColor(text_color)); + style_context_set_state(mpLinkButtonStyle, GTK_STATE_FLAG_VISITED); + style_context_get_color(mpLinkButtonStyle, &text_color); + aStyleSet.SetVisitedLinkColor(getColor(text_color)); + aContextState.restore(); + +#if !GTK_CHECK_VERSION(4, 0, 0) + { + GtkStyleContext *pCStyle = mpNotebookHeaderTabsTabLabelStyle; + aContextState.save(pCStyle); + style_context_set_state(pCStyle, GTK_STATE_FLAG_NORMAL); + style_context_get_color(pCStyle, &text_color); + aTextColor = getColor( text_color ); + aStyleSet.SetTabTextColor(aTextColor); + aStyleSet.SetTabFont(getFont(mpNotebookHeaderTabsTabLabelStyle, rSettings.GetUILanguageTag().getLocale())); + aContextState.restore(); + } + + { + GtkStyleContext *pCStyle = mpToolButtonStyle; + aContextState.save(pCStyle); + style_context_set_state(pCStyle, GTK_STATE_FLAG_NORMAL); + style_context_get_color(pCStyle, &text_color); + aTextColor = getColor( text_color ); + aStyleSet.SetToolTextColor(aTextColor); + aStyleSet.SetToolFont(getFont(mpToolButtonStyle, rSettings.GetUILanguageTag().getLocale())); + aContextState.restore(); + } + + // mouse over text colors + { + GtkStyleContext *pCStyle = mpNotebookHeaderTabsTabHoverLabelStyle; + aContextState.save(pCStyle); + style_context_set_state(pCStyle, GTK_STATE_FLAG_PRELIGHT); + style_context_get_color(pCStyle, &text_color); + aTextColor = getColor( text_color ); + aStyleSet.SetTabRolloverTextColor(aTextColor); + aContextState.restore(); + } + + { + GtkStyleContext *pCStyle = mpNotebookHeaderTabsTabActiveLabelStyle; + aContextState.save(pCStyle); + style_context_set_state(pCStyle, GTK_STATE_FLAG_CHECKED); + style_context_get_color(pCStyle, &text_color); + aTextColor = getColor( text_color ); + aStyleSet.SetTabHighlightTextColor(aTextColor); + aContextState.restore(); + } +#endif + + // get cursor blink time + gboolean blink = false; + + g_object_get( pSettings, "gtk-cursor-blink", &blink, nullptr ); + if( blink ) + { + gint blink_time = static_cast(STYLE_CURSOR_NOBLINKTIME); + g_object_get( pSettings, "gtk-cursor-blink-time", &blink_time, nullptr ); + // set the blink_time if there is a setting and it is reasonable + // else leave the default value + if( blink_time > 100 ) + aStyleSet.SetCursorBlinkTime( blink_time/2 ); + } + else + aStyleSet.SetCursorBlinkTime( STYLE_CURSOR_NOBLINKTIME ); + + MouseSettings aMouseSettings = rSettings.GetMouseSettings(); + int iDoubleClickTime, iDoubleClickDistance, iDragThreshold; + static const int MENU_POPUP_DELAY = 225; + g_object_get( pSettings, + "gtk-double-click-time", &iDoubleClickTime, + "gtk-double-click-distance", &iDoubleClickDistance, + "gtk-dnd-drag-threshold", &iDragThreshold, + nullptr ); + aMouseSettings.SetDoubleClickTime( iDoubleClickTime ); + aMouseSettings.SetDoubleClickWidth( iDoubleClickDistance ); + aMouseSettings.SetDoubleClickHeight( iDoubleClickDistance ); + aMouseSettings.SetStartDragWidth( iDragThreshold ); + aMouseSettings.SetStartDragHeight( iDragThreshold ); + aMouseSettings.SetMenuDelay( MENU_POPUP_DELAY ); + rSettings.SetMouseSettings( aMouseSettings ); + + gboolean primarybuttonwarps = false; + g_object_get( pSettings, + "gtk-primary-button-warps-slider", &primarybuttonwarps, + nullptr ); + aStyleSet.SetPreferredUseImagesInMenus(false); + aStyleSet.SetPrimaryButtonWarpsSlider(primarybuttonwarps); + + // set scrollbar settings + gint min_slider_length = 21; + +#if GTK_CHECK_VERSION(4, 0, 0) + GtkRequisition natural_size; + gtk_widget_get_preferred_size(gHScrollbar, nullptr, &natural_size); + aStyleSet.SetScrollBarSize(natural_size.height); +#else + // Grab some button style attributes + Size aSize; + QuerySize(mpHScrollbarStyle, aSize); + QuerySize(mpHScrollbarContentsStyle, aSize); + QuerySize(mpHScrollbarTroughStyle, aSize); + QuerySize(mpHScrollbarSliderStyle, aSize); + + gboolean has_forward, has_forward2, has_backward, has_backward2; + gtk_style_context_get_style(mpHScrollbarStyle, + "has-forward-stepper", &has_forward, + "has-secondary-forward-stepper", &has_forward2, + "has-backward-stepper", &has_backward, + "has-secondary-backward-stepper", &has_backward2, nullptr); + if (has_forward || has_backward || has_forward2 || has_backward2) + QuerySize(mpHScrollbarButtonStyle, aSize); + + aStyleSet.SetScrollBarSize(aSize.Height()); + + gtk_style_context_get(mpVScrollbarSliderStyle, gtk_style_context_get_state(mpVScrollbarSliderStyle), + "min-height", &min_slider_length, + nullptr); +#endif + aStyleSet.SetMinThumbSize(min_slider_length); + + // preferred icon style + gchar* pIconThemeName = nullptr; + gboolean bDarkIconTheme = false; + g_object_get(pSettings, "gtk-icon-theme-name", &pIconThemeName, + "gtk-application-prefer-dark-theme", &bDarkIconTheme, + nullptr ); + OUString sIconThemeName(OUString::createFromAscii(pIconThemeName)); + aStyleSet.SetPreferredIconTheme(sIconThemeName, bDarkIconTheme); + g_free( pIconThemeName ); + + aStyleSet.SetToolbarIconSize( ToolbarIconSize::Large ); + + gchar* pThemeName = nullptr; + g_object_get( pSettings, "gtk-theme-name", &pThemeName, nullptr ); + SAL_INFO("vcl.gtk3", "Theme name is \"" + << pThemeName + << "\"."); + // High contrast + aStyleSet.SetHighContrastMode(g_strcmp0(pThemeName, "HighContrast") == 0); + g_free(pThemeName); + + // finally update the collected settings + rSettings.SetStyleSettings( aStyleSet ); + + return true; +} + +#if !GTK_CHECK_VERSION(4, 0, 0) +bool GtkSalGraphics::isNativeControlSupported( ControlType nType, ControlPart nPart ) +{ + switch(nType) + { + case ControlType::Pushbutton: + case ControlType::Radiobutton: + case ControlType::Checkbox: + case ControlType::Progress: + case ControlType::ListNode: + case ControlType::ListNet: + if (nPart==ControlPart::Entire || nPart == ControlPart::Focus) + return true; + break; + + case ControlType::Scrollbar: + if(nPart==ControlPart::DrawBackgroundHorz || nPart==ControlPart::DrawBackgroundVert || + nPart==ControlPart::Entire || nPart==ControlPart::HasThreeButtons) + return true; + break; + + case ControlType::Editbox: + case ControlType::MultilineEditbox: + if (nPart==ControlPart::Entire || nPart==ControlPart::HasBackgroundTexture) + return true; + break; + + case ControlType::Combobox: + if (nPart==ControlPart::Entire || nPart==ControlPart::HasBackgroundTexture || nPart == ControlPart::AllButtons) + return true; + break; + + case ControlType::Spinbox: + if (nPart==ControlPart::Entire || nPart==ControlPart::HasBackgroundTexture || nPart == ControlPart::AllButtons || nPart == ControlPart::ButtonUp || nPart == ControlPart::ButtonDown) + return true; + break; + + case ControlType::SpinButtons: + if (nPart==ControlPart::Entire || nPart==ControlPart::AllButtons) + return true; + break; + + case ControlType::Frame: + case ControlType::WindowBackground: + return true; + + case ControlType::TabItem: + case ControlType::TabHeader: + case ControlType::TabPane: + case ControlType::TabBody: + if(nPart==ControlPart::Entire || nPart==ControlPart::TabsDrawRtl) + return true; + break; + + case ControlType::Listbox: + if (nPart==ControlPart::Entire || nPart==ControlPart::ListboxWindow || nPart==ControlPart::HasBackgroundTexture || nPart == ControlPart::Focus) + return true; + break; + + case ControlType::Toolbar: + if( nPart==ControlPart::Entire +// || nPart==ControlPart::DrawBackgroundHorz +// || nPart==ControlPart::DrawBackgroundVert +// || nPart==ControlPart::ThumbHorz +// || nPart==ControlPart::ThumbVert + || nPart==ControlPart::Button +// || nPart==ControlPart::SeparatorHorz + || nPart==ControlPart::SeparatorVert + ) + return true; + break; + + case ControlType::Menubar: + if (nPart==ControlPart::Entire || nPart==ControlPart::MenuItem) + return true; + break; + + case ControlType::MenuPopup: + if (nPart==ControlPart::Entire + || nPart==ControlPart::MenuItem + || nPart==ControlPart::MenuItemCheckMark + || nPart==ControlPart::MenuItemRadioMark + || nPart==ControlPart::Separator + || nPart==ControlPart::SubmenuArrow + ) + return true; + break; + +// case ControlType::Slider: +// if(nPart == ControlPart::TrackHorzArea || nPart == ControlPart::TrackVertArea) +// return true; +// break; + + case ControlType::Fixedline: + if (nPart == ControlPart::SeparatorVert || nPart == ControlPart::SeparatorHorz) + return true; + break; + + case ControlType::ListHeader: + if (nPart == ControlPart::Button || nPart == ControlPart::Arrow) + return true; + break; + default: break; + } + + SAL_INFO("vcl.gtk", "Unhandled is native supported for Type:" << static_cast(nType) << ", Part" << static_cast(nPart)); + return false; +} +#endif + +#if ENABLE_CAIRO_CANVAS + +bool GtkSalGraphics::SupportsCairo() const +{ + return true; +} + +cairo::SurfaceSharedPtr GtkSalGraphics::CreateSurface(const cairo::CairoSurfaceSharedPtr& rSurface) const +{ + return std::make_shared(rSurface); +} + +cairo::SurfaceSharedPtr GtkSalGraphics::CreateSurface(const OutputDevice& /*rRefDevice*/, int x, int y, int width, int height) const +{ + return std::make_shared(this, x, y, width, height); +} + +#endif + +void GtkSalGraphics::WidgetQueueDraw() const +{ + //request gtk to sync the entire contents + mpFrame->queue_draw(); +} + +namespace { + +void getStyleContext(GtkStyleContext** style, GtkWidget* widget) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_fixed_put(GTK_FIXED(gDumbContainer), widget, 0, 0); +#else + gtk_container_add(GTK_CONTAINER(gDumbContainer), widget); +#endif + *style = gtk_widget_get_style_context(widget); + g_object_ref(*style); +} + +} + +void GtkSalData::initNWF() +{ + ImplSVData* pSVData = ImplGetSVData(); + pSVData->maNWFData.mbFlatMenu = true; + pSVData->maNWFData.mbDockingAreaAvoidTBFrames = true; + pSVData->maNWFData.mbCanDrawWidgetAnySize = true; + pSVData->maNWFData.mbDDListBoxNoTextArea = true; + pSVData->maNWFData.mbNoFocusRects = true; + pSVData->maNWFData.mbNoFocusRectsForFlatButtons = true; + pSVData->maNWFData.mbAutoAccel = true; +#if GTK_CHECK_VERSION(4, 0, 0) + pSVData->maNWFData.mbNoFrameJunctionForPopups = true; +#endif + +#if defined(GDK_WINDOWING_WAYLAND) + //gnome#768128 for the car crash that is wayland + //and floating dockable toolbars + GdkDisplay *pDisplay = gdk_display_get_default(); + if (DLSYM_GDK_IS_WAYLAND_DISPLAY(pDisplay)) + pSVData->maNWFData.mbCanDetermineWindowPosition = false; +#endif +} + +void GtkSalData::deInitNWF() +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + if (gCacheWindow) + gtk_widget_destroy(gCacheWindow); +#endif +} + +GtkSalGraphics::GtkSalGraphics( GtkSalFrame *pFrame, GtkWidget *pWindow ) + : mpFrame( pFrame ), + mpWindow( pWindow ) +{ + if (style_loaded) + return; + + style_loaded = true; + + /* Load the GtkStyleContexts, it might be a bit slow, but usually, + * gtk apps create a lot of widgets at startup, so, it shouldn't be + * too slow */ + +#if GTK_CHECK_VERSION(4, 0, 0) + gCacheWindow = gtk_window_new(); +#else + gCacheWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL); +#endif + gDumbContainer = gtk_fixed_new(); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_window_set_child(GTK_WINDOW(gCacheWindow), gDumbContainer); +#else + gtk_container_add(GTK_CONTAINER(gCacheWindow), gDumbContainer); +#endif + gtk_widget_realize(gDumbContainer); + gtk_widget_realize(gCacheWindow); + + gEntryBox = gtk_entry_new(); +#if GTK_CHECK_VERSION(4, 0, 0) + gtk_fixed_put(GTK_FIXED(gDumbContainer), gEntryBox, 0, 0); +#else + gtk_container_add(GTK_CONTAINER(gDumbContainer), gEntryBox); +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + mpWindowStyle = createStyleContext(GtkControlPart::ToplevelWindow); + mpEntryStyle = createStyleContext(GtkControlPart::Entry); +#else + mpWindowStyle = gtk_widget_get_style_context(gCacheWindow); + getStyleContext(&mpEntryStyle, gtk_entry_new()); +#endif + + getStyleContext(&mpTextViewStyle, gtk_text_view_new()); + +#if !GTK_CHECK_VERSION(4, 0, 0) + mpButtonStyle = createStyleContext(GtkControlPart::Button); + mpLinkButtonStyle = createStyleContext(GtkControlPart::LinkButton); +#else + getStyleContext(&mpButtonStyle, gtk_button_new()); + getStyleContext(&mpLinkButtonStyle, gtk_link_button_new("https://www.libreoffice.org")); +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + GtkWidget* pToolbar = gtk_toolbar_new(); + mpToolbarStyle = gtk_widget_get_style_context(pToolbar); + gtk_style_context_add_class(mpToolbarStyle, GTK_STYLE_CLASS_TOOLBAR); + + GtkToolItem *item = gtk_separator_tool_item_new(); + gtk_toolbar_insert(GTK_TOOLBAR(pToolbar), item, -1); + mpToolbarSeparatorStyle = gtk_widget_get_style_context(GTK_WIDGET(item)); + gtk_style_context_get(mpToolbarSeparatorStyle, + gtk_style_context_get_state(mpToolbarSeparatorStyle), + "min-width", &mnVerticalSeparatorMinWidth, nullptr); + + GtkWidget *pButton = gtk_button_new(); + item = gtk_tool_button_new(pButton, nullptr); + gtk_toolbar_insert(GTK_TOOLBAR(pToolbar), item, -1); + mpToolButtonStyle = gtk_widget_get_style_context(GTK_WIDGET(pButton)); +#endif + +#if GTK_CHECK_VERSION(4, 0, 0) + gVScrollbar = gtk_scrollbar_new(GTK_ORIENTATION_VERTICAL, nullptr); + gtk_fixed_put(GTK_FIXED(gDumbContainer), gVScrollbar, 0, 0); + gtk_widget_show(gVScrollbar); + mpVScrollbarStyle = gtk_widget_get_style_context(gVScrollbar); + + gHScrollbar = gtk_scrollbar_new(GTK_ORIENTATION_HORIZONTAL, nullptr); + gtk_fixed_put(GTK_FIXED(gDumbContainer), gHScrollbar, 0, 0); + gtk_widget_show(gHScrollbar); + mpHScrollbarStyle = gtk_widget_get_style_context(gHScrollbar); + + gTextView = gtk_text_view_new(); + gtk_fixed_put(GTK_FIXED(gDumbContainer), gTextView, 0, 0); + gtk_widget_show(gTextView); +#else + mpVScrollbarStyle = createStyleContext(GtkControlPart::ScrollbarVertical); + mpVScrollbarContentsStyle = createStyleContext(GtkControlPart::ScrollbarVerticalContents); + mpVScrollbarTroughStyle = createStyleContext(GtkControlPart::ScrollbarVerticalTrough); + mpVScrollbarSliderStyle = createStyleContext(GtkControlPart::ScrollbarVerticalSlider); + mpVScrollbarButtonStyle = createStyleContext(GtkControlPart::ScrollbarVerticalButton); + mpHScrollbarStyle = createStyleContext(GtkControlPart::ScrollbarHorizontal); + mpHScrollbarContentsStyle = createStyleContext(GtkControlPart::ScrollbarHorizontalContents); + mpHScrollbarTroughStyle = createStyleContext(GtkControlPart::ScrollbarHorizontalTrough); + mpHScrollbarSliderStyle = createStyleContext(GtkControlPart::ScrollbarHorizontalSlider); + mpHScrollbarButtonStyle = createStyleContext(GtkControlPart::ScrollbarHorizontalButton); +#endif + +#if !GTK_CHECK_VERSION(4, 0, 0) + mpCheckButtonStyle = createStyleContext(GtkControlPart::CheckButton); + mpCheckButtonCheckStyle = createStyleContext(GtkControlPart::CheckButtonCheck); + + mpRadioButtonStyle = createStyleContext(GtkControlPart::RadioButton); + mpRadioButtonRadioStyle = createStyleContext(GtkControlPart::RadioButtonRadio); + + /* Spinbutton */ + gSpinBox = gtk_spin_button_new(nullptr, 0, 0); + gtk_container_add(GTK_CONTAINER(gDumbContainer), gSpinBox); + mpSpinStyle = createStyleContext(GtkControlPart::SpinButton); + mpSpinUpStyle = createStyleContext(GtkControlPart::SpinButtonUpButton); + mpSpinDownStyle = createStyleContext(GtkControlPart::SpinButtonDownButton); + + /* NoteBook */ + mpNotebookStyle = createStyleContext(GtkControlPart::Notebook); + mpNotebookStackStyle = createStyleContext(GtkControlPart::NotebookStack); + mpNotebookHeaderStyle = createStyleContext(GtkControlPart::NotebookHeader); + mpNotebookHeaderTabsStyle = createStyleContext(GtkControlPart::NotebookHeaderTabs); + mpNotebookHeaderTabsTabStyle = createStyleContext(GtkControlPart::NotebookHeaderTabsTab); + mpNotebookHeaderTabsTabLabelStyle = createStyleContext(GtkControlPart::NotebookHeaderTabsTabLabel); + mpNotebookHeaderTabsTabActiveLabelStyle = createStyleContext(GtkControlPart::NotebookHeaderTabsTabActiveLabel); + mpNotebookHeaderTabsTabHoverLabelStyle = createStyleContext(GtkControlPart::NotebookHeaderTabsTabHoverLabel); + + /* Combobox */ + gComboBox = gtk_combo_box_text_new_with_entry(); + gtk_container_add(GTK_CONTAINER(gDumbContainer), gComboBox); + mpComboboxStyle = createStyleContext(GtkControlPart::Combobox); + mpComboboxBoxStyle = createStyleContext(GtkControlPart::ComboboxBox); + mpComboboxEntryStyle = createStyleContext(GtkControlPart::ComboboxBoxEntry); + mpComboboxButtonStyle = createStyleContext(GtkControlPart::ComboboxBoxButton); + mpComboboxButtonBoxStyle = createStyleContext(GtkControlPart::ComboboxBoxButtonBox); + mpComboboxButtonArrowStyle = createStyleContext(GtkControlPart::ComboboxBoxButtonBoxArrow); + + /* Listbox */ + gListBox = gtk_combo_box_text_new(); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gListBox), "sample"); + gtk_container_add(GTK_CONTAINER(gDumbContainer), gListBox); + mpListboxStyle = createStyleContext(GtkControlPart::Listbox); + mpListboxBoxStyle = createStyleContext(GtkControlPart::ListboxBox); + mpListboxButtonStyle = createStyleContext(GtkControlPart::ListboxBoxButton); + mpListboxButtonBoxStyle = createStyleContext(GtkControlPart::ListboxBoxButtonBox); + mpListboxButtonArrowStyle = createStyleContext(GtkControlPart::ListboxBoxButtonBoxArrow); + + mpMenuBarStyle = createStyleContext(GtkControlPart::MenuBar); + mpMenuBarItemStyle = createStyleContext(GtkControlPart::MenuBarItem); + + /* Menu */ + mpMenuWindowStyle = createStyleContext(GtkControlPart::MenuWindow); + mpMenuStyle = createStyleContext(GtkControlPart::Menu); + + mpMenuItemStyle = createStyleContext(GtkControlPart::MenuItem); + mpMenuItemLabelStyle = createStyleContext(GtkControlPart::MenuItemLabel); + mpMenuItemArrowStyle = createStyleContext(GtkControlPart::MenuItemArrow); + mpCheckMenuItemStyle = createStyleContext(GtkControlPart::CheckMenuItem); + mpCheckMenuItemCheckStyle = createStyleContext(GtkControlPart::CheckMenuItemCheck); + mpRadioMenuItemStyle = createStyleContext(GtkControlPart::RadioMenuItem); + mpRadioMenuItemRadioStyle = createStyleContext(GtkControlPart::RadioMenuItemRadio); + mpSeparatorMenuItemStyle = createStyleContext(GtkControlPart::SeparatorMenuItem); + mpSeparatorMenuItemSeparatorStyle = createStyleContext(GtkControlPart::SeparatorMenuItemSeparator); + + /* Frames */ + mpFrameOutStyle = mpFrameInStyle = createStyleContext(GtkControlPart::FrameBorder); + getStyleContext(&mpFixedHoriLineStyle, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)); + getStyleContext(&mpFixedVertLineStyle, gtk_separator_new(GTK_ORIENTATION_VERTICAL)); + + + /* Tree List */ + gTreeViewWidget = gtk_tree_view_new(); + gtk_container_add(GTK_CONTAINER(gDumbContainer), gTreeViewWidget); + + GtkTreeViewColumn* firstTreeViewColumn = gtk_tree_view_column_new(); + gtk_tree_view_column_set_title(firstTreeViewColumn, "M"); + gtk_tree_view_append_column(GTK_TREE_VIEW(gTreeViewWidget), firstTreeViewColumn); + + GtkTreeViewColumn* middleTreeViewColumn = gtk_tree_view_column_new(); + gtk_tree_view_column_set_title(middleTreeViewColumn, "M"); + gtk_tree_view_append_column(GTK_TREE_VIEW(gTreeViewWidget), middleTreeViewColumn); + gtk_tree_view_set_expander_column(GTK_TREE_VIEW(gTreeViewWidget), middleTreeViewColumn); + + GtkTreeViewColumn* lastTreeViewColumn = gtk_tree_view_column_new(); + gtk_tree_view_column_set_title(lastTreeViewColumn, "M"); + gtk_tree_view_append_column(GTK_TREE_VIEW(gTreeViewWidget), lastTreeViewColumn); + + /* Use the middle column's header for our button */ + GtkWidget* pTreeHeaderCellWidget = gtk_tree_view_column_get_button(middleTreeViewColumn); + mpTreeHeaderButtonStyle = gtk_widget_get_style_context(pTreeHeaderCellWidget); + + /* Progress Bar */ + mpProgressBarStyle = createStyleContext(GtkControlPart::ProgressBar); + mpProgressBarTroughStyle = createStyleContext(GtkControlPart::ProgressBarTrough); + mpProgressBarProgressStyle = createStyleContext(GtkControlPart::ProgressBarProgress); + + gtk_widget_show_all(gDumbContainer); +#endif +} + +void GtkSalGraphics::GetResolution(sal_Int32& rDPIX, sal_Int32& rDPIY) +{ + char* pForceDpi; + if ((pForceDpi = getenv("SAL_FORCEDPI"))) + { + OString sForceDPI(pForceDpi); + rDPIX = rDPIY = sForceDPI.toInt32(); + return; + } + +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkScreen* pScreen = gtk_widget_get_screen(mpWindow); + double fResolution = -1.0; + g_object_get(pScreen, "resolution", &fResolution, nullptr); + + if (fResolution > 0.0) + rDPIX = rDPIY = sal_Int32(fResolution); + else + rDPIX = rDPIY = 96; +#else + rDPIX = rDPIY = 96; +#endif +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/FPServiceInfo.hxx b/vcl/unx/gtk3_kde5/FPServiceInfo.hxx new file mode 100644 index 0000000000..1fbb8fd276 --- /dev/null +++ b/vcl/unx/gtk3_kde5/FPServiceInfo.hxx @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +// the service names +#define FILE_PICKER_SERVICE_NAME "com.sun.star.ui.dialogs.Gtk3KDE5FilePicker" + +// the implementation names +#define FILE_PICKER_IMPL_NAME "com.sun.star.ui.dialogs.Gtk3KDE5FilePicker" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkaction.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkaction.cxx new file mode 100644 index 0000000000..6b9f5e3d8c --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkaction.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/a11y/atkaction.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkbridge.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkbridge.cxx new file mode 100644 index 0000000000..e07fc6c295 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkbridge.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/a11y/atkbridge.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkcomponent.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkcomponent.cxx new file mode 100644 index 0000000000..5eac70ebd9 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkcomponent.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/a11y/atkcomponent.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkeditabletext.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkeditabletext.cxx new file mode 100644 index 0000000000..792e432e13 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkeditabletext.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/a11y/atkeditabletext.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkfactory.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkfactory.cxx new file mode 100644 index 0000000000..2ddc5c55c4 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkfactory.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/a11y/atkfactory.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkhypertext.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkhypertext.cxx new file mode 100644 index 0000000000..f3fa99006e --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkhypertext.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/a11y/atkhypertext.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkimage.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkimage.cxx new file mode 100644 index 0000000000..eb3acc7134 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkimage.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/a11y/atkimage.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atklistener.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atklistener.cxx new file mode 100644 index 0000000000..cf10d29dfd --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atklistener.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/a11y/atklistener.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkregistry.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkregistry.cxx new file mode 100644 index 0000000000..cc10fbfdd5 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkregistry.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/a11y/atkregistry.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkselection.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkselection.cxx new file mode 100644 index 0000000000..4c7fc5c15f --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkselection.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/a11y/atkselection.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktable.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktable.cxx new file mode 100644 index 0000000000..672125d1a8 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktable.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/a11y/atktable.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktablecell.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktablecell.cxx new file mode 100644 index 0000000000..fdbb6f6263 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktablecell.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/a11y/atktablecell.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktext.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktext.cxx new file mode 100644 index 0000000000..de1d1f06ca --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktext.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/a11y/atktext.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktextattributes.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktextattributes.cxx new file mode 100644 index 0000000000..aeac0e02dc --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atktextattributes.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/a11y/atktextattributes.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkutil.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkutil.cxx new file mode 100644 index 0000000000..193b08e9ab --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkutil.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/a11y/atkutil.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkvalue.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkvalue.cxx new file mode 100644 index 0000000000..75825b4e12 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkvalue.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/a11y/atkvalue.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkwrapper.cxx b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkwrapper.cxx new file mode 100644 index 0000000000..a1bcc2e290 --- /dev/null +++ b/vcl/unx/gtk3_kde5/a11y/gtk3_kde5_atkwrapper.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/a11y/atkwrapper.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/filepicker_ipc_commands.hxx b/vcl/unx/gtk3_kde5/filepicker_ipc_commands.hxx new file mode 100644 index 0000000000..1d0925257b --- /dev/null +++ b/vcl/unx/gtk3_kde5/filepicker_ipc_commands.hxx @@ -0,0 +1,173 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include +#include +#include + +#include +#include + +// #define DEBUG_FILEPICKER_IPC + +namespace rtl +{ +class OUString; +} +class QString; + +enum class Commands : uint16_t +{ + SetTitle, + SetWinId, + Execute, + SetMultiSelectionMode, + SetDefaultName, + SetDisplayDirectory, + GetDisplayDirectory, + GetSelectedFiles, + AppendFilter, + SetCurrentFilter, + GetCurrentFilter, + SetValue, + GetValue, + EnableControl, + SetLabel, + GetLabel, + AddCheckBox, + Initialize, + Quit, + EnablePickFolderMode, +}; + +inline std::vector readIpcStringArg(std::istream& stream) +{ + uint32_t length = 0; + stream >> length; + stream.ignore(); // skip space separator + std::vector buffer(length, '\0'); + stream.read(buffer.data(), length); + return buffer; +} + +void readIpcArg(std::istream& stream, OUString& string); +void readIpcArg(std::istream& stream, QString& string); +void readIpcArg(std::istream& stream, css::uno::Sequence& seq); + +inline void readIpcArg(std::istream& stream, Commands& value) +{ + uint16_t v = 0; + stream >> v; + stream.ignore(); // skip space + value = static_cast(v); +} + +void readIpcArg(std::istream&, sal_Bool) = delete; + +inline void readIpcArg(std::istream& stream, bool& value) +{ + stream >> value; + stream.ignore(); // skip space +} + +inline void readIpcArg(std::istream& stream, sal_Int16& value) +{ + stream >> value; + stream.ignore(); // skip space +} + +inline void readIpcArg(std::istream& stream, sal_uIntPtr& value) +{ + stream >> value; + stream.ignore(); // skip space +} + +#if SAL_TYPES_SIZEOFPOINTER == 4 +inline void readIpcArg(std::istream& stream, uint64_t& value) +{ + stream >> value; + stream.ignore(); // skip space +} +#endif + +inline void readIpcArgs(std::istream& /*stream*/) +{ + // end of arguments, nothing to do +} + +template +inline void readIpcArgs(std::istream& stream, T& arg, Args&... args) +{ + readIpcArg(stream, arg); + readIpcArgs(stream, args...); +} + +void sendIpcArg(std::ostream& stream, const OUString& string); +void sendIpcArg(std::ostream& stream, const QString& string); + +inline void sendIpcStringArg(std::ostream& stream, uint32_t length, const char* string) +{ + stream << length << ' '; + stream.write(string, length); + stream << ' '; +} + +inline void sendIpcArg(std::ostream& stream, Commands value) +{ + stream << static_cast(value) << ' '; +} + +void sendIpcArg(std::ostream&, sal_Bool) = delete; + +inline void sendIpcArg(std::ostream& stream, bool value) { stream << value << ' '; } + +inline void sendIpcArg(std::ostream& stream, sal_Int16 value) { stream << value << ' '; } + +inline void sendIpcArg(std::ostream& stream, sal_uIntPtr value) { stream << value << ' '; } + +#if SAL_TYPES_SIZEOFPOINTER == 4 +inline void sendIpcArg(std::ostream& stream, uint64_t value) { stream << value << ' '; } +#endif + +inline void sendIpcArgsImpl(std::ostream& stream) +{ + // end of arguments, flush stream + stream << std::endl; +} + +template +inline void sendIpcArgsImpl(std::ostream& stream, const T& arg, const Args&... args) +{ + sendIpcArg(stream, arg); + sendIpcArgsImpl(stream, args...); +} + +template +inline void sendIpcArgs(std::ostream& stream, const T& arg, const Args&... args) +{ + sendIpcArgsImpl(stream, arg, args...); +#ifdef DEBUG_FILEPICKER_IPC + std::cerr << "IPC MSG: "; + sendIpcArgsImpl(std::cerr, arg, args...); +#endif +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_cairo.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_cairo.cxx new file mode 100644 index 0000000000..ed8780447f --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_cairo.cxx @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gtkcairo.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_customcellrenderer.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_customcellrenderer.cxx new file mode 100644 index 0000000000..aebe2c89ed --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_customcellrenderer.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../gtk3/customcellrenderer.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker.cxx new file mode 100644 index 0000000000..d3a053a080 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker.cxx @@ -0,0 +1,455 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include + +#include "gtk3_kde5_filepicker.hxx" + +#include +#include +#include +#include +#include + +#include +#include +#include "FPServiceInfo.hxx" + +#undef Region + +#include + +using namespace ::com::sun::star; +using namespace ::com::sun::star::ui::dialogs; +using namespace ::com::sun::star::ui::dialogs::TemplateDescription; +using namespace ::com::sun::star::ui::dialogs::ExtendedFilePickerElementIds; +using namespace ::com::sun::star::ui::dialogs::CommonFilePickerElementIds; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::beans; +using namespace ::com::sun::star::uno; + +// helper functions + +namespace +{ +uno::Sequence FilePicker_getSupportedServiceNames() +{ + return { "com.sun.star.ui.dialogs.FilePicker", "com.sun.star.ui.dialogs.SystemFilePicker", + "com.sun.star.ui.dialogs.Gtk3KDE5FilePicker" }; +} +} + +// Gtk3KDE5FilePicker + +Gtk3KDE5FilePicker::Gtk3KDE5FilePicker(const uno::Reference&) + : Gtk3KDE5FilePicker_Base(_helperMutex) +{ + setMultiSelectionMode(false); + + // tdf#124598 dummy KWidget use to make gtk3_kde5 VCL plugin link against KIO libraries + QString sDummyStr; + QUrl aUrl = KFileWidget::getStartUrl(QUrl(), sDummyStr); + aUrl.setPath("/dev/null"); +} + +Gtk3KDE5FilePicker::~Gtk3KDE5FilePicker() = default; + +void SAL_CALL +Gtk3KDE5FilePicker::addFilePickerListener(const uno::Reference& xListener) +{ + SolarMutexGuard aGuard; + m_xListener = xListener; +} + +void SAL_CALL +Gtk3KDE5FilePicker::removeFilePickerListener(const uno::Reference&) +{ + SolarMutexGuard aGuard; + m_xListener.clear(); +} + +void SAL_CALL Gtk3KDE5FilePicker::setTitle(const OUString& title) +{ + m_ipc.sendCommand(Commands::SetTitle, title); +} + +sal_Int16 SAL_CALL Gtk3KDE5FilePicker::execute() +{ + SolarMutexGuard g; + return m_ipc.execute(); +} + +void SAL_CALL Gtk3KDE5FilePicker::setMultiSelectionMode(sal_Bool multiSelect) +{ + m_ipc.sendCommand(Commands::SetMultiSelectionMode, bool(multiSelect)); +} + +void SAL_CALL Gtk3KDE5FilePicker::setDefaultName(const OUString& name) +{ + m_ipc.sendCommand(Commands::SetDefaultName, name); +} + +void SAL_CALL Gtk3KDE5FilePicker::setDisplayDirectory(const OUString& dir) +{ + m_ipc.sendCommand(Commands::SetDisplayDirectory, dir); +} + +OUString SAL_CALL Gtk3KDE5FilePicker::getDisplayDirectory() +{ + auto id = m_ipc.sendCommand(Commands::GetDisplayDirectory); + OUString dir; + m_ipc.readResponse(id, dir); + return dir; +} + +uno::Sequence SAL_CALL Gtk3KDE5FilePicker::getFiles() +{ + uno::Sequence seq = getSelectedFiles(); + if (seq.getLength() > 1) + seq.realloc(1); + return seq; +} + +uno::Sequence SAL_CALL Gtk3KDE5FilePicker::getSelectedFiles() +{ + auto id = m_ipc.sendCommand(Commands::GetSelectedFiles); + uno::Sequence seq; + m_ipc.readResponse(id, seq); + return seq; +} + +void SAL_CALL Gtk3KDE5FilePicker::appendFilter(const OUString& title, const OUString& filter) +{ + m_ipc.sendCommand(Commands::AppendFilter, title, filter); +} + +void SAL_CALL Gtk3KDE5FilePicker::setCurrentFilter(const OUString& title) +{ + m_ipc.sendCommand(Commands::SetCurrentFilter, title); +} + +OUString SAL_CALL Gtk3KDE5FilePicker::getCurrentFilter() +{ + auto id = m_ipc.sendCommand(Commands::GetCurrentFilter); + OUString filter; + m_ipc.readResponse(id, filter); + return filter; +} + +void SAL_CALL Gtk3KDE5FilePicker::appendFilterGroup(const OUString& /*rGroupTitle*/, + const uno::Sequence& filters) +{ + const sal_uInt16 length = filters.getLength(); + for (sal_uInt16 i = 0; i < length; ++i) + { + beans::StringPair aPair = filters[i]; + appendFilter(aPair.First, aPair.Second); + } +} + +void SAL_CALL Gtk3KDE5FilePicker::setValue(sal_Int16 controlId, sal_Int16 nControlAction, + const uno::Any& value) +{ + if (value.has()) + { + m_ipc.sendCommand(Commands::SetValue, controlId, nControlAction, value.get()); + } + else + { + SAL_INFO("vcl.gtkkde5", "set value of unhandled type " << controlId); + } +} + +uno::Any SAL_CALL Gtk3KDE5FilePicker::getValue(sal_Int16 controlId, sal_Int16 nControlAction) +{ + if (CHECKBOX_AUTOEXTENSION == controlId) + // We ignore this one and rely on QFileDialog to provide the function. + // Always return false, to pretend we do not support this, otherwise + // LO core would try to be smart and cut the extension in some places, + // interfering with QFileDialog's handling of it. QFileDialog also + // saves the value of the setting, so LO core is not needed for that either. + return uno::Any(false); + + auto id = m_ipc.sendCommand(Commands::GetValue, controlId, nControlAction); + + bool value = false; + m_ipc.readResponse(id, value); + + return uno::Any(value); +} + +void SAL_CALL Gtk3KDE5FilePicker::enableControl(sal_Int16 controlId, sal_Bool enable) +{ + m_ipc.sendCommand(Commands::EnableControl, controlId, bool(enable)); +} + +void SAL_CALL Gtk3KDE5FilePicker::setLabel(sal_Int16 controlId, const OUString& label) +{ + m_ipc.sendCommand(Commands::SetLabel, controlId, label); +} + +OUString SAL_CALL Gtk3KDE5FilePicker::getLabel(sal_Int16 controlId) +{ + auto id = m_ipc.sendCommand(Commands::GetLabel, controlId); + OUString label; + m_ipc.readResponse(id, label); + return label; +} + +void Gtk3KDE5FilePicker::addCustomControl(sal_Int16 controlId) +{ + TranslateId resId; + + switch (controlId) + { + case CHECKBOX_AUTOEXTENSION: + resId = STR_SVT_FILEPICKER_AUTO_EXTENSION; + break; + case CHECKBOX_PASSWORD: + resId = STR_SVT_FILEPICKER_PASSWORD; + break; + case CHECKBOX_FILTEROPTIONS: + resId = STR_SVT_FILEPICKER_FILTER_OPTIONS; + break; + case CHECKBOX_READONLY: + resId = STR_SVT_FILEPICKER_READONLY; + break; + case CHECKBOX_LINK: + resId = STR_SVT_FILEPICKER_INSERT_AS_LINK; + break; + case CHECKBOX_PREVIEW: + resId = STR_SVT_FILEPICKER_SHOW_PREVIEW; + break; + case CHECKBOX_SELECTION: + resId = STR_SVT_FILEPICKER_SELECTION; + break; + case CHECKBOX_GPGENCRYPTION: + resId = STR_SVT_FILEPICKER_GPGENCRYPT; + break; + case PUSHBUTTON_PLAY: + resId = STR_SVT_FILEPICKER_PLAY; + break; + case LISTBOX_VERSION: + resId = STR_SVT_FILEPICKER_VERSION; + break; + case LISTBOX_TEMPLATE: + resId = STR_SVT_FILEPICKER_TEMPLATES; + break; + case LISTBOX_IMAGE_TEMPLATE: + resId = STR_SVT_FILEPICKER_IMAGE_TEMPLATE; + break; + case LISTBOX_IMAGE_ANCHOR: + resId = STR_SVT_FILEPICKER_IMAGE_ANCHOR; + break; + case LISTBOX_VERSION_LABEL: + case LISTBOX_TEMPLATE_LABEL: + case LISTBOX_IMAGE_TEMPLATE_LABEL: + case LISTBOX_IMAGE_ANCHOR_LABEL: + case LISTBOX_FILTER_SELECTOR: + break; + } + + switch (controlId) + { + case CHECKBOX_AUTOEXTENSION: + case CHECKBOX_PASSWORD: + case CHECKBOX_FILTEROPTIONS: + case CHECKBOX_READONLY: + case CHECKBOX_LINK: + case CHECKBOX_PREVIEW: + case CHECKBOX_SELECTION: + case CHECKBOX_GPGENCRYPTION: + { + // the checkbox is created even for CHECKBOX_AUTOEXTENSION to simplify + // code, but the checkbox is hidden and ignored + bool hidden = controlId == CHECKBOX_AUTOEXTENSION; + + m_ipc.sendCommand(Commands::AddCheckBox, controlId, hidden, getResString(resId)); + + break; + } + case PUSHBUTTON_PLAY: + case LISTBOX_VERSION: + case LISTBOX_TEMPLATE: + case LISTBOX_IMAGE_TEMPLATE: + case LISTBOX_IMAGE_ANCHOR: + case LISTBOX_VERSION_LABEL: + case LISTBOX_TEMPLATE_LABEL: + case LISTBOX_IMAGE_TEMPLATE_LABEL: + case LISTBOX_IMAGE_ANCHOR_LABEL: + case LISTBOX_FILTER_SELECTOR: + break; + } +} + +void SAL_CALL Gtk3KDE5FilePicker::initialize(const uno::Sequence& args) +{ + // parameter checking + uno::Any arg; + if (args.getLength() == 0) + { + throw lang::IllegalArgumentException("no arguments", static_cast(this), 1); + } + + arg = args[0]; + + if ((arg.getValueType() != cppu::UnoType::get()) + && (arg.getValueType() != cppu::UnoType::get())) + { + throw lang::IllegalArgumentException("invalid argument type", + static_cast(this), 1); + } + + sal_Int16 templateId = -1; + arg >>= templateId; + + bool saveDialog = false; + switch (templateId) + { + case FILEOPEN_SIMPLE: + break; + + case FILESAVE_SIMPLE: + saveDialog = true; + break; + + case FILESAVE_AUTOEXTENSION: + saveDialog = true; + addCustomControl(CHECKBOX_AUTOEXTENSION); + break; + + case FILESAVE_AUTOEXTENSION_PASSWORD: + { + saveDialog = true; + addCustomControl(CHECKBOX_PASSWORD); + addCustomControl(CHECKBOX_GPGENCRYPTION); + break; + } + case FILESAVE_AUTOEXTENSION_PASSWORD_FILTEROPTIONS: + { + saveDialog = true; + addCustomControl(CHECKBOX_AUTOEXTENSION); + addCustomControl(CHECKBOX_PASSWORD); + addCustomControl(CHECKBOX_GPGENCRYPTION); + addCustomControl(CHECKBOX_FILTEROPTIONS); + break; + } + case FILESAVE_AUTOEXTENSION_SELECTION: + saveDialog = true; + addCustomControl(CHECKBOX_AUTOEXTENSION); + addCustomControl(CHECKBOX_SELECTION); + break; + + case FILESAVE_AUTOEXTENSION_TEMPLATE: + saveDialog = true; + addCustomControl(CHECKBOX_AUTOEXTENSION); + addCustomControl(LISTBOX_TEMPLATE); + break; + + case FILEOPEN_LINK_PREVIEW_IMAGE_TEMPLATE: + addCustomControl(CHECKBOX_LINK); + addCustomControl(CHECKBOX_PREVIEW); + addCustomControl(LISTBOX_IMAGE_TEMPLATE); + break; + + case FILEOPEN_LINK_PREVIEW_IMAGE_ANCHOR: + addCustomControl(CHECKBOX_LINK); + addCustomControl(CHECKBOX_PREVIEW); + addCustomControl(LISTBOX_IMAGE_ANCHOR); + break; + + case FILEOPEN_PLAY: + addCustomControl(PUSHBUTTON_PLAY); + break; + + case FILEOPEN_LINK_PLAY: + addCustomControl(CHECKBOX_LINK); + addCustomControl(PUSHBUTTON_PLAY); + break; + + case FILEOPEN_READONLY_VERSION: + addCustomControl(CHECKBOX_READONLY); + addCustomControl(LISTBOX_VERSION); + break; + + case FILEOPEN_LINK_PREVIEW: + addCustomControl(CHECKBOX_LINK); + addCustomControl(CHECKBOX_PREVIEW); + break; + + case FILEOPEN_PREVIEW: + addCustomControl(CHECKBOX_PREVIEW); + break; + + default: + SAL_INFO("vcl.gtkkde5", "unknown templates " << templateId); + return; + } + + setTitle(getResString(saveDialog ? STR_FILEDLG_SAVE : STR_FILEDLG_OPEN)); + + m_ipc.sendCommand(Commands::Initialize, saveDialog); +} + +void SAL_CALL Gtk3KDE5FilePicker::cancel() +{ + // TODO +} + +void Gtk3KDE5FilePicker::disposing(const lang::EventObject& rEvent) +{ + uno::Reference xFilePickerListener(rEvent.Source, uno::UNO_QUERY); + + if (xFilePickerListener.is()) + { + removeFilePickerListener(xFilePickerListener); + } +} + +OUString SAL_CALL Gtk3KDE5FilePicker::getImplementationName() { return FILE_PICKER_IMPL_NAME; } + +sal_Bool SAL_CALL Gtk3KDE5FilePicker::supportsService(const OUString& ServiceName) +{ + return cppu::supportsService(this, ServiceName); +} + +uno::Sequence SAL_CALL Gtk3KDE5FilePicker::getSupportedServiceNames() +{ + return FilePicker_getSupportedServiceNames(); +} + +void Gtk3KDE5FilePicker::filterChanged() +{ + FilePickerEvent aEvent; + aEvent.ElementId = LISTBOX_FILTER; + SAL_INFO("vcl.gtkkde5", "filter changed"); + if (m_xListener.is()) + m_xListener->controlStateChanged(aEvent); +} + +void Gtk3KDE5FilePicker::selectionChanged() +{ + FilePickerEvent aEvent; + SAL_INFO("vcl.gtkkde5", "file selection changed"); + if (m_xListener.is()) + m_xListener->fileSelectionChanged(aEvent); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker.hxx b/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker.hxx new file mode 100644 index 0000000000..7ce3910048 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker.hxx @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include + +#include "gtk3_kde5_filepicker_ipc.hxx" + +typedef ::cppu::WeakComponentImplHelper + Gtk3KDE5FilePicker_Base; + +class Gtk3KDE5FilePicker : public Gtk3KDE5FilePicker_Base +{ +protected: + css::uno::Reference m_xListener; + + osl::Mutex _helperMutex; + Gtk3KDE5FilePickerIpc m_ipc; + +public: + explicit Gtk3KDE5FilePicker(const css::uno::Reference&); + virtual ~Gtk3KDE5FilePicker() override; + + // XFilePickerNotifier + virtual void SAL_CALL addFilePickerListener( + const css::uno::Reference& xListener) override; + virtual void SAL_CALL removeFilePickerListener( + const css::uno::Reference& xListener) override; + + // XExecutableDialog functions + virtual void SAL_CALL setTitle(const OUString& rTitle) override; + virtual sal_Int16 SAL_CALL execute() override; + + // XFilePicker functions + virtual void SAL_CALL setMultiSelectionMode(sal_Bool bMode) override; + virtual void SAL_CALL setDefaultName(const OUString& rName) override; + virtual void SAL_CALL setDisplayDirectory(const OUString& rDirectory) override; + virtual OUString SAL_CALL getDisplayDirectory() override; + virtual css::uno::Sequence SAL_CALL getFiles() override; + + // XFilterManager functions + virtual void SAL_CALL appendFilter(const OUString& rTitle, const OUString& rFilter) override; + virtual void SAL_CALL setCurrentFilter(const OUString& rTitle) override; + virtual OUString SAL_CALL getCurrentFilter() override; + + // XFilterGroupManager functions + virtual void SAL_CALL + appendFilterGroup(const OUString& rGroupTitle, + const css::uno::Sequence& rFilters) override; + + // XFilePickerControlAccess functions + virtual void SAL_CALL setValue(sal_Int16 nControlId, sal_Int16 nControlAction, + const css::uno::Any& rValue) override; + virtual css::uno::Any SAL_CALL getValue(sal_Int16 nControlId, + sal_Int16 nControlAction) override; + virtual void SAL_CALL enableControl(sal_Int16 nControlId, sal_Bool bEnable) override; + virtual void SAL_CALL setLabel(sal_Int16 nControlId, const OUString& rLabel) override; + virtual OUString SAL_CALL getLabel(sal_Int16 nControlId) override; + + /* TODO XFilePreview + + virtual css::uno::Sequence< sal_Int16 > SAL_CALL getSupportedImageFormats( ); + virtual sal_Int32 SAL_CALL getTargetColorDepth( ); + virtual sal_Int32 SAL_CALL getAvailableWidth( ); + virtual sal_Int32 SAL_CALL getAvailableHeight( ); + virtual void SAL_CALL setImage( sal_Int16 aImageFormat, const css::uno::Any &rImage ); + virtual sal_Bool SAL_CALL setShowState( sal_Bool bShowState ); + virtual sal_Bool SAL_CALL getShowState( ); + */ + + // XFilePicker2 functions + virtual css::uno::Sequence SAL_CALL getSelectedFiles() override; + + // XInitialization + virtual void SAL_CALL initialize(const css::uno::Sequence& rArguments) override; + + // XCancellable + virtual void SAL_CALL cancel() override; + + // XEventListener + virtual void disposing(const css::lang::EventObject& rEvent); + using cppu::WeakComponentImplHelperBase::disposing; + + // XServiceInfo + virtual OUString SAL_CALL getImplementationName() override; + virtual sal_Bool SAL_CALL supportsService(const OUString& rServiceName) override; + virtual css::uno::Sequence SAL_CALL getSupportedServiceNames() override; + +private: + Gtk3KDE5FilePicker(const Gtk3KDE5FilePicker&) = delete; + Gtk3KDE5FilePicker& operator=(const Gtk3KDE5FilePicker&) = delete; + + //add a custom control widget to the file dialog + void addCustomControl(sal_Int16 controlId); + + // emit XFilePickerListener controlStateChanged event + void filterChanged(); + // emit XFilePickerListener fileSelectionChanged event + void selectionChanged(); +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker_ipc.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker_ipc.cxx new file mode 100644 index 0000000000..ebfa0fbd7e --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker_ipc.cxx @@ -0,0 +1,276 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include + +#include "gtk3_kde5_filepicker_ipc.hxx" + +#undef Region + +#include + +#include + +#include +#include +#include + +#include +#include + +#include + +#include + +#include + +using namespace ::com::sun::star::ui::dialogs; + +// helper functions + +namespace +{ +OUString applicationDirPath() +{ + OUString applicationFilePath; + osl_getExecutableFile(&applicationFilePath.pData); + OUString applicationSystemPath; + osl_getSystemPathFromFileURL(applicationFilePath.pData, &applicationSystemPath.pData); + const auto utf8Path = applicationSystemPath.toUtf8(); + auto ret = boost::filesystem::path(utf8Path.getStr(), utf8Path.getStr() + utf8Path.getLength()); + ret.remove_filename(); + return OUString::fromUtf8(std::string_view(ret.c_str())); +} + +OUString findPickerExecutable() +{ + const auto path = applicationDirPath(); + OUString ret; + osl_searchFileURL(u"lo_kde5filepicker"_ustr.pData, path.pData, &ret.pData); + if (ret.isEmpty()) + throw std::system_error(std::make_error_code(std::errc::no_such_file_or_directory), + "could not find lo_kde5filepicker executable"); + return ret; +} +} + +void readIpcArg(std::istream& stream, OUString& str) +{ + const auto buffer = readIpcStringArg(stream); + str = OUString::fromUtf8(std::string_view(buffer.data(), buffer.size())); +} + +void readIpcArg(std::istream& stream, css::uno::Sequence& seq) +{ + uint32_t numFiles = 0; + stream >> numFiles; + stream.ignore(); // skip space; + seq.realloc(numFiles); + OUString* pSeq = seq.getArray(); + for (size_t i = 0; i < numFiles; ++i) + { + readIpcArg(stream, pSeq[i]); + } +} + +void sendIpcArg(std::ostream& stream, const OUString& string) +{ + const auto utf8 = string.toUtf8(); + sendIpcStringArg(stream, utf8.getLength(), utf8.getStr()); +} + +OUString getResString(TranslateId pResId) +{ + if (!pResId) + return {}; + + return FpsResId(pResId); +} + +// handles the IPC commands for dialog execution and ends the dummy Gtk dialog once the IPC response is there +static void handleIpcForExecute(Gtk3KDE5FilePickerIpc* pFilePickerIpc, GtkWidget* pDummyDialog, + bool* bResult) +{ + auto id = pFilePickerIpc->sendCommand(Commands::Execute); + pFilePickerIpc->readResponse(id, *bResult); + + // end the dummy dialog + gtk_widget_hide(pDummyDialog); +} + +// Gtk3KDE5FilePicker + +Gtk3KDE5FilePickerIpc::Gtk3KDE5FilePickerIpc() +{ + const auto exe = findPickerExecutable(); + oslProcessError result; + oslSecurity pSecurity = osl_getCurrentSecurity(); + result = osl_executeProcess_WithRedirectedIO(exe.pData, nullptr, 0, osl_Process_NORMAL, + pSecurity, nullptr, nullptr, 0, &m_process, + &m_inputWrite, &m_outputRead, nullptr); + osl_freeSecurityHandle(pSecurity); + if (result != osl_Process_E_None) + throw std::system_error(std::make_error_code(std::errc::no_such_process), + "could not start lo_kde5filepicker executable"); +} + +Gtk3KDE5FilePickerIpc::~Gtk3KDE5FilePickerIpc() +{ + if (!m_process) + return; + + sendCommand(Commands::Quit); + osl_joinProcess(m_process); + + if (m_inputWrite) + osl_closeFile(m_inputWrite); + if (m_outputRead) + osl_closeFile(m_outputRead); + osl_freeProcessHandle(m_process); +} + +sal_Int16 Gtk3KDE5FilePickerIpc::execute() +{ + auto restoreMainWindow = blockMainWindow(); + + // dummy gtk dialog that will take care of processing events, + // not meant to be actually seen by user + GtkWidget* pDummyDialog = gtk_dialog_new(); + + bool accepted = false; + + // send IPC command and read response in a separate thread + std::thread aIpcHandler(&handleIpcForExecute, this, pDummyDialog, &accepted); + + // make dummy dialog not to be seen by user + gtk_window_set_decorated(GTK_WINDOW(pDummyDialog), false); + gtk_window_set_default_size(GTK_WINDOW(pDummyDialog), 0, 0); + gtk_window_set_accept_focus(GTK_WINDOW(pDummyDialog), false); + // gtk_widget_set_opacity() only has the desired effect when widget is already shown + gtk_widget_show(pDummyDialog); + gtk_widget_set_opacity(pDummyDialog, 0); + // run dialog, leaving event processing to GTK + // dialog will be closed by the separate 'aIpcHandler' thread once the IPC response is there + gtk_dialog_run(GTK_DIALOG(pDummyDialog)); + + aIpcHandler.join(); + + gtk_widget_destroy(pDummyDialog); + + if (restoreMainWindow) + restoreMainWindow(); + + return accepted ? ExecutableDialogResults::OK : ExecutableDialogResults::CANCEL; +} + +static gboolean ignoreDeleteEvent(GtkWidget* /*widget*/, GdkEvent* /*event*/, + gpointer /*user_data*/) +{ + return true; +} + +std::function Gtk3KDE5FilePickerIpc::blockMainWindow() +{ + weld::Window* pParentWin = Application::GetDefDialogParent(); + if (!pParentWin) + return {}; + + const SystemEnvData aSysData = pParentWin->get_system_data(); + auto* pMainWindow = static_cast(aSysData.pWidget); + if (!pMainWindow) + return {}; + + sendCommand(Commands::SetWinId, aSysData.GetWindowHandle(aSysData.pSalFrame)); + + SolarMutexGuard guard; + auto deleteEventSignalId = g_signal_lookup("delete_event", gtk_widget_get_type()); + + // disable the mainwindow + gtk_widget_set_sensitive(pMainWindow, false); + + // block the GtkSalFrame delete_event handler + auto blockedHandler = g_signal_handler_find( + pMainWindow, static_cast(G_SIGNAL_MATCH_ID | G_SIGNAL_MATCH_DATA), + deleteEventSignalId, 0, nullptr, nullptr, aSysData.pSalFrame); + g_signal_handler_block(pMainWindow, blockedHandler); + + // prevent the window from being closed + auto ignoreDeleteEventHandler + = g_signal_connect(pMainWindow, "delete_event", G_CALLBACK(ignoreDeleteEvent), nullptr); + + return [pMainWindow, ignoreDeleteEventHandler, blockedHandler] { + SolarMutexGuard cleanupGuard; + // re-enable window + gtk_widget_set_sensitive(pMainWindow, true); + + // allow it to be closed again + g_signal_handler_disconnect(pMainWindow, ignoreDeleteEventHandler); + + // unblock the GtkSalFrame handler + g_signal_handler_unblock(pMainWindow, blockedHandler); + }; +} + +void Gtk3KDE5FilePickerIpc::writeResponseLine(const std::string& line) +{ + sal_uInt64 bytesWritten = 0; + osl_writeFile(m_inputWrite, line.c_str(), line.size(), &bytesWritten); +} + +std::string Gtk3KDE5FilePickerIpc::readResponseLine() +{ + if (!m_responseBuffer.empty()) // check whether we have a line in our buffer + { + std::size_t it = m_responseBuffer.find('\n'); + if (it != std::string::npos) + { + auto ret = m_responseBuffer.substr(0, it); + m_responseBuffer.erase(0, it + 1); + return ret; + } + } + + const sal_uInt64 BUF_SIZE = 1024; + char buffer[BUF_SIZE]; + while (true) + { + sal_uInt64 bytesRead = 0; + auto err = osl_readFile(m_outputRead, buffer, BUF_SIZE, &bytesRead); + auto it = std::find(buffer, buffer + bytesRead, '\n'); + if (it != buffer + bytesRead) // check whether the chunk we read contains an EOL + { + // if so, append that part to the buffer and return it + std::string ret = m_responseBuffer.append(buffer, it); + // but keep anything else we may have read in our buffer + ++it; + m_responseBuffer.assign(it, buffer + bytesRead); + return ret; + } + // otherwise append everything we read to the buffer and try again + m_responseBuffer.append(buffer, bytesRead); + + if (err != osl_File_E_None && err != osl_File_E_AGAIN) + break; + } + return {}; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker_ipc.hxx b/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker_ipc.hxx new file mode 100644 index 0000000000..58ca3eb452 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_filepicker_ipc.hxx @@ -0,0 +1,135 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include +#include + +#include "filepicker_ipc_commands.hxx" + +#include +#include +#include +#include + +OUString getResString(TranslateId pResId); + +class Gtk3KDE5FilePickerIpc +{ +protected: + oslProcess m_process; + oslFileHandle m_inputWrite; + oslFileHandle m_outputRead; + // simple multiplexing: every command gets its own ID that can be used to + // read the corresponding response + uint64_t m_msgId = 1; + std::mutex m_mutex; + uint64_t m_incomingResponse = 0; + std::string m_responseBuffer; + std::stringstream m_responseStream; + +public: + explicit Gtk3KDE5FilePickerIpc(); + ~Gtk3KDE5FilePickerIpc(); + + sal_Int16 execute(); + + void writeResponseLine(const std::string& line); + + template uint64_t sendCommand(Commands command, const Args&... args) + { + auto id = m_msgId; + ++m_msgId; + std::stringstream stream; + sendIpcArgs(stream, id, command, args...); + writeResponseLine(stream.str()); + return id; + } + + std::string readResponseLine(); + + // workaround gcc <= 4.8 bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=55914 + template struct seq + { + }; + template struct gens : gens + { + }; + template struct gens<0, S...> + { + typedef seq type; + }; + template struct ArgsReader + { + ArgsReader(Args&... args) + : m_args(args...) + { + } + + void operator()(std::istream& stream) + { + callFunc(stream, typename gens::type()); + } + + private: + template void callFunc(std::istream& stream, seq) + { + readIpcArgs(stream, std::get(m_args)...); + } + + std::tuple m_args; + }; + + template void readResponse(uint64_t id, Args&... args) + { + ArgsReader argsReader(args...); + while (true) + { + // only let one thread read at any given time + std::scoped_lock lock(m_mutex); + + // check if we need to read (and potentially wait) a response ID + if (m_incomingResponse == 0) + { + m_responseStream.clear(); + m_responseStream.str(readResponseLine()); + readIpcArgs(m_responseStream, m_incomingResponse); + } + + if (m_incomingResponse == id) + { + // the response we are waiting for came in + argsReader(m_responseStream); + m_incomingResponse = 0; + break; + } + else + { + // the next response answers some other request, yield + std::this_thread::yield(); + } + } + } + +private: + std::function blockMainWindow(); +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_folderpicker.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_folderpicker.cxx new file mode 100644 index 0000000000..c1569e6be2 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_folderpicker.cxx @@ -0,0 +1,85 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "gtk3_kde5_folderpicker.hxx" + +#include + +#include + +using namespace ::com::sun::star; +using namespace ::com::sun::star::ui::dialogs; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::uno; + +// constructor + +Gtk3KDE5FolderPicker::Gtk3KDE5FolderPicker( + const uno::Reference& /*xContext*/) +{ + m_ipc.sendCommand(Commands::EnablePickFolderMode); + setTitle(getResString(STR_SVT_FOLDERPICKER_DEFAULT_TITLE)); +} + +Gtk3KDE5FolderPicker::~Gtk3KDE5FolderPicker() = default; + +void SAL_CALL Gtk3KDE5FolderPicker::setDisplayDirectory(const OUString& aDirectory) +{ + m_ipc.sendCommand(Commands::SetDisplayDirectory, aDirectory); +} + +OUString SAL_CALL Gtk3KDE5FolderPicker::getDisplayDirectory() +{ + auto id = m_ipc.sendCommand(Commands::GetDisplayDirectory); + OUString ret; + m_ipc.readResponse(id, ret); + return ret; +} + +OUString SAL_CALL Gtk3KDE5FolderPicker::getDirectory() +{ + auto id = m_ipc.sendCommand(Commands::GetSelectedFiles); + uno::Sequence seq; + m_ipc.readResponse(id, seq); + return seq.hasElements() ? seq[0] : OUString(); +} + +void SAL_CALL Gtk3KDE5FolderPicker::setDescription(const OUString& /*rDescription*/) {} + +// XExecutableDialog functions + +void SAL_CALL Gtk3KDE5FolderPicker::setTitle(const OUString& aTitle) +{ + m_ipc.sendCommand(Commands::SetTitle, aTitle); +} + +sal_Int16 SAL_CALL Gtk3KDE5FolderPicker::execute() +{ + SolarMutexGuard g; + return m_ipc.execute(); +} + +// XCancellable + +void SAL_CALL Gtk3KDE5FolderPicker::cancel() +{ + // TODO +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_folderpicker.hxx b/vcl/unx/gtk3_kde5/gtk3_kde5_folderpicker.hxx new file mode 100644 index 0000000000..9801a072ac --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_folderpicker.hxx @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include +#include + +#include +#include + +#include "gtk3_kde5_filepicker_ipc.hxx" + +class Gtk3KDE5FolderPicker : public cppu::WeakImplHelper +{ +protected: + Gtk3KDE5FilePickerIpc m_ipc; + +public: + // constructor + explicit Gtk3KDE5FolderPicker( + const css::uno::Reference& xServiceMgr); + virtual ~Gtk3KDE5FolderPicker() override; + + // XExecutableDialog functions + virtual void SAL_CALL setTitle(const OUString& aTitle) override; + virtual sal_Int16 SAL_CALL execute() override; + + // XFolderPicker functions + virtual void SAL_CALL setDisplayDirectory(const OUString& rDirectory) override; + virtual OUString SAL_CALL getDisplayDirectory() override; + virtual OUString SAL_CALL getDirectory() override; + virtual void SAL_CALL setDescription(const OUString& rDescription) override; + + // XCancellable + virtual void SAL_CALL cancel() override; + +private: + Gtk3KDE5FolderPicker(const Gtk3KDE5FolderPicker&) = delete; + Gtk3KDE5FolderPicker& operator=(const Gtk3KDE5FolderPicker&) = delete; +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_gloactiongroup.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_gloactiongroup.cxx new file mode 100644 index 0000000000..eccf49d3df --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_gloactiongroup.cxx @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gloactiongroup.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_glomenu.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_glomenu.cxx new file mode 100644 index 0000000000..979c9fba07 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_glomenu.cxx @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/glomenu.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_gtkdata.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_gtkdata.cxx new file mode 100644 index 0000000000..f03dce99a7 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_gtkdata.cxx @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gtkdata.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_gtkframe.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_gtkframe.cxx new file mode 100644 index 0000000000..8d8a07a3f2 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_gtkframe.cxx @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gtkframe.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_gtkinst.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_gtkinst.cxx new file mode 100644 index 0000000000..dd6aace5dc --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_gtkinst.cxx @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +// make gtk3 plug advertise correctly as kde5 hybrid +#define GTK_TOOLKIT_NAME "gtk3_kde5" +#include "../gtk3/gtkinst.cxx" + +#include "gtk3_kde5_filepicker.hxx" +#include "gtk3_kde5_folderpicker.hxx" + +#include + +uno::Reference +GtkInstance::createFilePicker(const uno::Reference& xMSF) +{ + try + { + return uno::Reference(new Gtk3KDE5FilePicker(xMSF)); + } + catch (const std::system_error& error) + { + OSL_FAIL(error.what()); + return { nullptr }; + } +} + +uno::Reference +GtkInstance::createFolderPicker(const uno::Reference& xMSF) +{ + try + { + return uno::Reference(new Gtk3KDE5FolderPicker(xMSF)); + } + catch (const std::system_error& error) + { + OSL_FAIL(error.what()); + return { nullptr }; + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_gtkobject.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_gtkobject.cxx new file mode 100644 index 0000000000..948c6bd3a8 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_gtkobject.cxx @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gtkobject.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_gtksalmenu.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_gtksalmenu.cxx new file mode 100644 index 0000000000..7963e6b603 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_gtksalmenu.cxx @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gtksalmenu.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_gtksys.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_gtksys.cxx new file mode 100644 index 0000000000..3f3ad9f1d3 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_gtksys.cxx @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/gtksys.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_hudawareness.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_hudawareness.cxx new file mode 100644 index 0000000000..e0b8f1812d --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_hudawareness.cxx @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/hudawareness.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/gtk3_kde5_salnativewidgets-gtk.cxx b/vcl/unx/gtk3_kde5/gtk3_kde5_salnativewidgets-gtk.cxx new file mode 100644 index 0000000000..428ff82042 --- /dev/null +++ b/vcl/unx/gtk3_kde5/gtk3_kde5_salnativewidgets-gtk.cxx @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "../gtk3/salnativewidgets-gtk.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/kde5_filepicker.cxx b/vcl/unx/gtk3_kde5/kde5_filepicker.cxx new file mode 100644 index 0000000000..28aee235a3 --- /dev/null +++ b/vcl/unx/gtk3_kde5/kde5_filepicker.cxx @@ -0,0 +1,286 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include "kde5_filepicker.hxx" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +// KDE5FilePicker + +KDE5FilePicker::KDE5FilePicker(QObject* parent) + : QObject(parent) + , _dialog(new QFileDialog(nullptr, {}, QDir::homePath())) + , _extraControls(new QWidget) + , _layout(new QGridLayout(_extraControls)) + , _winId(0) +{ + _dialog->setSupportedSchemes({ + QStringLiteral("file"), QStringLiteral("http"), QStringLiteral("https"), + QStringLiteral("webdav"), QStringLiteral("webdavs"), QStringLiteral("smb"), + QStringLiteral(""), // this makes removable devices shown + }); + + setMultiSelectionMode(false); + + connect(_dialog, &QFileDialog::filterSelected, this, &KDE5FilePicker::filterChanged); + connect(_dialog, &QFileDialog::fileSelected, this, &KDE5FilePicker::selectionChanged); + + setupCustomWidgets(); +} + +void KDE5FilePicker::enableFolderMode() +{ + _dialog->setOption(QFileDialog::ShowDirsOnly, true); + // Workaround for https://bugs.kde.org/show_bug.cgi?id=406464 : + // Don't set file mode to QFileDialog::Directory when native KDE Plasma 5 + // file dialog is used, since clicking on directory "bar" inside directory "foo" + // and then confirming would return "foo" rather than "foo/bar"; + // on the other hand, non-native file dialog needs 'QFileDialog::Directory' + // and doesn't allow folder selection otherwise + if (Application::GetDesktopEnvironment() != "PLASMA5") + { + _dialog->setFileMode(QFileDialog::Directory); + } +} + +KDE5FilePicker::~KDE5FilePicker() +{ + delete _extraControls; + delete _dialog; +} + +void KDE5FilePicker::setTitle(const QString& title) { _dialog->setWindowTitle(title); } + +bool KDE5FilePicker::execute() +{ + if (!_filters.isEmpty()) + _dialog->setNameFilters(_filters); + if (!_currentFilter.isEmpty()) + _dialog->selectNameFilter(_currentFilter); + + _dialog->show(); + //block and wait for user input + return _dialog->exec() == QFileDialog::Accepted; +} + +void KDE5FilePicker::setMultiSelectionMode(bool multiSelect) +{ + if (_dialog->acceptMode() == QFileDialog::AcceptSave) + return; + + _dialog->setFileMode(multiSelect ? QFileDialog::ExistingFiles : QFileDialog::ExistingFile); +} + +void KDE5FilePicker::setDefaultName(const QString& name) { _dialog->selectFile(name); } + +void KDE5FilePicker::setDisplayDirectory(const QString& dir) +{ + _dialog->setDirectoryUrl(QUrl(dir)); +} + +QString KDE5FilePicker::getDisplayDirectory() const { return _dialog->directoryUrl().url(); } + +QList KDE5FilePicker::getSelectedFiles() const { return _dialog->selectedUrls(); } + +void KDE5FilePicker::appendFilter(const QString& title, const QString& filter) +{ + QString t = title; + QString f = filter; + // '/' need to be escaped else they are assumed to be mime types by kfiledialog + //see the docs + t.replace("/", "\\/"); + + // openoffice gives us filters separated by ';' qt dialogs just want space separated + f.replace(";", " "); + + // make sure "*.*" is not used as "all files" + f.replace("*.*", "*"); + + _filters << QStringLiteral("%1 (%2)").arg(t, f); + _titleToFilters[t] = _filters.constLast(); +} + +void KDE5FilePicker::setCurrentFilter(const QString& title) +{ + _currentFilter = _titleToFilters.value(title); +} + +QString KDE5FilePicker::getCurrentFilter() const +{ + QString filter = _titleToFilters.key(_dialog->selectedNameFilter()); + + //default if not found + if (filter.isEmpty()) + filter = "ODF Text Document (.odt)"; + + return filter; +} + +void KDE5FilePicker::setValue(sal_Int16 controlId, sal_Int16 /*nControlAction*/, bool value) +{ + if (_customWidgets.contains(controlId)) + { + QCheckBox* cb = dynamic_cast(_customWidgets.value(controlId)); + if (cb) + cb->setChecked(value); + } + else + qWarning() << "set value on unknown control" << controlId; +} + +bool KDE5FilePicker::getValue(sal_Int16 controlId, sal_Int16 /*nControlAction*/) const +{ + bool ret = false; + if (_customWidgets.contains(controlId)) + { + QCheckBox* cb = dynamic_cast(_customWidgets.value(controlId)); + if (cb) + ret = cb->isChecked(); + } + else + qWarning() << "get value on unknown control" << controlId; + + return ret; +} + +void KDE5FilePicker::enableControl(sal_Int16 controlId, bool enable) +{ + if (_customWidgets.contains(controlId)) + _customWidgets.value(controlId)->setEnabled(enable); + else + qWarning() << "enable on unknown control" << controlId; +} + +void KDE5FilePicker::setLabel(sal_Int16 controlId, const QString& label) +{ + if (_customWidgets.contains(controlId)) + { + QCheckBox* cb = dynamic_cast(_customWidgets.value(controlId)); + if (cb) + cb->setText(label); + } + else + qWarning() << "set label on unknown control" << controlId; +} + +QString KDE5FilePicker::getLabel(sal_Int16 controlId) const +{ + QString label; + if (_customWidgets.contains(controlId)) + { + QCheckBox* cb = dynamic_cast(_customWidgets.value(controlId)); + if (cb) + label = cb->text(); + } + else + qWarning() << "get label on unknown control" << controlId; + + return label; +} + +void KDE5FilePicker::addCheckBox(sal_Int16 controlId, const QString& label, bool hidden) +{ + auto resString = label; + resString.replace('~', '&'); + + auto widget = new QCheckBox(resString, _extraControls); + widget->setHidden(hidden); + if (!hidden) + { + _layout->addWidget(widget); + } + _customWidgets.insert(controlId, widget); +} + +void KDE5FilePicker::initialize(bool saveDialog) +{ + //default is opening + QFileDialog::AcceptMode operationMode + = saveDialog ? QFileDialog::AcceptSave : QFileDialog::AcceptOpen; + + _dialog->setAcceptMode(operationMode); + + if (saveDialog) + _dialog->setFileMode(QFileDialog::AnyFile); +} + +void KDE5FilePicker::setWinId(sal_uInt64 winId) { _winId = winId; } + +void KDE5FilePicker::setupCustomWidgets() +{ + // When using the platform-native Plasma/KDE5 file picker, we currently rely on KFileWidget + // being present to add the custom controls visible (s. 'eventFilter' method). + // Since this doesn't work for other desktop environments, use a non-native + // dialog there in order not to lose the custom controls and insert the custom + // widget in the layout returned by QFileDialog::layout() + // (which returns nullptr for native file dialogs) + if (Application::GetDesktopEnvironment() == "PLASMA5") + { + qApp->installEventFilter(this); + } + else + { + _dialog->setOption(QFileDialog::DontUseNativeDialog); + QGridLayout* pLayout = static_cast(_dialog->layout()); + assert(pLayout); + const int row = pLayout->rowCount(); + pLayout->addWidget(_extraControls, row, 1); + } +} + +bool KDE5FilePicker::eventFilter(QObject* o, QEvent* e) +{ + if (e->type() == QEvent::Show && o->isWidgetType()) + { + auto* w = static_cast(o); + if (!w->parentWidget() && w->isModal()) + { + /* + To replace when baseline will include kwindowsystem >= 5.62 with: + w->setAttribute(Qt::WA_NativeWindow, true); + KWindowSystem::setMainWindow(w->windowHandle(), _winId); + */ + SAL_WNODEPRECATED_DECLARATIONS_PUSH + KWindowSystem::setMainWindow(w, _winId); + SAL_WNODEPRECATED_DECLARATIONS_POP + if (auto* fileWidget = w->findChild({}, Qt::FindDirectChildrenOnly)) + { + fileWidget->setCustomWidget(_extraControls); + // remove event filter again; the only purpose was to set the custom widget here + qApp->removeEventFilter(this); + } + } + } + return QObject::eventFilter(o, e); +} + +#include + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/kde5_filepicker.hxx b/vcl/unx/gtk3_kde5/kde5_filepicker.hxx new file mode 100644 index 0000000000..74f94d2221 --- /dev/null +++ b/vcl/unx/gtk3_kde5/kde5_filepicker.hxx @@ -0,0 +1,110 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include +#include +#include +#include + +#include + +class QFileDialog; +class QWidget; +class QGridLayout; + +class KDE5FilePicker : public QObject +{ + Q_OBJECT +protected: + //the dialog to display + QFileDialog* _dialog; + + //running filter string to add to dialog + QStringList _filters; + // map of filter titles to full filter for selection + QHash _titleToFilters; + // string to set the current filter + QString _currentFilter; + + //mapping of SAL control ID's to created custom controls + QHash _customWidgets; + + //widget to contain extra custom controls + QWidget* _extraControls; + + //layout for extra custom controls + QGridLayout* _layout; + + sal_uInt64 _winId; + +public: + explicit KDE5FilePicker(QObject* parent = nullptr); + ~KDE5FilePicker() override; + + void enableFolderMode(); + + // XExecutableDialog functions + void setTitle(const QString& rTitle); + bool execute(); + + // XFilePicker functions + void setMultiSelectionMode(bool bMode); + void setDefaultName(const QString& rName); + void setDisplayDirectory(const QString& rDirectory); + QString getDisplayDirectory() const; + + // XFilterManager functions + void appendFilter(const QString& rTitle, const QString& rFilter); + void setCurrentFilter(const QString& rTitle); + QString getCurrentFilter() const; + + // XFilePickerControlAccess functions + void setValue(sal_Int16 nControlId, sal_Int16 nControlAction, bool rValue); + bool getValue(sal_Int16 nControlId, sal_Int16 nControlAction) const; + void enableControl(sal_Int16 nControlId, bool bEnable); + void setLabel(sal_Int16 nControlId, const QString& rLabel); + QString getLabel(sal_Int16 nControlId) const; + + // XFilePicker2 functions + QList getSelectedFiles() const; + + // XInitialization + void initialize(bool saveDialog); + + //add a custom control widget to the file dialog + void addCheckBox(sal_Int16 nControlId, const QString& label, bool hidden); + + void setWinId(sal_uInt64 winId); + +private: + Q_DISABLE_COPY(KDE5FilePicker) + // adds the custom controls to the dialog + void setupCustomWidgets(); + +protected: + bool eventFilter(QObject* watched, QEvent* event) override; + +Q_SIGNALS: + void filterChanged(); + void selectionChanged(); +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/kde5_filepicker_ipc.cxx b/vcl/unx/gtk3_kde5/kde5_filepicker_ipc.cxx new file mode 100644 index 0000000000..7f0bfff985 --- /dev/null +++ b/vcl/unx/gtk3_kde5/kde5_filepicker_ipc.cxx @@ -0,0 +1,374 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "kde5_filepicker_ipc.hxx" + +#include +#include +#include + +#include + +#include "kde5_filepicker.hxx" + +void readIpcArg(std::istream& stream, QString& string) +{ + const auto buffer = readIpcStringArg(stream); + string = QString::fromUtf8(buffer.data(), buffer.size()); +} + +void sendIpcArg(std::ostream& stream, const QString& string) +{ + const auto utf8 = string.toUtf8(); + sendIpcStringArg(stream, utf8.size(), utf8.data()); +} + +static void sendIpcArg(std::ostream& stream, const QStringList& list) +{ + stream << static_cast(list.size()) << ' '; + for (const auto& entry : list) + { + sendIpcArg(stream, entry); + } +} + +static void readCommandArgs(Commands command, QList& args) +{ + switch (command) + { + case Commands::SetTitle: + { + QString title; + readIpcArgs(std::cin, title); + args.append(title); + break; + } + case Commands::SetWinId: + { + sal_uIntPtr winId = 0; + readIpcArgs(std::cin, winId); + QVariant aWinIdVariant; + aWinIdVariant.setValue(winId); + args.append(aWinIdVariant); + break; + } + case Commands::SetMultiSelectionMode: + { + bool multiSelection = false; + readIpcArgs(std::cin, multiSelection); + args.append(multiSelection); + break; + } + case Commands::SetDefaultName: + { + QString name; + readIpcArgs(std::cin, name); + args.append(name); + break; + } + case Commands::SetDisplayDirectory: + { + QString dir; + readIpcArgs(std::cin, dir); + args.append(dir); + break; + } + case Commands::AppendFilter: + { + QString title, filter; + readIpcArgs(std::cin, title, filter); + args.append(title); + args.append(filter); + break; + } + case Commands::SetCurrentFilter: + { + QString title; + readIpcArgs(std::cin, title); + args.append(title); + break; + } + case Commands::SetValue: + { + sal_Int16 controlId = 0; + sal_Int16 nControlAction = 0; + bool value = false; + readIpcArgs(std::cin, controlId, nControlAction, value); + args.append(controlId); + args.append(nControlAction); + args.append(value); + break; + } + case Commands::GetValue: + { + sal_Int16 controlId = 0; + sal_Int16 nControlAction = 0; + readIpcArgs(std::cin, controlId, nControlAction); + args.append(controlId); + args.append(nControlAction); + break; + } + case Commands::EnableControl: + { + sal_Int16 controlId = 0; + bool enabled = false; + readIpcArgs(std::cin, controlId, enabled); + args.append(controlId); + args.append(enabled); + break; + } + case Commands::SetLabel: + { + sal_Int16 controlId = 0; + QString label; + readIpcArgs(std::cin, controlId, label); + args.append(controlId); + args.append(label); + break; + } + case Commands::GetLabel: + { + sal_Int16 controlId = 0; + readIpcArgs(std::cin, controlId); + args.append(controlId); + break; + } + case Commands::AddCheckBox: + { + sal_Int16 controlId = 0; + bool hidden = false; + QString label; + readIpcArgs(std::cin, controlId, hidden, label); + args.append(controlId); + args.append(hidden); + args.append(label); + break; + } + case Commands::Initialize: + { + bool saveDialog = false; + readIpcArgs(std::cin, saveDialog); + args.append(saveDialog); + break; + } + default: + { + // no extra parameters/arguments + break; + } + }; +} + +static void readCommands(FilePickerIpc* ipc) +{ + while (!std::cin.eof()) + { + uint64_t messageId = 0; + Commands command; + readIpcArgs(std::cin, messageId, command); + + // retrieve additional command-specific arguments + QList args; + readCommandArgs(command, args); + + emit ipc->commandReceived(messageId, command, args); + + // stop processing once 'Quit' command has been sent + if (command == Commands::Quit) + { + return; + } + } +} + +FilePickerIpc::FilePickerIpc(KDE5FilePicker* filePicker, QObject* parent) + : QObject(parent) + , m_filePicker(filePicker) +{ + // required to be able to pass those via signal/slot + qRegisterMetaType("uint64_t"); + qRegisterMetaType("Commands"); + + connect(this, &FilePickerIpc::commandReceived, this, &FilePickerIpc::handleCommand); + + // read IPC commands and their args in a separate thread, so this does not block everything else; + // 'commandReceived' signal is emitted every time a command and its args have been read; + // thread will run until the filepicker process is terminated + m_ipcReaderThread = std::make_unique(readCommands, this); +} + +FilePickerIpc::~FilePickerIpc() +{ + // join thread that reads commands + m_ipcReaderThread->join(); +}; + +bool FilePickerIpc::handleCommand(uint64_t messageId, Commands command, QList args) +{ + switch (command) + { + case Commands::SetTitle: + { + QString title = args.takeFirst().toString(); + m_filePicker->setTitle(title); + return true; + } + case Commands::SetWinId: + { + sal_uIntPtr winId = args.takeFirst().value(); + m_filePicker->setWinId(winId); + return true; + } + case Commands::Execute: + { + sendIpcArgs(std::cout, messageId, m_filePicker->execute()); + return true; + } + case Commands::SetMultiSelectionMode: + { + bool multiSelection = args.takeFirst().toBool(); + m_filePicker->setMultiSelectionMode(multiSelection); + return true; + } + case Commands::SetDefaultName: + { + QString name = args.takeFirst().toString(); + m_filePicker->setDefaultName(name); + return true; + } + case Commands::SetDisplayDirectory: + { + QString dir = args.takeFirst().toString(); + m_filePicker->setDisplayDirectory(dir); + return true; + } + case Commands::GetDisplayDirectory: + { + sendIpcArgs(std::cout, messageId, m_filePicker->getDisplayDirectory()); + return true; + } + case Commands::GetSelectedFiles: + { + QStringList files; + for (auto const& url_ : m_filePicker->getSelectedFiles()) + { + auto url = url_; + if (url.scheme() == QLatin1String("webdav") + || url.scheme() == QLatin1String("webdavs")) + { + // translate webdav and webdavs URLs into a format supported by LO + url.setScheme(QLatin1String("vnd.sun.star.") + url.scheme()); + } + else if (url.scheme() == QLatin1String("smb")) + { + // clear the user name - the GIO backend does not support this apparently + // when no username is available, it will ask for the password + url.setUserName({}); + } + files << url.toString(); + } + sendIpcArgs(std::cout, messageId, files); + return true; + } + case Commands::AppendFilter: + { + QString title = args.takeFirst().toString(); + QString filter = args.takeFirst().toString(); + m_filePicker->appendFilter(title, filter); + return true; + } + case Commands::SetCurrentFilter: + { + QString title = args.takeFirst().toString(); + m_filePicker->setCurrentFilter(title); + return true; + } + case Commands::GetCurrentFilter: + { + sendIpcArgs(std::cout, messageId, m_filePicker->getCurrentFilter()); + return true; + } + case Commands::SetValue: + { + sal_Int16 controlId = args.takeFirst().value(); + sal_Int16 nControlAction = args.takeFirst().value(); + bool value = args.takeFirst().toBool(); + m_filePicker->setValue(controlId, nControlAction, value); + return true; + } + case Commands::GetValue: + { + sal_Int16 controlId = args.takeFirst().value(); + sal_Int16 nControlAction = args.takeFirst().value(); + sendIpcArgs(std::cout, messageId, m_filePicker->getValue(controlId, nControlAction)); + return true; + } + case Commands::EnableControl: + { + sal_Int16 controlId = args.takeFirst().value(); + bool enabled = args.takeFirst().toBool(); + m_filePicker->enableControl(controlId, enabled); + return true; + } + case Commands::SetLabel: + { + sal_Int16 controlId = args.takeFirst().value(); + QString label = args.takeFirst().toString(); + m_filePicker->setLabel(controlId, label); + return true; + } + case Commands::GetLabel: + { + sal_Int16 controlId = args.takeFirst().value(); + sendIpcArgs(std::cout, messageId, m_filePicker->getLabel(controlId)); + return true; + } + case Commands::AddCheckBox: + { + sal_Int16 controlId = args.takeFirst().value(); + bool hidden = args.takeFirst().toBool(); + QString label = args.takeFirst().toString(); + m_filePicker->addCheckBox(controlId, label, hidden); + return true; + } + case Commands::Initialize: + { + bool saveDialog = args.takeFirst().toBool(); + m_filePicker->initialize(saveDialog); + return true; + } + case Commands::EnablePickFolderMode: + { + m_filePicker->enableFolderMode(); + return true; + } + case Commands::Quit: + { + QCoreApplication::quit(); + return false; + } + } + qWarning() << "unhandled command " << static_cast(command); + QCoreApplication::exit(1); + return false; +} + +#include + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/kde5_filepicker_ipc.hxx b/vcl/unx/gtk3_kde5/kde5_filepicker_ipc.hxx new file mode 100644 index 0000000000..fa9be696c5 --- /dev/null +++ b/vcl/unx/gtk3_kde5/kde5_filepicker_ipc.hxx @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include +#include + +#include + +#include "filepicker_ipc_commands.hxx" + +class KDE5FilePicker; +class WinIdEmbedder; +class QSocketNotifier; + +class FilePickerIpc : public QObject +{ + Q_OBJECT +public: + explicit FilePickerIpc(KDE5FilePicker* filePicker, QObject* parent = nullptr); + ~FilePickerIpc() override; + +private: + KDE5FilePicker* m_filePicker = nullptr; + std::unique_ptr m_ipcReaderThread; + +private Q_SLOTS: + bool handleCommand(uint64_t messageId, Commands command, QList args); + +Q_SIGNALS: + bool commandReceived(uint64_t messageId, Commands command, QList args); +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk3_kde5/kde5_lo_filepicker_main.cxx b/vcl/unx/gtk3_kde5/kde5_lo_filepicker_main.cxx new file mode 100644 index 0000000000..4e73e9ee45 --- /dev/null +++ b/vcl/unx/gtk3_kde5/kde5_lo_filepicker_main.cxx @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "kde5_filepicker.hxx" +#include "kde5_filepicker_ipc.hxx" + +#include +#include + +#include + +int main(int argc, char** argv) +{ + QApplication::setOrganizationName("LibreOffice"); + QApplication::setOrganizationDomain("libreoffice.org"); + QApplication::setApplicationName(QStringLiteral("lo_kde5filepicker")); + QApplication::setQuitOnLastWindowClosed(false); + QApplication::setApplicationVersion(LIBO_VERSION_DOTTED); + + QApplication app(argc, argv); + + QCommandLineParser parser; + parser.setApplicationDescription( + QObject::tr("Helper executable for LibreOffice KDE/Plasma integration.\n" + "Do not run this executable directly. Rather, use it indirectly via " + "the gtk3_kde5 VCL plugin (SAL_USE_VCLPLUGIN=gtk3_kde5).")); + parser.addVersionOption(); + parser.addHelpOption(); + parser.process(app); + + KDE5FilePicker filePicker; + FilePickerIpc ipc(&filePicker); + + return QApplication::exec(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/a11y.cxx b/vcl/unx/gtk4/a11y.cxx new file mode 100644 index 0000000000..c8103471b0 --- /dev/null +++ b/vcl/unx/gtk4/a11y.cxx @@ -0,0 +1,851 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if GTK_CHECK_VERSION(4, 9, 0) + +#include "a11y.hxx" + +#define OOO_TYPE_FIXED (ooo_fixed_get_type()) +#define OOO_FIXED(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), OOO_TYPE_FIXED, OOoFixed)) +// #define OOO_IS_FIXED(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), OOO_TYPE_FIXED)) + +struct OOoFixed +{ + GtkFixed parent_instance; + GtkATContext* at_context; +}; + +struct OOoFixedClass +{ + GtkFixedClass parent_class; +}; + +static GtkAccessibleRole +map_accessible_role(const css::uno::Reference& rAccessible) +{ + GtkAccessibleRole eRole(GTK_ACCESSIBLE_ROLE_WIDGET); + + if (rAccessible.is()) + { + css::uno::Reference xContext( + rAccessible->getAccessibleContext()); + // https://w3c.github.io/core-aam/#mapping_role + // https://hg.mozilla.org/mozilla-central/file/tip/accessible/base/RoleMap.h + // https://gitlab.gnome.org/GNOME/gtk/-/blob/main/gtk/a11y/gtkatspiutils.c + switch (xContext->getAccessibleRole()) + { + case css::accessibility::AccessibleRole::ALERT: + case css::accessibility::AccessibleRole::NOTIFICATION: + eRole = GTK_ACCESSIBLE_ROLE_ALERT; + break; + case css::accessibility::AccessibleRole::BLOCK_QUOTE: +#if GTK_CHECK_VERSION(4, 13, 4) + eRole = GTK_ACCESSIBLE_ROLE_BLOCK_QUOTE; +#else + eRole = GTK_ACCESSIBLE_ROLE_GROUP; +#endif + break; + case css::accessibility::AccessibleRole::CAPTION: + eRole = GTK_ACCESSIBLE_ROLE_CAPTION; + break; + case css::accessibility::AccessibleRole::COLUMN_HEADER: + eRole = GTK_ACCESSIBLE_ROLE_COLUMN_HEADER; + break; + case css::accessibility::AccessibleRole::COMBO_BOX: + eRole = GTK_ACCESSIBLE_ROLE_COMBO_BOX; + break; + case css::accessibility::AccessibleRole::DIALOG: + case css::accessibility::AccessibleRole::FILE_CHOOSER: + eRole = GTK_ACCESSIBLE_ROLE_DIALOG; + break; + case css::accessibility::AccessibleRole::FOOTNOTE: + case css::accessibility::AccessibleRole::NOTE: + eRole = GTK_ACCESSIBLE_ROLE_NOTE; + break; + case css::accessibility::AccessibleRole::FORM: + eRole = GTK_ACCESSIBLE_ROLE_FORM; + break; + case css::accessibility::AccessibleRole::HEADING: + eRole = GTK_ACCESSIBLE_ROLE_HEADING; + break; + case css::accessibility::AccessibleRole::HYPER_LINK: + eRole = GTK_ACCESSIBLE_ROLE_LINK; + break; + case css::accessibility::AccessibleRole::PANEL: + eRole = GTK_ACCESSIBLE_ROLE_GROUP; + break; + case css::accessibility::AccessibleRole::ROOT_PANE: + eRole = GTK_ACCESSIBLE_ROLE_GROUP; + break; + case css::accessibility::AccessibleRole::MENU_BAR: + eRole = GTK_ACCESSIBLE_ROLE_MENU_BAR; + break; + case css::accessibility::AccessibleRole::STATUS_BAR: + eRole = GTK_ACCESSIBLE_ROLE_STATUS; + break; + case css::accessibility::AccessibleRole::MENU: + case css::accessibility::AccessibleRole::POPUP_MENU: + eRole = GTK_ACCESSIBLE_ROLE_MENU; + break; + case css::accessibility::AccessibleRole::SPLIT_PANE: + eRole = GTK_ACCESSIBLE_ROLE_GROUP; + break; + case css::accessibility::AccessibleRole::TOOL_BAR: + eRole = GTK_ACCESSIBLE_ROLE_TOOLBAR; + break; + case css::accessibility::AccessibleRole::LABEL: + case css::accessibility::AccessibleRole::STATIC: + eRole = GTK_ACCESSIBLE_ROLE_LABEL; + break; + case css::accessibility::AccessibleRole::LIST: + eRole = GTK_ACCESSIBLE_ROLE_LIST; + break; + case css::accessibility::AccessibleRole::LIST_ITEM: + eRole = GTK_ACCESSIBLE_ROLE_LIST_ITEM; + break; + case css::accessibility::AccessibleRole::MENU_ITEM: + eRole = GTK_ACCESSIBLE_ROLE_MENU_ITEM; + break; + case css::accessibility::AccessibleRole::SEPARATOR: + eRole = GTK_ACCESSIBLE_ROLE_SEPARATOR; + break; + case css::accessibility::AccessibleRole::CHECK_BOX: + eRole = GTK_ACCESSIBLE_ROLE_CHECKBOX; + break; + case css::accessibility::AccessibleRole::CHECK_MENU_ITEM: + eRole = GTK_ACCESSIBLE_ROLE_MENU_ITEM_CHECKBOX; + break; + case css::accessibility::AccessibleRole::RADIO_MENU_ITEM: + eRole = GTK_ACCESSIBLE_ROLE_MENU_ITEM_RADIO; + break; + case css::accessibility::AccessibleRole::DOCUMENT: + case css::accessibility::AccessibleRole::DOCUMENT_PRESENTATION: + case css::accessibility::AccessibleRole::DOCUMENT_SPREADSHEET: + case css::accessibility::AccessibleRole::DOCUMENT_TEXT: + eRole = GTK_ACCESSIBLE_ROLE_DOCUMENT; + break; + case css::accessibility::AccessibleRole::ROW_HEADER: + eRole = GTK_ACCESSIBLE_ROLE_ROW_HEADER; + break; + case css::accessibility::AccessibleRole::RULER: + eRole = GTK_ACCESSIBLE_ROLE_WIDGET; + break; + case css::accessibility::AccessibleRole::PARAGRAPH: +#if GTK_CHECK_VERSION(4, 13, 1) + eRole = GTK_ACCESSIBLE_ROLE_PARAGRAPH; +#else + eRole = GTK_ACCESSIBLE_ROLE_GROUP; +#endif + break; + case css::accessibility::AccessibleRole::FILLER: + eRole = GTK_ACCESSIBLE_ROLE_GENERIC; + break; + case css::accessibility::AccessibleRole::PUSH_BUTTON: + case css::accessibility::AccessibleRole::BUTTON_DROPDOWN: + case css::accessibility::AccessibleRole::BUTTON_MENU: + eRole = GTK_ACCESSIBLE_ROLE_BUTTON; + break; + case css::accessibility::AccessibleRole::TOGGLE_BUTTON: + eRole = GTK_ACCESSIBLE_ROLE_TOGGLE_BUTTON; + break; + case css::accessibility::AccessibleRole::TABLE: + eRole = GTK_ACCESSIBLE_ROLE_TABLE; + break; + case css::accessibility::AccessibleRole::TABLE_CELL: + eRole = GTK_ACCESSIBLE_ROLE_CELL; + break; + case css::accessibility::AccessibleRole::PAGE_TAB: + eRole = GTK_ACCESSIBLE_ROLE_TAB; + break; + case css::accessibility::AccessibleRole::PAGE_TAB_LIST: + eRole = GTK_ACCESSIBLE_ROLE_TAB_LIST; + break; + case css::accessibility::AccessibleRole::PROGRESS_BAR: + eRole = GTK_ACCESSIBLE_ROLE_PROGRESS_BAR; + break; + case css::accessibility::AccessibleRole::RADIO_BUTTON: + eRole = GTK_ACCESSIBLE_ROLE_RADIO; + break; + case css::accessibility::AccessibleRole::SCROLL_BAR: + eRole = GTK_ACCESSIBLE_ROLE_SCROLLBAR; + break; + case css::accessibility::AccessibleRole::SLIDER: + eRole = GTK_ACCESSIBLE_ROLE_SLIDER; + break; + case css::accessibility::AccessibleRole::SPIN_BOX: + eRole = GTK_ACCESSIBLE_ROLE_SPIN_BUTTON; + break; + case css::accessibility::AccessibleRole::TEXT: + case css::accessibility::AccessibleRole::PASSWORD_TEXT: + eRole = GTK_ACCESSIBLE_ROLE_TEXT_BOX; + break; + case css::accessibility::AccessibleRole::TOOL_TIP: + eRole = GTK_ACCESSIBLE_ROLE_TOOLTIP; + break; + case css::accessibility::AccessibleRole::TREE: + eRole = GTK_ACCESSIBLE_ROLE_TREE; + break; + case css::accessibility::AccessibleRole::TREE_ITEM: + eRole = GTK_ACCESSIBLE_ROLE_TREE_ITEM; + break; + case css::accessibility::AccessibleRole::TREE_TABLE: + eRole = GTK_ACCESSIBLE_ROLE_TREE_GRID; + break; + case css::accessibility::AccessibleRole::GRAPHIC: + case css::accessibility::AccessibleRole::ICON: + case css::accessibility::AccessibleRole::SHAPE: + eRole = GTK_ACCESSIBLE_ROLE_IMG; + break; + default: + SAL_WARN("vcl.gtk", + "unmapped GtkAccessibleRole: " << xContext->getAccessibleRole()); + break; + } + } + + return eRole; +} + +static css::uno::Reference get_uno_accessible(GtkWidget* pWidget) +{ + GtkWidget* pTopLevel = widget_get_toplevel(pWidget); + if (!pTopLevel) + return nullptr; + + GtkSalFrame* pFrame = GtkSalFrame::getFromWindow(pTopLevel); + if (!pFrame) + return nullptr; + + vcl::Window* pFrameWindow = pFrame->GetWindow(); + if (!pFrameWindow) + return nullptr; + + vcl::Window* pWindow = pFrameWindow; + + // skip accessible objects already exposed by the frame objects + if (WindowType::BORDERWINDOW == pWindow->GetType()) + pWindow = pFrameWindow->GetAccessibleChildWindow(0); + + if (!pWindow) + return nullptr; + + return pWindow->GetAccessible(); +} + +/** + * Based on the states set in xContext, set the corresponding Gtk states/properties + * in pGtkAccessible. + */ +static void applyStates(GtkAccessible* pGtkAccessible, + css::uno::Reference& xContext) +{ + assert(pGtkAccessible); + + if (!xContext.is()) + return; + + // Gtk differentiates between GtkAccessibleState and GtkAccessibleProperty + // (both handled here) and GtkAccessiblePlatformState (handled in + // 'lo_accessible_get_platform_state') + const sal_Int64 nStates = xContext->getAccessibleStateSet(); + gtk_accessible_update_property( + pGtkAccessible, GTK_ACCESSIBLE_PROPERTY_MODAL, + bool(nStates & com::sun::star::accessibility::AccessibleStateType::MODAL), + GTK_ACCESSIBLE_PROPERTY_MULTI_LINE, + bool(nStates & com::sun::star::accessibility::AccessibleStateType::MULTI_LINE), + GTK_ACCESSIBLE_PROPERTY_MULTI_SELECTABLE, + bool(nStates & com::sun::star::accessibility::AccessibleStateType::MULTI_SELECTABLE), + GTK_ACCESSIBLE_PROPERTY_READ_ONLY, + bool(!(nStates & com::sun::star::accessibility::AccessibleStateType::EDITABLE)), -1); + if (nStates & com::sun::star::accessibility::AccessibleStateType::HORIZONTAL) + { + gtk_accessible_update_property(pGtkAccessible, GTK_ACCESSIBLE_PROPERTY_ORIENTATION, + GTK_ORIENTATION_HORIZONTAL, -1); + } + else if (nStates & com::sun::star::accessibility::AccessibleStateType::VERTICAL) + { + gtk_accessible_update_property(pGtkAccessible, GTK_ACCESSIBLE_PROPERTY_ORIENTATION, + GTK_ORIENTATION_VERTICAL, -1); + } + + gtk_accessible_update_state( + pGtkAccessible, GTK_ACCESSIBLE_STATE_BUSY, + bool(nStates & com::sun::star::accessibility::AccessibleStateType::BUSY), + GTK_ACCESSIBLE_STATE_DISABLED, + bool(!(nStates & com::sun::star::accessibility::AccessibleStateType::ENABLED)), + GTK_ACCESSIBLE_STATE_EXPANDED, + bool(nStates & com::sun::star::accessibility::AccessibleStateType::EXPANDED), + GTK_ACCESSIBLE_STATE_SELECTED, + bool(nStates & com::sun::star::accessibility::AccessibleStateType::SELECTED), -1); + + // when explicitly setting any value for GTK_ACCESSIBLE_STATE_CHECKED, + // Gtk will also report ATSPI_STATE_CHECKABLE on the AT-SPI layer + if (nStates & com::sun::star::accessibility::AccessibleStateType::CHECKABLE) + { + GtkAccessibleTristate eState = GTK_ACCESSIBLE_TRISTATE_FALSE; + if (nStates & com::sun::star::accessibility::AccessibleStateType::INDETERMINATE) + eState = GTK_ACCESSIBLE_TRISTATE_MIXED; + else if (nStates & com::sun::star::accessibility::AccessibleStateType::CHECKED) + eState = GTK_ACCESSIBLE_TRISTATE_TRUE; + gtk_accessible_update_state(pGtkAccessible, GTK_ACCESSIBLE_STATE_CHECKED, eState, -1); + } + + const sal_Int16 nRole = xContext->getAccessibleRole(); + if (nRole == com::sun::star::accessibility::AccessibleRole::TOGGLE_BUTTON) + { + GtkAccessibleTristate eState = GTK_ACCESSIBLE_TRISTATE_FALSE; + if (nStates & com::sun::star::accessibility::AccessibleStateType::INDETERMINATE) + eState = GTK_ACCESSIBLE_TRISTATE_MIXED; + else if (nStates & com::sun::star::accessibility::AccessibleStateType::PRESSED) + eState = GTK_ACCESSIBLE_TRISTATE_TRUE; + gtk_accessible_update_state(pGtkAccessible, GTK_ACCESSIBLE_STATE_PRESSED, eState, -1); + } +} + +static void applyObjectAttribute(GtkAccessible* pGtkAccessible, std::u16string_view rName, + const OUString& rValue) +{ + assert(pGtkAccessible); + + if (rName == u"colindextext") + { + gtk_accessible_update_relation(pGtkAccessible, GTK_ACCESSIBLE_RELATION_COL_INDEX_TEXT, + rValue.toUtf8().getStr(), -1); + } + else if (rName == u"level") + { + const int nLevel = o3tl::toInt32(rValue); + gtk_accessible_update_property(pGtkAccessible, GTK_ACCESSIBLE_PROPERTY_LEVEL, nLevel, -1); + } + else if (rName == u"rowindextext") + { + gtk_accessible_update_relation(pGtkAccessible, GTK_ACCESSIBLE_RELATION_ROW_INDEX_TEXT, + rValue.toUtf8().getStr(), -1); + } +} + +/** + * Based on the object attributes set for xContext, set the corresponding Gtk equivalents + * in pGtkAccessible, where applicable. + */ +static void +applyObjectAttributes(GtkAccessible* pGtkAccessible, + css::uno::Reference& xContext) +{ + assert(pGtkAccessible); + + css::uno::Reference xAttributes( + xContext, css::uno::UNO_QUERY); + if (!xAttributes.is()) + return; + + OUString sAttrs; + xAttributes->getExtendedAttributes() >>= sAttrs; + + sal_Int32 nIndex = 0; + do + { + const OUString sAttribute = sAttrs.getToken(0, ';', nIndex); + sal_Int32 nColonPos = 0; + const OUString sName = sAttribute.getToken(0, ':', nColonPos); + const OUString sValue = sAttribute.getToken(0, ':', nColonPos); + assert(nColonPos == -1 + && "Too many colons in attribute that should have \"name:value\" syntax"); + + applyObjectAttribute(pGtkAccessible, sName, sValue); + } while (nIndex >= 0); +} + +#define LO_TYPE_ACCESSIBLE (lo_accessible_get_type()) +#define LO_ACCESSIBLE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), LO_TYPE_ACCESSIBLE, LoAccessible)) +// #define LO_IS_ACCESSIBLE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), LO_TYPE_ACCESSIBLE)) + +struct LoAccessible +{ + GObject parent_instance; + GdkDisplay* display; + GtkAccessible* parent; + GtkATContext* at_context; + css::uno::Reference uno_accessible; +}; + +struct LoAccessibleClass +{ + GObjectClass parent_class; +}; + +#if GTK_CHECK_VERSION(4, 10, 0) +static void lo_accessible_range_init(GtkAccessibleRangeInterface* iface); +static gboolean lo_accessible_range_set_current_value(GtkAccessibleRange* self, double fNewValue); +#endif + +extern "C" { +typedef GType (*GetGIfaceType)(); +} +const struct +{ + const char* name; + GInterfaceInitFunc const aInit; + GetGIfaceType const aGetGIfaceType; + const css::uno::Type& (*aGetUnoType)(); +} TYPE_TABLE[] = { +#if GTK_CHECK_VERSION(4, 10, 0) + { "Value", reinterpret_cast(lo_accessible_range_init), + gtk_accessible_range_get_type, cppu::UnoType::get } +#endif +}; + +static bool isOfType(css::uno::XInterface* xInterface, const css::uno::Type& rType) +{ + if (!xInterface) + return false; + + try + { + css::uno::Any aRet = xInterface->queryInterface(rType); + const bool bIs = (typelib_TypeClass_INTERFACE == aRet.pType->eTypeClass) + && (aRet.pReserved != nullptr); + return bIs; + } + catch (const css::uno::Exception&) + { + return false; + } +} + +static GType ensureTypeFor(css::uno::XInterface* xAccessible) +{ + OStringBuffer aTypeNameBuf("OOoGtkAccessibleObj"); + std::vector bTypes(std::size(TYPE_TABLE), false); + for (size_t i = 0; i < std::size(TYPE_TABLE); i++) + { + if (isOfType(xAccessible, TYPE_TABLE[i].aGetUnoType())) + { + aTypeNameBuf.append(TYPE_TABLE[i].name); + bTypes[i] = true; + } + } + + const OString aTypeName = aTypeNameBuf.makeStringAndClear(); + GType nType = g_type_from_name(aTypeName.getStr()); + if (nType != G_TYPE_INVALID) + return nType; + + GTypeInfo aTypeInfo = { sizeof(LoAccessibleClass), nullptr, nullptr, nullptr, nullptr, nullptr, + sizeof(LoAccessible), 0, nullptr, nullptr }; + nType + = g_type_register_static(LO_TYPE_ACCESSIBLE, aTypeName.getStr(), &aTypeInfo, GTypeFlags(0)); + + for (size_t i = 0; i < std::size(TYPE_TABLE); i++) + { + if (bTypes[i]) + { + GInterfaceInfo aIfaceInfo = { nullptr, nullptr, nullptr }; + aIfaceInfo.interface_init = TYPE_TABLE[i].aInit; + g_type_add_interface_static(nType, TYPE_TABLE[i].aGetGIfaceType(), &aIfaceInfo); + } + } + return nType; +} + +enum +{ + CHILD_PROP_0, + LAST_CHILD_PROP, + + PROP_ACCESSIBLE_ROLE +}; + +static void lo_accessible_get_property(GObject* object, guint property_id, GValue* value, + GParamSpec* pspec) +{ + LoAccessible* accessible = LO_ACCESSIBLE(object); + + switch (property_id) + { + case PROP_ACCESSIBLE_ROLE: + { + GtkAccessibleRole eRole(map_accessible_role(accessible->uno_accessible)); + g_value_set_enum(value, eRole); + break; + } + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void lo_accessible_set_property(GObject* object, guint property_id, const GValue* /*value*/, + GParamSpec* pspec) +{ + // LoAccessible* accessible = LO_ACCESSIBLE(object); + + switch (property_id) + { + case PROP_ACCESSIBLE_ROLE: + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static GtkAccessible* lo_accessible_get_accessible_parent(GtkAccessible* accessible) +{ + LoAccessible* lo_accessible = LO_ACCESSIBLE(accessible); + if (!lo_accessible->parent) + return nullptr; + return GTK_ACCESSIBLE(g_object_ref(lo_accessible->parent)); +} + +static GtkATContext* lo_accessible_get_at_context(GtkAccessible* self) +{ + LoAccessible* pAccessible = LO_ACCESSIBLE(self); + + GtkAccessibleRole eRole = map_accessible_role(pAccessible->uno_accessible); + + if (!pAccessible->at_context + || gtk_at_context_get_accessible_role(pAccessible->at_context) != eRole) + { + pAccessible->at_context = gtk_at_context_create(eRole, self, pAccessible->display); + if (!pAccessible->at_context) + return nullptr; + } + + return g_object_ref(pAccessible->at_context); +} + +static GtkAccessible* lo_accessible_get_first_accessible_child(GtkAccessible* self); + +static GtkAccessible* lo_accessible_get_next_accessible_sibling(GtkAccessible* self); + +static gboolean lo_accessible_get_platform_state(GtkAccessible* self, + GtkAccessiblePlatformState state); + +static gboolean lo_accessible_get_bounds(GtkAccessible* accessible, int* x, int* y, int* width, + int* height); + +static void lo_accessible_accessible_init(GtkAccessibleInterface* iface) +{ + iface->get_accessible_parent = lo_accessible_get_accessible_parent; + iface->get_at_context = lo_accessible_get_at_context; + iface->get_bounds = lo_accessible_get_bounds; + iface->get_first_accessible_child = lo_accessible_get_first_accessible_child; + iface->get_next_accessible_sibling = lo_accessible_get_next_accessible_sibling; + iface->get_platform_state = lo_accessible_get_platform_state; +} + +#if GTK_CHECK_VERSION(4, 10, 0) +static void lo_accessible_range_init(GtkAccessibleRangeInterface* iface) +{ + iface->set_current_value = lo_accessible_range_set_current_value; +} +#endif + +G_DEFINE_TYPE_WITH_CODE(LoAccessible, lo_accessible, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(GTK_TYPE_ACCESSIBLE, lo_accessible_accessible_init)) + +static void lo_accessible_class_init(LoAccessibleClass* klass) +{ + GObjectClass* object_class = G_OBJECT_CLASS(klass); + + // object_class->finalize = lo_accessible_finalize; + // object_class->dispose = lo_accessible_dispose; + object_class->get_property = lo_accessible_get_property; + object_class->set_property = lo_accessible_set_property; + // object_class->constructed = lo_accessible_constructed; + + // g_object_class_install_properties(object_class, LAST_CHILD_PROP, lo_accessible_props); + g_object_class_override_property(object_class, PROP_ACCESSIBLE_ROLE, "accessible-role"); +} + +static LoAccessible* +lo_accessible_new(GdkDisplay* pDisplay, GtkAccessible* pParent, + const css::uno::Reference& rAccessible) +{ + assert(rAccessible.is()); + + GType nType = ensureTypeFor(rAccessible.get()); + LoAccessible* ret = LO_ACCESSIBLE(g_object_new(nType, nullptr)); + ret->display = pDisplay; + ret->parent = pParent; + ret->uno_accessible = rAccessible; + + css::uno::Reference xContext( + ret->uno_accessible->getAccessibleContext()); + assert(xContext.is() && "No accessible context"); + + GtkAccessible* pGtkAccessible = GTK_ACCESSIBLE(ret); + + // set accessible name and description + gtk_accessible_update_property(pGtkAccessible, GTK_ACCESSIBLE_PROPERTY_LABEL, + xContext->getAccessibleName().toUtf8().getStr(), + GTK_ACCESSIBLE_PROPERTY_DESCRIPTION, + xContext->getAccessibleDescription().toUtf8().getStr(), -1); + + applyStates(pGtkAccessible, xContext); + + applyObjectAttributes(GTK_ACCESSIBLE(ret), xContext); + + // set values from XAccessibleValue interface if that's implemented + css::uno::Reference xAccessibleValue(xContext, + css::uno::UNO_QUERY); + if (xAccessibleValue.is()) + { + double fCurrentValue = 0, fMinValue = 0, fMaxValue = 0; + xAccessibleValue->getCurrentValue() >>= fCurrentValue; + xAccessibleValue->getMinimumValue() >>= fMinValue; + xAccessibleValue->getMaximumValue() >>= fMaxValue; + gtk_accessible_update_property(GTK_ACCESSIBLE(ret), GTK_ACCESSIBLE_PROPERTY_VALUE_NOW, + fCurrentValue, GTK_ACCESSIBLE_PROPERTY_VALUE_MIN, fMinValue, + GTK_ACCESSIBLE_PROPERTY_VALUE_MAX, fMaxValue, -1); + } + + return ret; +} + +static gboolean lo_accessible_get_bounds(GtkAccessible* self, int* x, int* y, int* width, + int* height) +{ + LoAccessible* pAccessible = LO_ACCESSIBLE(self); + + if (!pAccessible->uno_accessible) + return false; + + css::uno::Reference xContext( + pAccessible->uno_accessible->getAccessibleContext()); + css::uno::Reference xAccessibleComponent( + xContext, css::uno::UNO_QUERY); + if (!xAccessibleComponent) + return false; + + css::awt::Rectangle aBounds = xAccessibleComponent->getBounds(); + *x = aBounds.X; + *y = aBounds.Y; + *width = aBounds.Width; + *height = aBounds.Height; + return true; +} + +static GtkAccessible* lo_accessible_get_first_accessible_child(GtkAccessible* self) +{ + LoAccessible* pAccessible = LO_ACCESSIBLE(self); + + if (!pAccessible->uno_accessible) + return nullptr; + + css::uno::Reference xContext( + pAccessible->uno_accessible->getAccessibleContext()); + if (!xContext->getAccessibleChildCount()) + return nullptr; + css::uno::Reference xFirstChild( + xContext->getAccessibleChild(0)); + if (!xFirstChild) + return nullptr; + + LoAccessible* child_accessible = lo_accessible_new(pAccessible->display, self, xFirstChild); + return GTK_ACCESSIBLE(g_object_ref(child_accessible)); +} + +static GtkAccessible* lo_accessible_get_next_accessible_sibling(GtkAccessible* self) +{ + LoAccessible* pAccessible = LO_ACCESSIBLE(self); + + if (!pAccessible->uno_accessible) + return nullptr; + + css::uno::Reference xContext( + pAccessible->uno_accessible->getAccessibleContext()); + sal_Int64 nThisChildIndex = xContext->getAccessibleIndexInParent(); + assert(nThisChildIndex != -1); + sal_Int64 nNextChildIndex = nThisChildIndex + 1; + + css::uno::Reference xParent = xContext->getAccessibleParent(); + css::uno::Reference xParentContext( + xParent->getAccessibleContext()); + if (nNextChildIndex >= xParentContext->getAccessibleChildCount()) + return nullptr; + css::uno::Reference xNextChild( + xParentContext->getAccessibleChild(nNextChildIndex)); + if (!xNextChild) + return nullptr; + + LoAccessible* child_accessible + = lo_accessible_new(pAccessible->display, pAccessible->parent, xNextChild); + return GTK_ACCESSIBLE(g_object_ref(child_accessible)); +} + +static gboolean lo_accessible_get_platform_state(GtkAccessible* self, + GtkAccessiblePlatformState state) +{ + LoAccessible* pAccessible = LO_ACCESSIBLE(self); + + if (!pAccessible->uno_accessible) + return false; + + css::uno::Reference xContext( + pAccessible->uno_accessible->getAccessibleContext()); + sal_Int64 nStateSet = xContext->getAccessibleStateSet(); + + switch (state) + { + case GTK_ACCESSIBLE_PLATFORM_STATE_FOCUSABLE: + return (nStateSet & css::accessibility::AccessibleStateType::FOCUSABLE) != 0; + case GTK_ACCESSIBLE_PLATFORM_STATE_FOCUSED: + return (nStateSet & css::accessibility::AccessibleStateType::FOCUSED) != 0; + case GTK_ACCESSIBLE_PLATFORM_STATE_ACTIVE: + return (nStateSet & css::accessibility::AccessibleStateType::ACTIVE) != 0; + } + + return false; +} + +#if GTK_CHECK_VERSION(4, 10, 0) +static gboolean lo_accessible_range_set_current_value(GtkAccessibleRange* self, double fNewValue) +{ + // return 'true' in any case, since otherwise no proper AT-SPI DBus reply gets sent + // and the app crashes, s. + // https://gitlab.gnome.org/GNOME/gtk/-/issues/6150 + // https://gitlab.gnome.org/GNOME/gtk/-/commit/0dbd2bd09eff8c9233e45338a05daf2a835529ab + + LoAccessible* pAccessible = LO_ACCESSIBLE(self); + if (!pAccessible->uno_accessible) + return true; + + css::uno::Reference xContext( + pAccessible->uno_accessible->getAccessibleContext()); + + css::uno::Reference xValue(xContext, css::uno::UNO_QUERY); + if (!xValue.is()) + return true; + + // Different types of numerical values for XAccessibleValue are possible. + // If current value has an integer type, also use that for the new value, to make + // sure underlying implementations expecting that can handle the value properly. + const css::uno::Any aCurrentValue = xValue->getCurrentValue(); + if (aCurrentValue.getValueTypeClass() == css::uno::TypeClass::TypeClass_LONG) + { + const sal_Int32 nValue = std::round(fNewValue); + xValue->setCurrentValue(css::uno::Any(nValue)); + return true; + } + else if (aCurrentValue.getValueTypeClass() == css::uno::TypeClass::TypeClass_HYPER) + { + const sal_Int64 nValue = std::round(fNewValue); + xValue->setCurrentValue(css::uno::Any(nValue)); + return true; + } + + css::uno::Any aValue; + aValue <<= fNewValue; + xValue->setCurrentValue(aValue); + return true; +} +#endif + +static void lo_accessible_init(LoAccessible* /*iface*/) {} + +static GtkATContext* get_at_context(GtkAccessible* self) +{ + OOoFixed* pFixed = OOO_FIXED(self); + + css::uno::Reference xAccessible( + get_uno_accessible(GTK_WIDGET(pFixed))); + GtkAccessibleRole eRole = map_accessible_role(xAccessible); + + if (!pFixed->at_context || gtk_at_context_get_accessible_role(pFixed->at_context) != eRole) + { + // if (pFixed->at_context) + // g_clear_object(&pFixed->at_context); + + pFixed->at_context + = gtk_at_context_create(eRole, self, gtk_widget_get_display(GTK_WIDGET(pFixed))); + if (!pFixed->at_context) + return nullptr; + } + + return g_object_ref(pFixed->at_context); +} + +#if 0 +gboolean get_platform_state(GtkAccessible* self, GtkAccessiblePlatformState state) +{ + return false; +} +#endif + +static gboolean get_bounds(GtkAccessible* accessible, int* x, int* y, int* width, int* height) +{ + OOoFixed* pFixed = OOO_FIXED(accessible); + css::uno::Reference xAccessible( + get_uno_accessible(GTK_WIDGET(pFixed))); + css::uno::Reference xContext( + xAccessible->getAccessibleContext()); + css::uno::Reference xAccessibleComponent( + xContext, css::uno::UNO_QUERY); + + css::awt::Rectangle aBounds = xAccessibleComponent->getBounds(); + *x = aBounds.X; + *y = aBounds.Y; + *width = aBounds.Width; + *height = aBounds.Height; + return true; +} + +static GtkAccessible* get_first_accessible_child(GtkAccessible* accessible) +{ + OOoFixed* pFixed = OOO_FIXED(accessible); + css::uno::Reference xAccessible( + get_uno_accessible(GTK_WIDGET(pFixed))); + if (!xAccessible) + return nullptr; + css::uno::Reference xContext( + xAccessible->getAccessibleContext()); + if (!xContext->getAccessibleChildCount()) + return nullptr; + css::uno::Reference xFirstChild( + xContext->getAccessibleChild(0)); + LoAccessible* child_accessible + = lo_accessible_new(gtk_widget_get_display(GTK_WIDGET(pFixed)), accessible, xFirstChild); + return GTK_ACCESSIBLE(g_object_ref(child_accessible)); +} + +static void ooo_fixed_accessible_init(GtkAccessibleInterface* iface) +{ + GtkAccessibleInterface* parent_iface + = static_cast(g_type_interface_peek_parent(iface)); + iface->get_at_context = get_at_context; + iface->get_bounds = get_bounds; + iface->get_first_accessible_child = get_first_accessible_child; + iface->get_platform_state = parent_iface->get_platform_state; + // iface->get_platform_state = get_platform_state; +} + +G_DEFINE_TYPE_WITH_CODE(OOoFixed, ooo_fixed, GTK_TYPE_FIXED, + G_IMPLEMENT_INTERFACE(GTK_TYPE_ACCESSIBLE, ooo_fixed_accessible_init)) + +static void ooo_fixed_class_init(OOoFixedClass* /*klass*/) {} + +static void ooo_fixed_init(OOoFixed* /*area*/) {} + +GtkWidget* ooo_fixed_new() { return GTK_WIDGET(g_object_new(OOO_TYPE_FIXED, nullptr)); } + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/vcl/unx/gtk4/a11y.hxx b/vcl/unx/gtk4/a11y.hxx new file mode 100644 index 0000000000..90711515bc --- /dev/null +++ b/vcl/unx/gtk4/a11y.hxx @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include + +//TODO: Silence various loplugin:external and loplugin:unreffun in (WIP?) a11y.cxx for now: +struct LoAccessible; +struct LoAccessibleClass; +struct OOoFixed; +struct OOoFixedClass; +static inline gpointer lo_accessible_get_instance_private(LoAccessible*); +GType lo_accessible_get_type(); +static inline gpointer ooo_fixed_get_instance_private(OOoFixed*); +GtkWidget* ooo_fixed_new(); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/vcl/unx/gtk4/convert3to4.cxx b/vcl/unx/gtk4/convert3to4.cxx new file mode 100644 index 0000000000..16c7403bb2 --- /dev/null +++ b/vcl/unx/gtk4/convert3to4.cxx @@ -0,0 +1,1603 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "convert3to4.hxx" + +namespace +{ +typedef std::pair, OUString> named_node; + +bool sortButtonNodes(const named_node& rA, const named_node& rB) +{ + OUString sA(rA.second); + OUString sB(rB.second); + //order within groups according to platform rules + return getButtonPriority(sA) < getButtonPriority(sB); +} + +// 6 +css::uno::Reference +CreateProperty(const css::uno::Reference& xDoc, const OUString& rPropName, + const OUString& rValue) +{ + css::uno::Reference xProperty = xDoc->createElement("property"); + css::uno::Reference xPropName = xDoc->createAttribute("name"); + xPropName->setValue(rPropName); + xProperty->setAttributeNode(xPropName); + css::uno::Reference xValue = xDoc->createTextNode(rValue); + xProperty->appendChild(xValue); + return xProperty; +} + +bool ToplevelIsMessageDialog(const css::uno::Reference& xNode) +{ + for (css::uno::Reference xObjectCandidate = xNode->getParentNode(); + xObjectCandidate.is(); xObjectCandidate = xObjectCandidate->getParentNode()) + { + if (xObjectCandidate->getNodeName() == "object") + { + css::uno::Reference xObjectMap + = xObjectCandidate->getAttributes(); + css::uno::Reference xClass = xObjectMap->getNamedItem("class"); + if (xClass->getNodeValue() == "GtkMessageDialog") + return true; + } + } + return false; +} + +void insertAsFirstChild(const css::uno::Reference& xParentNode, + const css::uno::Reference& xChildNode) +{ + auto xFirstChild = xParentNode->getFirstChild(); + if (xFirstChild.is()) + xParentNode->insertBefore(xChildNode, xFirstChild); + else + xParentNode->appendChild(xChildNode); +} + +void SetPropertyOnTopLevel(const css::uno::Reference& xNode, + const css::uno::Reference& xProperty) +{ + for (css::uno::Reference xObjectCandidate = xNode->getParentNode(); + xObjectCandidate.is(); xObjectCandidate = xObjectCandidate->getParentNode()) + { + if (xObjectCandidate->getNodeName() == "object") + { + css::uno::Reference xObjectMap + = xObjectCandidate->getAttributes(); + css::uno::Reference xClass = xObjectMap->getNamedItem("class"); + if (xClass->getNodeValue() == "GtkDialog") + { + insertAsFirstChild(xObjectCandidate, xProperty); + break; + } + } + } +} + +OUString GetParentObjectType(const css::uno::Reference& xNode) +{ + auto xParent = xNode->getParentNode(); + assert(xParent->getNodeName() == "object"); + css::uno::Reference xParentMap = xParent->getAttributes(); + css::uno::Reference xClass = xParentMap->getNamedItem("class"); + return xClass->getNodeValue(); +} + +css::uno::Reference +GetChildObject(const css::uno::Reference& xChild) +{ + for (css::uno::Reference xObjectCandidate = xChild->getFirstChild(); + xObjectCandidate.is(); xObjectCandidate = xObjectCandidate->getNextSibling()) + { + if (xObjectCandidate->getNodeName() == "object") + return xObjectCandidate; + } + return nullptr; +} + +// currently runs the risk of duplicate margin-* properties if there was already such as well +// as the border +void AddBorderAsMargins(const css::uno::Reference& xNode, + const OUString& rBorderWidth) +{ + auto xDoc = xNode->getOwnerDocument(); + + auto xMarginEnd = CreateProperty(xDoc, "margin-end", rBorderWidth); + insertAsFirstChild(xNode, xMarginEnd); + xNode->insertBefore(CreateProperty(xDoc, "margin-top", rBorderWidth), xMarginEnd); + xNode->insertBefore(CreateProperty(xDoc, "margin-bottom", rBorderWidth), xMarginEnd); + xNode->insertBefore(CreateProperty(xDoc, "margin-start", rBorderWidth), xMarginEnd); +} + +struct MenuEntry +{ + bool m_bDrawAsRadio; + css::uno::Reference m_xPropertyLabel; + + MenuEntry(bool bDrawAsRadio, const css::uno::Reference& rPropertyLabel) + : m_bDrawAsRadio(bDrawAsRadio) + , m_xPropertyLabel(rPropertyLabel) + { + } +}; + +MenuEntry ConvertMenu(css::uno::Reference& xMenuSection, + const css::uno::Reference& xNode) +{ + bool bDrawAsRadio = false; + css::uno::Reference xPropertyLabel; + + css::uno::Reference xChild = xNode->getFirstChild(); + while (xChild.is()) + { + if (xChild->getNodeName() == "property") + { + css::uno::Reference xMap = xChild->getAttributes(); + css::uno::Reference xName = xMap->getNamedItem("name"); + OUString sName(xName->getNodeValue().replace('_', '-')); + + if (sName == "label") + { + xPropertyLabel = xChild; + } + else if (sName == "draw-as-radio") + { + bDrawAsRadio = toBool(xChild->getFirstChild()->getNodeValue()); + } + } + + auto xNextChild = xChild->getNextSibling(); + + auto xSavedMenuSection = xMenuSection; + + if (xChild->getNodeName() == "object") + { + auto xDoc = xChild->getOwnerDocument(); + + css::uno::Reference xMap = xChild->getAttributes(); + css::uno::Reference xClass = xMap->getNamedItem("class"); + OUString sClass(xClass->getNodeValue()); + + if (sClass == "GtkMenuItem" || sClass == "GtkRadioMenuItem") + { + /* */ + css::uno::Reference xItem = xDoc->createElement("item"); + xMenuSection->appendChild(xItem); + } + else if (sClass == "GtkSeparatorMenuItem") + { + /*
*/ + css::uno::Reference xSection + = xDoc->createElement("section"); + xMenuSection->getParentNode()->appendChild(xSection); + xMenuSection = xSection; + xSavedMenuSection = xMenuSection; + } + else if (sClass == "GtkMenu") + { + xMenuSection->removeChild(xMenuSection->getLastChild()); // remove preceding + + css::uno::Reference xSubMenu + = xDoc->createElement("submenu"); + css::uno::Reference xIdAttr = xDoc->createAttribute("id"); + + css::uno::Reference xId = xMap->getNamedItem("id"); + OUString sId(xId->getNodeValue()); + + xIdAttr->setValue(sId); + xSubMenu->setAttributeNode(xIdAttr); + xMenuSection->appendChild(xSubMenu); + + css::uno::Reference xSection + = xDoc->createElement("section"); + xSubMenu->appendChild(xSection); + + xMenuSection = xSection; + xSavedMenuSection = xMenuSection; + } + } + + bool bChildDrawAsRadio = false; + css::uno::Reference xChildPropertyLabel; + if (xChild->hasChildNodes()) + { + MenuEntry aEntry = ConvertMenu(xMenuSection, xChild); + bChildDrawAsRadio = aEntry.m_bDrawAsRadio; + xChildPropertyLabel = aEntry.m_xPropertyLabel; + } + + if (xChild->getNodeName() == "object") + { + xMenuSection = xSavedMenuSection; + + auto xDoc = xChild->getOwnerDocument(); + + css::uno::Reference xMap = xChild->getAttributes(); + css::uno::Reference xClass = xMap->getNamedItem("class"); + OUString sClass(xClass->getNodeValue()); + + if (sClass == "GtkMenuItem" || sClass == "GtkRadioMenuItem") + { + css::uno::Reference xId = xMap->getNamedItem("id"); + OUString sId = xId->getNodeValue(); + + /* + whatever + menu.action + id + */ + auto xItem = xMenuSection->getLastChild(); + + if (xChildPropertyLabel) + { + css::uno::Reference xChildPropertyElem( + xChildPropertyLabel, css::uno::UNO_QUERY_THROW); + + css::uno::Reference xLabelAttr + = xDoc->createElement("attribute"); + + css::uno::Reference xLabelMap + = xChildPropertyLabel->getAttributes(); + while (xLabelMap->getLength()) + { + css::uno::Reference xAttr(xLabelMap->item(0), + css::uno::UNO_QUERY_THROW); + xLabelAttr->setAttributeNode( + xChildPropertyElem->removeAttributeNode(xAttr)); + } + xLabelAttr->appendChild( + xChildPropertyLabel->removeChild(xChildPropertyLabel->getFirstChild())); + + xChildPropertyLabel->getParentNode()->removeChild(xChildPropertyLabel); + xItem->appendChild(xLabelAttr); + } + + css::uno::Reference xActionAttr + = xDoc->createElement("attribute"); + css::uno::Reference xActionName + = xDoc->createAttribute("name"); + xActionName->setValue("action"); + xActionAttr->setAttributeNode(xActionName); + if (bChildDrawAsRadio) + xActionAttr->appendChild(xDoc->createTextNode("menu.radio." + sId)); + else + xActionAttr->appendChild(xDoc->createTextNode("menu.normal." + sId)); + xItem->appendChild(xActionAttr); + + css::uno::Reference xTargetAttr + = xDoc->createElement("attribute"); + css::uno::Reference xTargetName + = xDoc->createAttribute("name"); + xTargetName->setValue("target"); + xTargetAttr->setAttributeNode(xTargetName); + xTargetAttr->appendChild(xDoc->createTextNode(sId)); + xItem->appendChild(xTargetAttr); + + css::uno::Reference xHiddenWhenAttr + = xDoc->createElement("attribute"); + css::uno::Reference xHiddenWhenName + = xDoc->createAttribute("name"); + xHiddenWhenName->setValue("hidden-when"); + xHiddenWhenAttr->setAttributeNode(xHiddenWhenName); + xHiddenWhenAttr->appendChild(xDoc->createTextNode("action-missing")); + xItem->appendChild(xHiddenWhenAttr); + } + } + + xChild = xNextChild; + } + + return MenuEntry(bDrawAsRadio, xPropertyLabel); +} + +struct ConvertResult +{ + bool m_bChildCanFocus; + bool m_bHasVisible; + bool m_bHasIconSize; + bool m_bAlwaysShowImage; + bool m_bUseUnderline; + bool m_bVertOrientation; + bool m_bXAlign; + GtkPositionType m_eImagePos; + css::uno::Reference m_xPropertyLabel; + css::uno::Reference m_xPropertyIconName; + + ConvertResult(bool bChildCanFocus, bool bHasVisible, bool bHasIconSize, bool bAlwaysShowImage, + bool bUseUnderline, bool bVertOrientation, bool bXAlign, + GtkPositionType eImagePos, + const css::uno::Reference& rPropertyLabel, + const css::uno::Reference& rPropertyIconName) + : m_bChildCanFocus(bChildCanFocus) + , m_bHasVisible(bHasVisible) + , m_bHasIconSize(bHasIconSize) + , m_bAlwaysShowImage(bAlwaysShowImage) + , m_bUseUnderline(bUseUnderline) + , m_bVertOrientation(bVertOrientation) + , m_bXAlign(bXAlign) + , m_eImagePos(eImagePos) + , m_xPropertyLabel(rPropertyLabel) + , m_xPropertyIconName(rPropertyIconName) + { + } +}; + +bool IsAllowedBuiltInIcon(std::u16string_view iconName) +{ + // limit the named icons to those known by VclBuilder + return VclBuilder::mapStockToSymbol(iconName) != SymbolType::DONTKNOW; +} + +ConvertResult Convert3To4(const css::uno::Reference& xNode) +{ + css::uno::Reference xNodeList = xNode->getChildNodes(); + if (!xNodeList.is()) + { + return ConvertResult(false, false, false, false, false, false, false, GTK_POS_LEFT, nullptr, + nullptr); + } + + std::vector> xRemoveList; + + OUString sBorderWidth; + bool bChildCanFocus = false; + bool bHasVisible = false; + bool bHasIconSize = false; + bool bAlwaysShowImage = false; + GtkPositionType eImagePos = GTK_POS_LEFT; + bool bUseUnderline = false; + bool bVertOrientation = false; + bool bXAlign = false; + css::uno::Reference xPropertyLabel; + css::uno::Reference xPropertyIconName; + css::uno::Reference xCantFocus; + + css::uno::Reference xGeneratedImageChild; + + css::uno::Reference xChild = xNode->getFirstChild(); + while (xChild.is()) + { + if (xChild->getNodeName() == "requires") + { + css::uno::Reference xMap = xChild->getAttributes(); + css::uno::Reference xLib = xMap->getNamedItem("lib"); + assert(xLib->getNodeValue() == "gtk+"); + xLib->setNodeValue("gtk"); + css::uno::Reference xVersion = xMap->getNamedItem("version"); + assert(xVersion->getNodeValue() == "3.20"); + xVersion->setNodeValue("4.0"); + } + else if (xChild->getNodeName() == "property") + { + css::uno::Reference xMap = xChild->getAttributes(); + css::uno::Reference xName = xMap->getNamedItem("name"); + OUString sName(xName->getNodeValue().replace('_', '-')); + + if (sName == "border-width") + sBorderWidth = xChild->getFirstChild()->getNodeValue(); + + if (sName == "has-default") + { + css::uno::Reference xParentMap + = xChild->getParentNode()->getAttributes(); + css::uno::Reference xId = xParentMap->getNamedItem("id"); + auto xDoc = xChild->getOwnerDocument(); + auto xDefaultWidget = CreateProperty(xDoc, "default-widget", xId->getNodeValue()); + SetPropertyOnTopLevel(xChild, xDefaultWidget); + xRemoveList.push_back(xChild); + } + + if (sName == "has-focus" || sName == "is-focus") + { + css::uno::Reference xParentMap + = xChild->getParentNode()->getAttributes(); + css::uno::Reference xId = xParentMap->getNamedItem("id"); + auto xDoc = xChild->getOwnerDocument(); + auto xDefaultWidget = CreateProperty(xDoc, "focus-widget", xId->getNodeValue()); + SetPropertyOnTopLevel(xChild, xDefaultWidget); + xRemoveList.push_back(xChild); + } + + if (sName == "can-focus") + { + bChildCanFocus = toBool(xChild->getFirstChild()->getNodeValue()); + if (!bChildCanFocus) + { + OUString sParentClass = GetParentObjectType(xChild); + if (sParentClass == "GtkBox" || sParentClass == "GtkGrid" + || sParentClass == "GtkViewport") + { + // e.g. for the case of notebooks without children yet, just remove the can't focus property + // from Boxes and Grids + xRemoveList.push_back(xChild); + } + else if (sParentClass == "GtkComboBoxText") + { + // this was always a bit finicky in gtk3, fix it up to default to can-focus + xRemoveList.push_back(xChild); + } + else + { + // otherwise mark the property as needing removal if there turns out to be a child + // with can-focus of true, in which case remove this parent conflicting property + xCantFocus = xChild; + } + } + } + + if (sName == "label") + { + OUString sParentClass = GetParentObjectType(xChild); + if (sParentClass == "GtkToolButton" || sParentClass == "GtkMenuToolButton" + || sParentClass == "GtkToggleToolButton") + { + xName->setNodeValue("tooltip-text"); + } + xPropertyLabel = xChild; + } + + if (sName == "modal") + { + OUString sParentClass = GetParentObjectType(xChild); + if (sParentClass == "GtkPopover") + xName->setNodeValue("autohide"); + } + + if (sName == "visible") + bHasVisible = true; + + if (sName == "icon-name") + xPropertyIconName = xChild; + + if (sName == "show-arrow") + xRemoveList.push_back(xChild); + + if (sName == "events") + xRemoveList.push_back(xChild); + + if (sName == "constrain-to") + xRemoveList.push_back(xChild); + + if (sName == "activates-default") + { + if (GetParentObjectType(xChild) == "GtkSpinButton") + xRemoveList.push_back(xChild); + } + + if (sName == "width-chars") + { + if (GetParentObjectType(xChild) == "GtkEntry") + { + // I don't quite get what the difference should be wrt width-chars and max-width-chars + // but glade doesn't write max-width-chars and in gtk4 where we have width-chars, e.g + // print dialog, then max-width-chars gives the effect we wanted with width-chars + auto xDoc = xChild->getOwnerDocument(); + auto mMaxWidthChars = CreateProperty(xDoc, "max-width-chars", + xChild->getFirstChild()->getNodeValue()); + xChild->getParentNode()->insertBefore(mMaxWidthChars, xChild); + } + } + + // remove 'Help' button label and replace with a help icon instead. Unless the toplevel is a message dialog + if (sName == "label" && GetParentObjectType(xChild) == "GtkButton" + && !ToplevelIsMessageDialog(xChild)) + { + css::uno::Reference xParentMap + = xChild->getParentNode()->getAttributes(); + css::uno::Reference xId = xParentMap->getNamedItem("id"); + if (xId && xId->getNodeValue() == "help") + { + auto xDoc = xChild->getOwnerDocument(); + auto xIconName = CreateProperty(xDoc, "icon-name", "help-browser-symbolic"); + xChild->getParentNode()->insertBefore(xIconName, xChild); + xRemoveList.push_back(xChild); + } + } + + if (sName == "icon-size") + { + if (GetParentObjectType(xChild) == "GtkImage") + { + bHasIconSize = true; + + OUString sSize = xChild->getFirstChild()->getNodeValue(); + /* + old: + 3 -> GTK_ICON_SIZE_LARGE_TOOLBAR: Size appropriate for large toolbars (24px) + 5 -> GTK_ICON_SIZE_DND: Size appropriate for drag and drop (32px) + 6 -> GTK_ICON_SIZE_DIALOG: Size appropriate for dialogs (48px) + + new: + 2 -> GTK_ICON_SIZE_LARGE + */ + if (sSize == "3" || sSize == "5" || sSize == "6") + { + auto xDoc = xChild->getOwnerDocument(); + auto xIconSize = CreateProperty(xDoc, "icon-size", "2"); + xChild->getParentNode()->insertBefore(xIconSize, xChild); + } + + xRemoveList.push_back(xChild); + } + + if (GetParentObjectType(xChild) == "GtkToolbar") + xRemoveList.push_back(xChild); + } + + if (sName == "truncate-multiline") + { + if (GetParentObjectType(xChild) == "GtkSpinButton") + xRemoveList.push_back(xChild); + } + + if (sName == "has-frame") + { + if (GetParentObjectType(xChild) == "GtkSpinButton") + xRemoveList.push_back(xChild); + } + + if (sName == "toolbar-style") + { + // is there an equivalent for this ? + xRemoveList.push_back(xChild); + } + + if (sName == "shadow-type") + { + if (GetParentObjectType(xChild) == "GtkFrame") + xRemoveList.push_back(xChild); + else if (GetParentObjectType(xChild) == "GtkScrolledWindow") + { + bool bHasFrame = xChild->getFirstChild()->getNodeValue() != "none"; + auto xDoc = xChild->getOwnerDocument(); + auto xHasFrame = CreateProperty( + xDoc, "has-frame", bHasFrame ? OUString("True") : OUString("False")); + xChild->getParentNode()->insertBefore(xHasFrame, xChild); + xRemoveList.push_back(xChild); + } + } + + if (sName == "always-show-image") + { + if (GetParentObjectType(xChild) == "GtkButton" + || GetParentObjectType(xChild) == "GtkMenuButton" + || GetParentObjectType(xChild) == "GtkToggleButton") + { + // we will turn always-show-image into a GtkBox child for + // GtkButton and a GtkLabel child for the GtkBox and move + // the label property into it. + bAlwaysShowImage = toBool(xChild->getFirstChild()->getNodeValue()); + xRemoveList.push_back(xChild); + } + } + + if (sName == "image-position") + { + if (GetParentObjectType(xChild) == "GtkButton") + { + // we will turn always-show-image into a GtkBox child for + // GtkButton and a GtkLabel child for the GtkBox and move + // the label property into it. + OUString sImagePos = xChild->getFirstChild()->getNodeValue(); + if (sImagePos == "top") + eImagePos = GTK_POS_TOP; + else if (sImagePos == "bottom") + eImagePos = GTK_POS_BOTTOM; + else if (sImagePos == "right") + eImagePos = GTK_POS_RIGHT; + else + assert(sImagePos == "left"); + xRemoveList.push_back(xChild); + } + } + + if (sName == "use-underline") + bUseUnderline = toBool(xChild->getFirstChild()->getNodeValue()); + + if (sName == "orientation") + bVertOrientation = xChild->getFirstChild()->getNodeValue() == "vertical"; + + if (sName == "relief") + { + if (GetParentObjectType(xChild) == "GtkToggleButton" + || GetParentObjectType(xChild) == "GtkMenuButton" + || GetParentObjectType(xChild) == "GtkLinkButton" + || GetParentObjectType(xChild) == "GtkButton") + { + assert(xChild->getFirstChild()->getNodeValue() == "none"); + auto xDoc = xChild->getOwnerDocument(); + auto xHasFrame = CreateProperty(xDoc, "has-frame", "False"); + xChild->getParentNode()->insertBefore(xHasFrame, xChild); + xRemoveList.push_back(xChild); + } + } + + if (sName == "xalign") + { + if (GetParentObjectType(xChild) == "GtkLinkButton" + || GetParentObjectType(xChild) == "GtkMenuButton" + || GetParentObjectType(xChild) == "GtkToggleButton" + || GetParentObjectType(xChild) == "GtkButton") + { + // TODO expand into a GtkLabel child with alignment on that instead + assert(xChild->getFirstChild()->getNodeValue() == "0"); + bXAlign = true; + xRemoveList.push_back(xChild); + } + } + + if (sName == "use-popover") + { + if (GetParentObjectType(xChild) == "GtkMenuButton") + xRemoveList.push_back(xChild); + } + + if (sName == "hscrollbar-policy") + { + if (GetParentObjectType(xChild) == "GtkScrolledWindow") + { + if (xChild->getFirstChild()->getNodeValue() == "never") + { + auto xDoc = xChild->getOwnerDocument(); + auto xHasFrame = CreateProperty(xDoc, "propagate-natural-width", "True"); + xChild->getParentNode()->insertBefore(xHasFrame, xChild); + } + } + } + + if (sName == "vscrollbar-policy") + { + if (GetParentObjectType(xChild) == "GtkScrolledWindow") + { + if (xChild->getFirstChild()->getNodeValue() == "never") + { + auto xDoc = xChild->getOwnerDocument(); + auto xHasFrame = CreateProperty(xDoc, "propagate-natural-height", "True"); + xChild->getParentNode()->insertBefore(xHasFrame, xChild); + } + } + } + + if (sName == "popup") + { + if (GetParentObjectType(xChild) == "GtkMenuButton") + { + OUString sMenuName = xChild->getFirstChild()->getNodeValue(); + auto xDoc = xChild->getOwnerDocument(); + auto xPopover = CreateProperty(xDoc, "popover", sMenuName); + xChild->getParentNode()->insertBefore(xPopover, xChild); + xRemoveList.push_back(xChild); + } + } + + if (sName == "image") + { + if (GetParentObjectType(xChild) == "GtkButton" + || GetParentObjectType(xChild) == "GtkMenuButton" + || GetParentObjectType(xChild) == "GtkToggleButton") + { + // find the image object, expected to be a child of "interface" + auto xObjectCandidate = xChild->getParentNode(); + if (xObjectCandidate->getNodeName() == "object") + { + OUString sImageId = xChild->getFirstChild()->getNodeValue(); + + css::uno::Reference xRootCandidate + = xChild->getParentNode(); + while (xRootCandidate) + { + if (xRootCandidate->getNodeName() == "interface") + break; + xRootCandidate = xRootCandidate->getParentNode(); + } + + css::uno::Reference xImageNode; + + for (auto xImageCandidate = xRootCandidate->getFirstChild(); + xImageCandidate.is(); + xImageCandidate = xImageCandidate->getNextSibling()) + { + css::uno::Reference xImageCandidateMap + = xImageCandidate->getAttributes(); + if (!xImageCandidateMap.is()) + continue; + css::uno::Reference xId + = xImageCandidateMap->getNamedItem("id"); + if (xId && xId->getNodeValue() == sImageId) + { + xImageNode = xImageCandidate; + break; + } + } + + auto xDoc = xChild->getOwnerDocument(); + + // relocate it to be a child of this GtkButton + xGeneratedImageChild = xDoc->createElement("child"); + xGeneratedImageChild->appendChild( + xImageNode->getParentNode()->removeChild(xImageNode)); + xObjectCandidate->appendChild(xGeneratedImageChild); + } + + xRemoveList.push_back(xChild); + } + } + + if (sName == "draw-indicator") + { + assert(toBool(xChild->getFirstChild()->getNodeValue())); + if (GetParentObjectType(xChild) == "GtkMenuButton" && gtk_get_minor_version() >= 4) + { + auto xDoc = xChild->getOwnerDocument(); + auto xAlwaysShowArrow = CreateProperty(xDoc, "always-show-arrow", "True"); + xChild->getParentNode()->insertBefore(xAlwaysShowArrow, xChild); + } + xRemoveList.push_back(xChild); + } + + if (sName == "type-hint" || sName == "skip-taskbar-hint" || sName == "can-default" + || sName == "border-width" || sName == "layout-style" || sName == "no-show-all" + || sName == "ignore-hidden" || sName == "window-position") + { + xRemoveList.push_back(xChild); + } + + if (sName == "AtkObject::accessible-description") + xName->setNodeValue("description"); + + if (sName == "AtkObject::accessible-name") + xName->setNodeValue("label"); + + if (sName == "AtkObject::accessible-role") + xName->setNodeValue("accessible-role"); + } + else if (xChild->getNodeName() == "child") + { + bool bContentArea = false; + + css::uno::Reference xMap = xChild->getAttributes(); + css::uno::Reference xName = xMap->getNamedItem("internal-child"); + if (xName) + { + OUString sName(xName->getNodeValue()); + if (sName == "vbox") + { + xName->setNodeValue("content_area"); + bContentArea = true; + } + else if (sName == "accessible") + { + xRemoveList.push_back(xChild); + } + } + + if (bContentArea) + { + css::uno::Reference xObject = GetChildObject(xChild); + if (xObject) + { + auto xDoc = xChild->getOwnerDocument(); + + auto xVExpand = CreateProperty(xDoc, "vexpand", "True"); + insertAsFirstChild(xObject, xVExpand); + + if (!sBorderWidth.isEmpty()) + { + AddBorderAsMargins(xObject, sBorderWidth); + sBorderWidth.clear(); + } + } + } + } + else if (xChild->getNodeName() == "packing") + { + // remove "packing" and if its grid packing insert a replacement "layout" into + // the associated "object" + auto xDoc = xChild->getOwnerDocument(); + css::uno::Reference xNew = xDoc->createElement("layout"); + + bool bGridPacking = false; + + // iterate over all children and append them to the new element + for (css::uno::Reference xCurrent = xChild->getFirstChild(); + xCurrent.is(); xCurrent = xChild->getFirstChild()) + { + css::uno::Reference xMap = xCurrent->getAttributes(); + if (xMap.is()) + { + css::uno::Reference xName = xMap->getNamedItem("name"); + OUString sName(xName->getNodeValue().replace('_', '-')); + if (sName == "left-attach") + { + xName->setNodeValue("column"); + bGridPacking = true; + } + else if (sName == "top-attach") + { + xName->setNodeValue("row"); + bGridPacking = true; + } + else if (sName == "width") + { + xName->setNodeValue("column-span"); + bGridPacking = true; + } + else if (sName == "height") + { + xName->setNodeValue("row-span"); + bGridPacking = true; + } + else if (sName == "secondary") + { + // turn parent tag of into + auto xParent = xChild->getParentNode(); + css::uno::Reference xTypeStart + = xDoc->createAttribute("type"); + xTypeStart->setValue("start"); + css::uno::Reference xElem( + xParent, css::uno::UNO_QUERY_THROW); + xElem->setAttributeNode(xTypeStart); + } + else if (sName == "pack-type") + { + // turn parent tag of into + auto xParent = xChild->getParentNode(); + + css::uno::Reference xParentMap + = xParent->getAttributes(); + css::uno::Reference xParentType + = xParentMap->getNamedItem("type"); + assert(!xParentType || xParentType->getNodeValue() == "titlebar"); + if (!xParentType) + { + css::uno::Reference xTypeStart + = xDoc->createAttribute("type"); + xTypeStart->setValue(xCurrent->getFirstChild()->getNodeValue()); + css::uno::Reference xElem( + xParent, css::uno::UNO_QUERY_THROW); + xElem->setAttributeNode(xTypeStart); + } + } + } + xNew->appendChild(xChild->removeChild(xCurrent)); + } + + if (bGridPacking) + { + // go back to parent and find the object child and insert this "layout" as a + // new child of the object + auto xParent = xChild->getParentNode(); + css::uno::Reference xObject = GetChildObject(xParent); + if (xObject) + xObject->appendChild(xNew); + } + + xRemoveList.push_back(xChild); + } + else if (xChild->getNodeName() == "accelerator") + { + // TODO is anything like this supported anymore in .ui files + xRemoveList.push_back(xChild); + } + else if (xChild->getNodeName() == "relation") + { + auto xDoc = xChild->getOwnerDocument(); + + css::uno::Reference xMap = xChild->getAttributes(); + css::uno::Reference xType = xMap->getNamedItem("type"); + if (xType->getNodeValue() == "label-for") + { + // there will be a matching labelled-by which should be sufficient in the gtk4 world + xRemoveList.push_back(xChild); + } + if (xType->getNodeValue() == "controlled-by") + { + // there will be a matching controller-for converted to -> controls + // which should be sufficient in the gtk4 world + xRemoveList.push_back(xChild); + } + else + { + css::uno::Reference xTarget = xMap->getNamedItem("target"); + + css::uno::Reference xValue + = xDoc->createTextNode(xTarget->getNodeValue()); + xChild->appendChild(xValue); + + css::uno::Reference xName = xDoc->createAttribute("name"); + if (xType->getNodeValue() == "controller-for") + xName->setValue("controls"); + else + xName->setValue(xType->getNodeValue()); + + css::uno::Reference xElem(xChild, + css::uno::UNO_QUERY_THROW); + xElem->setAttributeNode(xName); + xElem->removeAttribute("type"); + xElem->removeAttribute("target"); + } + } + + auto xNextChild = xChild->getNextSibling(); + + if (xChild->getNodeName() == "object") + { + auto xDoc = xChild->getOwnerDocument(); + + css::uno::Reference xMap = xChild->getAttributes(); + css::uno::Reference xClass = xMap->getNamedItem("class"); + OUString sClass(xClass->getNodeValue()); + + if (sClass == "GtkMenu") + { + css::uno::Reference xId = xMap->getNamedItem("id"); + OUString sId(xId->getNodeValue() + "-menu-model"); + + // + css::uno::Reference xMenu = xDoc->createElement("menu"); + css::uno::Reference xIdAttr = xDoc->createAttribute("id"); + xIdAttr->setValue(sId); + xMenu->setAttributeNode(xIdAttr); + xChild->getParentNode()->insertBefore(xMenu, xChild); + + css::uno::Reference xSection + = xDoc->createElement("section"); + xMenu->appendChild(xSection); + + css::uno::Reference xMenuSection(xSection); + ConvertMenu(xMenuSection, xChild); + + // now remove GtkMenu contents + while (true) + { + auto xFirstChild = xChild->getFirstChild(); + if (!xFirstChild.is()) + break; + xChild->removeChild(xFirstChild); + } + + // change to GtkPopoverMenu + xClass->setNodeValue("GtkPopoverMenu"); + + // + xChild->appendChild(CreateProperty(xDoc, "menu-model", sId)); + xChild->appendChild(CreateProperty(xDoc, "has-arrow", "False")); + xChild->appendChild(CreateProperty(xDoc, "visible", "False")); + } + } + + bool bChildHasIconSize = false; + bool bChildHasVisible = false; + bool bChildAlwaysShowImage = false; + GtkPositionType eChildImagePos = GTK_POS_LEFT; + bool bChildUseUnderline = false; + bool bChildVertOrientation = false; + bool bChildXAlign = false; + css::uno::Reference xChildPropertyLabel; + css::uno::Reference xChildPropertyIconName; + if (xChild->hasChildNodes() && xChild != xGeneratedImageChild) + { + auto aChildRes = Convert3To4(xChild); + bChildCanFocus |= aChildRes.m_bChildCanFocus; + if (bChildCanFocus && xCantFocus.is()) + { + xNode->removeChild(xCantFocus); + xCantFocus.clear(); + } + if (xChild->getNodeName() == "object") + { + bChildHasVisible = aChildRes.m_bHasVisible; + bChildHasIconSize = aChildRes.m_bHasIconSize; + bChildAlwaysShowImage = aChildRes.m_bAlwaysShowImage; + eChildImagePos = aChildRes.m_eImagePos; + bChildUseUnderline = aChildRes.m_bUseUnderline; + bChildVertOrientation = aChildRes.m_bVertOrientation; + bChildXAlign = aChildRes.m_bXAlign; + xChildPropertyLabel = aChildRes.m_xPropertyLabel; + xChildPropertyIconName = aChildRes.m_xPropertyIconName; + } + } + + if (xChild->getNodeName() == "object") + { + auto xDoc = xChild->getOwnerDocument(); + + css::uno::Reference xMap = xChild->getAttributes(); + css::uno::Reference xClass = xMap->getNamedItem("class"); + OUString sClass(xClass->getNodeValue()); + + auto xInternalChildCandidate = xChild->getParentNode(); + css::uno::Reference xInternalChildCandidateMap + = xInternalChildCandidate->getAttributes(); + css::uno::Reference xInternalChild + = xInternalChildCandidateMap->getNamedItem("internal-child"); + + // turn default gtk3 invisibility for widget objects into explicit invisible, but ignore internal-children + if (!bChildHasVisible && !xInternalChild) + { + if (sClass == "GtkBox" || sClass == "GtkButton" || sClass == "GtkCalendar" + || sClass == "GtkCheckButton" || sClass == "GtkRadioButton" + || sClass == "GtkComboBox" || sClass == "GtkComboBoxText" + || sClass == "GtkDrawingArea" || sClass == "GtkEntry" || sClass == "GtkExpander" + || sClass == "GtkFrame" || sClass == "GtkGrid" || sClass == "GtkImage" + || sClass == "GtkLabel" || sClass == "GtkMenuButton" || sClass == "GtkNotebook" + || sClass == "GtkOverlay" || sClass == "GtkPaned" || sClass == "GtkProgressBar" + || sClass == "GtkScrolledWindow" || sClass == "GtkScrollbar" + || sClass == "GtkSeparator" || sClass == "GtkSpinButton" + || sClass == "GtkSpinner" || sClass == "GtkTextView" || sClass == "GtkTreeView" + || sClass == "GtkViewport" || sClass == "GtkLinkButton" + || sClass == "GtkToggleButton" || sClass == "GtkButtonBox") + + { + auto xVisible = CreateProperty(xDoc, "visible", "False"); + insertAsFirstChild(xChild, xVisible); + } + } + + if (sClass == "GtkButtonBox") + { + if (xInternalChild && xInternalChild->getNodeValue() == "action_area" + && !ToplevelIsMessageDialog(xChild)) + { + xClass->setNodeValue("GtkHeaderBar"); + auto xSpacingNode = CreateProperty(xDoc, "show-title-buttons", "False"); + insertAsFirstChild(xChild, xSpacingNode); + + // move the replacement GtkHeaderBar up to before the content_area + auto xContentAreaCandidate = xChild->getParentNode(); + while (xContentAreaCandidate) + { + css::uno::Reference xChildMap + = xContentAreaCandidate->getAttributes(); + css::uno::Reference xName + = xChildMap->getNamedItem("internal-child"); + if (xName && xName->getNodeValue() == "content_area") + { + auto xActionArea = xChild->getParentNode(); + + xActionArea->getParentNode()->removeChild(xActionArea); + + css::uno::Reference xTypeTitleBar + = xDoc->createAttribute("type"); + xTypeTitleBar->setValue("titlebar"); + css::uno::Reference xElem( + xActionArea, css::uno::UNO_QUERY_THROW); + xElem->setAttributeNode(xTypeTitleBar); + xElem->removeAttribute("internal-child"); + + xContentAreaCandidate->getParentNode()->insertBefore( + xActionArea, xContentAreaCandidate); + + std::vector aChildren; + + css::uno::Reference xTitleChild + = xChild->getFirstChild(); + while (xTitleChild.is()) + { + auto xNextTitleChild = xTitleChild->getNextSibling(); + if (xTitleChild->getNodeName() == "child") + { + OUString sNodeId; + + css::uno::Reference xObject + = GetChildObject(xTitleChild); + if (xObject) + { + css::uno::Reference xObjectMap + = xObject->getAttributes(); + css::uno::Reference xObjectId + = xObjectMap->getNamedItem("id"); + sNodeId = xObjectId->getNodeValue(); + } + + aChildren.push_back(std::make_pair(xTitleChild, sNodeId)); + } + else if (xTitleChild->getNodeName() == "property") + { + // remove any tag + css::uno::Reference xTitleChildMap + = xTitleChild->getAttributes(); + css::uno::Reference xPropName + = xTitleChildMap->getNamedItem("name"); + OUString sPropName(xPropName->getNodeValue().replace('_', '-')); + if (sPropName == "homogeneous") + xChild->removeChild(xTitleChild); + } + + xTitleChild = xNextTitleChild; + } + + //sort child order within parent so that we match the platform button order + std::stable_sort(aChildren.begin(), aChildren.end(), sortButtonNodes); + + int nNonHelpButtonCount = 0; + + for (const auto& rTitleChild : aChildren) + { + xChild->removeChild(rTitleChild.first); + if (rTitleChild.second != "help") + ++nNonHelpButtonCount; + } + + std::reverse(aChildren.begin(), aChildren.end()); + + for (const auto& rTitleChild : aChildren) + { + xChild->appendChild(rTitleChild.first); + + css::uno::Reference xChildElem( + rTitleChild.first, css::uno::UNO_QUERY_THROW); + if (!xChildElem->hasAttribute("type")) + { + // turn parent tag of into except for cancel/close which we'll + // put at start unless there is nothing at end + css::uno::Reference xTypeEnd + = xDoc->createAttribute("type"); + if (nNonHelpButtonCount >= 2 + && (rTitleChild.second == "cancel" + || rTitleChild.second == "close")) + xTypeEnd->setValue("start"); + else + xTypeEnd->setValue("end"); + xChildElem->setAttributeNode(xTypeEnd); + } + } + + auto xUseHeaderBar = CreateProperty(xDoc, "use-header-bar", "1"); + SetPropertyOnTopLevel(xContentAreaCandidate, xUseHeaderBar); + + break; + } + xContentAreaCandidate = xContentAreaCandidate->getParentNode(); + } + } + else // GtkMessageDialog + xClass->setNodeValue("GtkBox"); + } + else if (sClass == "GtkToolbar") + { + xClass->setNodeValue("GtkBox"); + css::uno::Reference xStyle = xDoc->createElement("style"); + css::uno::Reference xToolbarClass + = xDoc->createElement("class"); + css::uno::Reference xPropName = xDoc->createAttribute("name"); + xPropName->setValue("toolbar"); + xToolbarClass->setAttributeNode(xPropName); + xStyle->appendChild(xToolbarClass); + xChild->appendChild(xStyle); + } + else if (sClass == "GtkToolButton") + { + xClass->setNodeValue("GtkButton"); + } + else if (sClass == "GtkToolItem") + { + xClass->setNodeValue("GtkBox"); + } + else if (sClass == "GtkMenuToolButton") + { + xClass->setNodeValue("GtkMenuButton"); + if (gtk_get_minor_version() >= 4) + { + auto xAlwaysShowArrow = CreateProperty(xDoc, "always-show-arrow", "True"); + insertAsFirstChild(xChild, xAlwaysShowArrow); + } + } + else if (sClass == "GtkRadioToolButton") + { + xClass->setNodeValue("GtkCheckButton"); + } + else if (sClass == "GtkToggleToolButton") + { + xClass->setNodeValue("GtkToggleButton"); + } + else if (sClass == "GtkSeparatorToolItem") + { + xClass->setNodeValue("GtkSeparator"); + } + else if (sClass == "GtkBox") + { + // reverse the order of the pack-type=end widgets + std::vector> aPackEnds; + std::vector> aPackStarts; + css::uno::Reference xBoxChild = xChild->getFirstChild(); + while (xBoxChild.is()) + { + auto xNextBoxChild = xBoxChild->getNextSibling(); + + if (xBoxChild->getNodeName() == "child") + { + css::uno::Reference xBoxChildMap + = xBoxChild->getAttributes(); + css::uno::Reference xType + = xBoxChildMap->getNamedItem("type"); + if (xType && xType->getNodeValue() == "end") + aPackEnds.push_back(xChild->removeChild(xBoxChild)); + else + aPackStarts.push_back(xBoxChild); + } + + xBoxChild = xNextBoxChild; + } + + if (!aPackEnds.empty()) + { + std::reverse(aPackEnds.begin(), aPackEnds.end()); + + if (!bChildVertOrientation) + { + bool bHasStartObject = false; + bool bLastStartExpands = false; + if (!aPackStarts.empty()) + { + css::uno::Reference xLastStartObject; + for (auto it = aPackStarts.rbegin(); it != aPackStarts.rend(); ++it) + { + xLastStartObject = GetChildObject(*it); + if (xLastStartObject.is()) + { + bHasStartObject = true; + for (css::uno::Reference xExpandCandidate + = xLastStartObject->getFirstChild(); + xExpandCandidate.is(); + xExpandCandidate = xExpandCandidate->getNextSibling()) + { + if (xExpandCandidate->getNodeName() == "property") + { + css::uno::Reference + xExpandMap = xExpandCandidate->getAttributes(); + css::uno::Reference xName + = xExpandMap->getNamedItem("name"); + OUString sPropName(xName->getNodeValue()); + if (sPropName == "hexpand") + { + bLastStartExpands + = toBool(xExpandCandidate->getFirstChild() + ->getNodeValue()); + break; + } + } + } + break; + } + } + } + + if (bHasStartObject && !bLastStartExpands) + { + auto xAlign = CreateProperty(xDoc, "halign", "end"); + insertAsFirstChild(GetChildObject(aPackEnds[0]), xAlign); + auto xExpand = CreateProperty(xDoc, "hexpand", "True"); + insertAsFirstChild(GetChildObject(aPackEnds[0]), xExpand); + } + } + + for (auto& xPackEnd : aPackEnds) + xChild->appendChild(xPackEnd); + } + } + else if (sClass == "GtkRadioButton") + { + xClass->setNodeValue("GtkCheckButton"); + } + else if (sClass == "GtkImage") + { + /* a) keep symbolic icon-names as GtkImage, e.g. writer, format, columns, next/prev + buttons + b) assume that an explicit icon-size represents a request for a scaled icon + so keep those as GtkImage. e.g. hyperlink dialog notebook tab images and + calc paste special button images + c) turn everything else into a GtkPicture, e.g. help, about. If a GtkPicture + ends up used to display just a simple icon that's generally not a problem. + */ + bool bKeepAsImage = false; + if (bChildHasIconSize) + bKeepAsImage = true; + else if (xChildPropertyIconName.is()) + { + OUString sIconName(xChildPropertyIconName->getFirstChild()->getNodeValue()); + bool bHasSymbolicIconName = IsAllowedBuiltInIcon(sIconName); + if (bHasSymbolicIconName) + { + if (sIconName != "missing-image") + bKeepAsImage = true; + else + { + // If the symbolic icon-name is missing-image then decide to make + // it a GtkPicture if it has a parent widget and keep it as GtkImage + // if it has just the root "interface" as parent. + // for e.g. view, user interface + css::uno::Reference xParent + = xChild->getParentNode(); + bKeepAsImage = xParent->getNodeName() == "interface"; + if (!bKeepAsImage) + xChild->removeChild(xChildPropertyIconName); + } + } + else + { + // private:graphicrepository/ would be turned by gio (?) into private:///graphicrepository/ + // so use private:///graphicrepository/ here. At the moment we just want this to be transported + // as-is to postprocess_widget. Though it might be nice to register a protocol handler with gio + // to avoid us doing the load in that second pass. + auto xUri = CreateProperty(xDoc, "file", + "private:///graphicrepository/" + sIconName); + xChild->insertBefore(xUri, xChildPropertyIconName); + // calc, insert, header and footer, custom header menubutton icon + auto xCanShrink = CreateProperty(xDoc, "can-shrink", "False"); + xChild->insertBefore(xCanShrink, xChildPropertyIconName); + xChild->removeChild(xChildPropertyIconName); + } + } + if (!bKeepAsImage) + xClass->setNodeValue("GtkPicture"); + } + else if (sClass == "GtkPopover" && !bHasVisible) + { + auto xVisible = CreateProperty(xDoc, "visible", "False"); + insertAsFirstChild(xChild, xVisible); + } + else if (sClass == "AtkObject") + { + css::uno::Reference xParentObject = xNode->getParentNode(); + css::uno::Reference xAccessibility + = xDoc->createElement("accessibility"); + xParentObject->insertBefore(xAccessibility, xNode); + + // move the converted old AtkObject:: properties into a new accessibility parent + css::uno::Reference xRole; + for (css::uno::Reference xCurrent = xChild->getFirstChild(); + xCurrent.is(); xCurrent = xChild->getFirstChild()) + { + if (css::uno::Reference xCurrentMap + = xCurrent->getAttributes()) + { + css::uno::Reference xName + = xCurrentMap->getNamedItem("name"); + OUString sName(xName->getNodeValue()); + if (sName == "accessible-role") + { + xRole = xCurrent; + } + } + + xAccessibility->appendChild(xChild->removeChild(xCurrent)); + } + + if (xRole) + { + auto xRoleText = xRole->getFirstChild(); + if (xRoleText.is()) + { + OUString sText = xRoleText->getNodeValue(); + // don't really know what the right solution here should be, but "static" isn't + // a role that exists in gtk4, nothing seems to match exactly, but maybe "alert" + // is a useful place to land for now + if (sText == "static") + xRoleText->setNodeValue("alert"); + } + // move out of accessibility section to property section + xParentObject->insertBefore(xRole->getParentNode()->removeChild(xRole), + xAccessibility); + } + } + + // only create the child box for GtkButton/GtkToggleButton + if (bChildAlwaysShowImage) + { + auto xImageCandidateNode = xChild->getLastChild(); + if (xImageCandidateNode && xImageCandidateNode->getNodeName() != "child") + xImageCandidateNode.clear(); + if (xImageCandidateNode) + xChild->removeChild(xImageCandidateNode); + + // for GtkMenuButton if this is a gearmenu with just an icon + // then "icon-name" is used for the indicator and there is + // expected to be no text. If there is a GtkPicture then treat + // this like a GtkButton and presumably it's a ToggleMenuButton + // and the relocation of contents happens in the builder + if (sClass == "GtkMenuButton") + { + bChildAlwaysShowImage = false; + if (xImageCandidateNode) + { + bChildAlwaysShowImage = true; + auto xImageObject = GetChildObject(xImageCandidateNode); + auto xProp = xImageObject->getFirstChild(); + while (xProp.is()) + { + if (xProp->getNodeName() == "property") + { + css::uno::Reference xPropMap + = xProp->getAttributes(); + css::uno::Reference xPropName + = xPropMap->getNamedItem("name"); + OUString sPropName(xPropName->getNodeValue().replace('_', '-')); + if (sPropName == "icon-name") + { + OUString sIconName(xProp->getFirstChild()->getNodeValue()); + auto xIconName = CreateProperty(xDoc, "icon-name", sIconName); + insertAsFirstChild(xChild, xIconName); + bChildAlwaysShowImage = false; + break; + } + } + + xProp = xProp->getNextSibling(); + } + } + } + + if (bChildAlwaysShowImage) + { + css::uno::Reference xNewChildNode + = xDoc->createElement("child"); + css::uno::Reference xNewObjectNode + = xDoc->createElement("object"); + css::uno::Reference xBoxClassName + = xDoc->createAttribute("class"); + xBoxClassName->setValue("GtkBox"); + xNewObjectNode->setAttributeNode(xBoxClassName); + + if (eChildImagePos == GTK_POS_TOP || eChildImagePos == GTK_POS_BOTTOM) + { + auto xOrientation = CreateProperty(xDoc, "orientation", "vertical"); + xNewObjectNode->appendChild(xOrientation); + } + + xNewObjectNode->appendChild(CreateProperty(xDoc, "spacing", "6")); + if (!bChildXAlign) + xNewObjectNode->appendChild(CreateProperty(xDoc, "halign", "center")); + + xNewChildNode->appendChild(xNewObjectNode); + + xChild->appendChild(xNewChildNode); + + css::uno::Reference xNewLabelChildNode + = xDoc->createElement("child"); + css::uno::Reference xNewChildObjectNode + = xDoc->createElement("object"); + css::uno::Reference xLabelClassName + = xDoc->createAttribute("class"); + xLabelClassName->setValue("GtkLabel"); + xNewChildObjectNode->setAttributeNode(xLabelClassName); + if (xChildPropertyLabel) + { + xNewChildObjectNode->appendChild( + xChildPropertyLabel->getParentNode()->removeChild(xChildPropertyLabel)); + } + else + { + auto xNotVisible = CreateProperty(xDoc, "visible", "False"); + xNewChildObjectNode->appendChild(xNotVisible); + } + if (bChildUseUnderline) + { + auto xUseUnderline = CreateProperty(xDoc, "use-underline", "True"); + xNewChildObjectNode->appendChild(xUseUnderline); + } + xNewLabelChildNode->appendChild(xNewChildObjectNode); + + if (eChildImagePos == GTK_POS_LEFT || eChildImagePos == GTK_POS_TOP) + { + if (xImageCandidateNode) + xNewObjectNode->appendChild(xImageCandidateNode); + xNewObjectNode->appendChild(xNewLabelChildNode); + } + else + { + xNewObjectNode->appendChild(xNewLabelChildNode); + if (xImageCandidateNode) + xNewObjectNode->appendChild(xImageCandidateNode); + } + } + } + } + else if (xChild->getNodeName() == "items") + { + // gtk4-4.6.7 won't set an active item if the active item + // property precedes the list of combobox items so if we + // have "items" move them to the start + insertAsFirstChild(xNode, xNode->removeChild(xChild)); + } + + xChild = xNextChild; + } + + if (!sBorderWidth.isEmpty()) + AddBorderAsMargins(xNode, sBorderWidth); + + for (auto& xRemove : xRemoveList) + xNode->removeChild(xRemove); + + return ConvertResult(bChildCanFocus, bHasVisible, bHasIconSize, bAlwaysShowImage, bUseUnderline, + bVertOrientation, bXAlign, eImagePos, xPropertyLabel, xPropertyIconName); +} + +std::once_flag cairo_surface_type_flag; + +void ensure_cairo_surface_type() +{ + // cairo_gobject_surface_get_type needs to be called at least once for + // g_type_from_name to be able to resolve "CairoSurface". In gtk3 there was fallback + // mechanism to attempt to resolve such "missing" types which is not in place for + // gtk4 so ensure it can be found explicitly + std::call_once(cairo_surface_type_flag, []() { cairo_gobject_surface_get_type(); }); +} +} + +void builder_add_from_gtk3_file(GtkBuilder* pBuilder, const OUString& rUri) +{ + GError* err = nullptr; + + // load the xml + css::uno::Reference xContext + = ::comphelper::getProcessComponentContext(); + css::uno::Reference xBuilder + = css::xml::dom::DocumentBuilder::create(xContext); + css::uno::Reference xDocument = xBuilder->parseURI(rUri); + + // convert it from gtk3 to gtk4 + Convert3To4(xDocument); + + css::uno::Reference xTempFile(css::io::TempFile::create(xContext), + css::uno::UNO_QUERY_THROW); + css::uno::Reference xTempStream(xTempFile, css::uno::UNO_QUERY_THROW); + xTempFile->setPropertyValue("RemoveFile", css::uno::Any(false)); + + // serialize it back to xml + css::uno::Reference xSerializer(xDocument, + css::uno::UNO_QUERY_THROW); + css::uno::Reference xWriter = css::xml::sax::Writer::create(xContext); + css::uno::Reference xTempOut = xTempStream->getOutputStream(); + xWriter->setOutputStream(xTempOut); + xSerializer->serialize( + css::uno::Reference(xWriter, css::uno::UNO_QUERY_THROW), + css::uno::Sequence()); + + ensure_cairo_surface_type(); + + // feed it to GtkBuilder + css::uno::Reference xTempSeek(xTempStream, css::uno::UNO_QUERY_THROW); + xTempSeek->seek(0); + auto xInput = xTempStream->getInputStream(); + css::uno::Sequence bytes; + sal_Int32 nToRead = xInput->available(); + while (true) + { + sal_Int32 nRead = xInput->readBytes(bytes, std::max(nToRead, 4096)); + if (!nRead) + break; + // fprintf(stderr, "text is %s\n", reinterpret_cast(bytes.getArray())); + bool rc = gtk_builder_add_from_string( + pBuilder, reinterpret_cast(bytes.getArray()), nRead, &err); + if (!rc) + { + SAL_WARN("vcl.gtk", + "GtkInstanceBuilder: error when calling gtk_builder_add_from_string: " + << err->message); + g_error_free(err); + } + assert(rc && "could not load UI file"); + // in the real world the first loop has read the entire file because its all 'available' without blocking + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/convert3to4.hxx b/vcl/unx/gtk4/convert3to4.hxx new file mode 100644 index 0000000000..d75d9fd910 --- /dev/null +++ b/vcl/unx/gtk4/convert3to4.hxx @@ -0,0 +1,16 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include +#include + +// convert gtk3 .ui to gtk4 and load it +void builder_add_from_gtk3_file(GtkBuilder* pBuilder, const OUString& rUri); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/customcellrenderer.cxx b/vcl/unx/gtk4/customcellrenderer.cxx new file mode 100644 index 0000000000..aebe2c89ed --- /dev/null +++ b/vcl/unx/gtk4/customcellrenderer.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../gtk3/customcellrenderer.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/vcl/unx/gtk4/fpicker/SalGtkFilePicker.cxx b/vcl/unx/gtk4/fpicker/SalGtkFilePicker.cxx new file mode 100644 index 0000000000..a820b49956 --- /dev/null +++ b/vcl/unx/gtk4/fpicker/SalGtkFilePicker.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/fpicker/SalGtkFilePicker.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/fpicker/SalGtkFilePicker.hxx b/vcl/unx/gtk4/fpicker/SalGtkFilePicker.hxx new file mode 100644 index 0000000000..44ea7bd44b --- /dev/null +++ b/vcl/unx/gtk4/fpicker/SalGtkFilePicker.hxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/fpicker/SalGtkFilePicker.hxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/fpicker/SalGtkFolderPicker.cxx b/vcl/unx/gtk4/fpicker/SalGtkFolderPicker.cxx new file mode 100644 index 0000000000..41dfdf0216 --- /dev/null +++ b/vcl/unx/gtk4/fpicker/SalGtkFolderPicker.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/fpicker/SalGtkFolderPicker.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/fpicker/SalGtkFolderPicker.hxx b/vcl/unx/gtk4/fpicker/SalGtkFolderPicker.hxx new file mode 100644 index 0000000000..129a699336 --- /dev/null +++ b/vcl/unx/gtk4/fpicker/SalGtkFolderPicker.hxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/fpicker/SalGtkFolderPicker.hxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/fpicker/SalGtkPicker.cxx b/vcl/unx/gtk4/fpicker/SalGtkPicker.cxx new file mode 100644 index 0000000000..159ef47040 --- /dev/null +++ b/vcl/unx/gtk4/fpicker/SalGtkPicker.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/fpicker/SalGtkPicker.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/fpicker/SalGtkPicker.hxx b/vcl/unx/gtk4/fpicker/SalGtkPicker.hxx new file mode 100644 index 0000000000..0432004a05 --- /dev/null +++ b/vcl/unx/gtk4/fpicker/SalGtkPicker.hxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/fpicker/SalGtkPicker.hxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/fpicker/eventnotification.hxx b/vcl/unx/gtk4/fpicker/eventnotification.hxx new file mode 100644 index 0000000000..b1e16bd653 --- /dev/null +++ b/vcl/unx/gtk4/fpicker/eventnotification.hxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/fpicker/eventnotification.hxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/fpicker/resourceprovider.cxx b/vcl/unx/gtk4/fpicker/resourceprovider.cxx new file mode 100644 index 0000000000..d4251cc94a --- /dev/null +++ b/vcl/unx/gtk4/fpicker/resourceprovider.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/fpicker/resourceprovider.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/gloactiongroup.cxx b/vcl/unx/gtk4/gloactiongroup.cxx new file mode 100644 index 0000000000..f147af2834 --- /dev/null +++ b/vcl/unx/gtk4/gloactiongroup.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../gtk3/gloactiongroup.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/glomenu.cxx b/vcl/unx/gtk4/glomenu.cxx new file mode 100644 index 0000000000..cae9bc03a4 --- /dev/null +++ b/vcl/unx/gtk4/glomenu.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../gtk3/glomenu.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/gtkcairo.cxx b/vcl/unx/gtk4/gtkcairo.cxx new file mode 100644 index 0000000000..3178fdea02 --- /dev/null +++ b/vcl/unx/gtk4/gtkcairo.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../gtk3/gtkcairo.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/gtkcairo.hxx b/vcl/unx/gtk4/gtkcairo.hxx new file mode 100644 index 0000000000..97f63101dd --- /dev/null +++ b/vcl/unx/gtk4/gtkcairo.hxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../../gtk3/gtkdata.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/gtkdata.cxx b/vcl/unx/gtk4/gtkdata.cxx new file mode 100644 index 0000000000..41d85217ce --- /dev/null +++ b/vcl/unx/gtk4/gtkdata.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../gtk3/gtkdata.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/gtkframe.cxx b/vcl/unx/gtk4/gtkframe.cxx new file mode 100644 index 0000000000..e446ba8a55 --- /dev/null +++ b/vcl/unx/gtk4/gtkframe.cxx @@ -0,0 +1,14 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "transferableprovider.hxx" + +#include "../gtk3/gtkframe.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/gtkinst.cxx b/vcl/unx/gtk4/gtkinst.cxx new file mode 100644 index 0000000000..658c01233c --- /dev/null +++ b/vcl/unx/gtk4/gtkinst.cxx @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +// make gtk4 plug advertise correctly as gtk4 +#define GTK_TOOLKIT_NAME "gtk4" + +#include "convert3to4.hxx" +#include "notifyinglayout.hxx" +#include "surfacecellrenderer.hxx" +#include "surfacepaintable.hxx" +#include "transferableprovider.hxx" + +#include "../gtk3/gtkinst.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/gtkobject.cxx b/vcl/unx/gtk4/gtkobject.cxx new file mode 100644 index 0000000000..8eb5dcf1a9 --- /dev/null +++ b/vcl/unx/gtk4/gtkobject.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../gtk3/gtkobject.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/gtksalmenu.cxx b/vcl/unx/gtk4/gtksalmenu.cxx new file mode 100644 index 0000000000..8950781246 --- /dev/null +++ b/vcl/unx/gtk4/gtksalmenu.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../gtk3/gtksalmenu.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/gtksys.cxx b/vcl/unx/gtk4/gtksys.cxx new file mode 100644 index 0000000000..dfdcf0a7c4 --- /dev/null +++ b/vcl/unx/gtk4/gtksys.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../gtk3/gtksys.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/hudawareness.cxx b/vcl/unx/gtk4/hudawareness.cxx new file mode 100644 index 0000000000..b2683b2bd7 --- /dev/null +++ b/vcl/unx/gtk4/hudawareness.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../gtk3/hudawareness.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/notifyinglayout.cxx b/vcl/unx/gtk4/notifyinglayout.cxx new file mode 100644 index 0000000000..46b9a2d95f --- /dev/null +++ b/vcl/unx/gtk4/notifyinglayout.cxx @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "notifyinglayout.hxx" + +struct _NotifyingLayout +{ + GtkLayoutManager parent_instance; + + GtkWidget* m_pWidget; + GtkLayoutManager* m_pOrigManager; + Link m_aLink; +}; + +G_DEFINE_TYPE(NotifyingLayout, notifying_layout, GTK_TYPE_LAYOUT_MANAGER) + +static void notifying_layout_measure(GtkLayoutManager* pLayoutManager, GtkWidget* widget, + GtkOrientation orientation, int for_size, int* minimum, + int* natural, int* minimum_baseline, int* natural_baseline) +{ + NotifyingLayout* self = NOTIFYING_LAYOUT(pLayoutManager); + GtkLayoutManagerClass* pKlass + = GTK_LAYOUT_MANAGER_CLASS(G_OBJECT_GET_CLASS(self->m_pOrigManager)); + pKlass->measure(self->m_pOrigManager, widget, orientation, for_size, minimum, natural, + minimum_baseline, natural_baseline); +} + +static void notifying_layout_allocate(GtkLayoutManager* pLayoutManager, GtkWidget* widget, + int width, int height, int baseline) +{ + NotifyingLayout* self = NOTIFYING_LAYOUT(pLayoutManager); + GtkLayoutManagerClass* pKlass + = GTK_LAYOUT_MANAGER_CLASS(G_OBJECT_GET_CLASS(self->m_pOrigManager)); + pKlass->allocate(self->m_pOrigManager, widget, width, height, baseline); + self->m_aLink.Call(nullptr); +} + +static GtkSizeRequestMode notifying_layout_get_request_mode(GtkLayoutManager* pLayoutManager, + GtkWidget* widget) +{ + NotifyingLayout* self = NOTIFYING_LAYOUT(pLayoutManager); + GtkLayoutManagerClass* pKlass + = GTK_LAYOUT_MANAGER_CLASS(G_OBJECT_GET_CLASS(self->m_pOrigManager)); + return pKlass->get_request_mode(self->m_pOrigManager, widget); +} + +static void notifying_layout_class_init(NotifyingLayoutClass* klass) +{ + GtkLayoutManagerClass* layout_class = GTK_LAYOUT_MANAGER_CLASS(klass); + + layout_class->get_request_mode = notifying_layout_get_request_mode; + layout_class->measure = notifying_layout_measure; + layout_class->allocate = notifying_layout_allocate; +} + +static void notifying_layout_init(NotifyingLayout* self) +{ + self->m_pWidget = nullptr; + self->m_pOrigManager = nullptr; + + // prevent loplugin:unreffun firing on macro generated function + (void)notifying_layout_get_instance_private(self); +} + +void notifying_layout_start_watch(NotifyingLayout* self, GtkWidget* pWidget, + const Link& rLink) +{ + self->m_pWidget = pWidget; + self->m_aLink = rLink; + + self->m_pOrigManager = gtk_widget_get_layout_manager(self->m_pWidget); + g_object_ref(self->m_pOrigManager); + + gtk_widget_set_layout_manager(pWidget, GTK_LAYOUT_MANAGER(self)); +} + +void notifying_layout_stop_watch(NotifyingLayout* self) +{ + gtk_widget_set_layout_manager(self->m_pWidget, self->m_pOrigManager); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/notifyinglayout.hxx b/vcl/unx/gtk4/notifyinglayout.hxx new file mode 100644 index 0000000000..a717a30613 --- /dev/null +++ b/vcl/unx/gtk4/notifyinglayout.hxx @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +G_DECLARE_FINAL_TYPE(NotifyingLayout, notifying_layout, NOTIFYING, LAYOUT, GtkLayoutManager) + +/* + Replace the existing GtkLayoutManager of pWidget with pLayout instead which will + forward all requests to the original GtkLayoutManager but additionally call + rLink when a size is allocated to the pWidget. + + This provides a workaround for the removal of the size-allocate signal in gtk4 +*/ +void notifying_layout_start_watch(NotifyingLayout* pLayout, GtkWidget* pWidget, + const Link& rLink); + +/* + Undo a previous notifying_layout_start_watch. +*/ +void notifying_layout_stop_watch(NotifyingLayout* pLayout); + +G_END_DECLS + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/salnativewidgets-gtk.cxx b/vcl/unx/gtk4/salnativewidgets-gtk.cxx new file mode 100644 index 0000000000..ed036e98c9 --- /dev/null +++ b/vcl/unx/gtk4/salnativewidgets-gtk.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../gtk3/salnativewidgets-gtk.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/surfacecellrenderer.cxx b/vcl/unx/gtk4/surfacecellrenderer.cxx new file mode 100644 index 0000000000..2ae0782b98 --- /dev/null +++ b/vcl/unx/gtk4/surfacecellrenderer.cxx @@ -0,0 +1,241 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include +#include "surfacecellrenderer.hxx" +#include + +namespace +{ +struct _SurfaceCellRendererClass : public GtkCellRendererClass +{ +}; + +enum +{ + PROP_SURFACE = 10000, +}; +} + +G_DEFINE_TYPE(SurfaceCellRenderer, surface_cell_renderer, GTK_TYPE_CELL_RENDERER) + +static void surface_cell_renderer_init(SurfaceCellRenderer* self) +{ + self->surface = nullptr; + // prevent loplugin:unreffun firing on macro generated function + (void)surface_cell_renderer_get_instance_private(self); +} + +static void surface_cell_renderer_get_property(GObject* object, guint param_id, GValue* value, + GParamSpec* pspec) +{ + SurfaceCellRenderer* cellsurface = SURFACE_CELL_RENDERER(object); + + switch (param_id) + { + case PROP_SURFACE: + g_value_set_boxed(value, cellsurface->surface); + break; + default: + G_OBJECT_CLASS(surface_cell_renderer_parent_class) + ->get_property(object, param_id, value, pspec); + break; + } +} + +static void surface_cell_renderer_set_property(GObject* object, guint param_id, const GValue* value, + GParamSpec* pspec) +{ + SurfaceCellRenderer* cellsurface = SURFACE_CELL_RENDERER(object); + + switch (param_id) + { + case PROP_SURFACE: + if (cellsurface->surface) + cairo_surface_destroy(cellsurface->surface); + cellsurface->surface = static_cast(g_value_get_boxed(value)); + if (cellsurface->surface) + cairo_surface_reference(cellsurface->surface); + break; + default: + G_OBJECT_CLASS(surface_cell_renderer_parent_class) + ->set_property(object, param_id, value, pspec); + break; + } +} + +static bool surface_cell_renderer_get_preferred_size(GtkCellRenderer* cell, + GtkOrientation orientation, gint* minimum_size, + gint* natural_size); + +static void surface_cell_renderer_snapshot(GtkCellRenderer* cell, GtkSnapshot* snapshot, + GtkWidget* widget, const GdkRectangle* background_area, + const GdkRectangle* cell_area, + GtkCellRendererState flags); + +static void surface_cell_renderer_render(GtkCellRenderer* cell, cairo_t* cr, GtkWidget* widget, + const GdkRectangle* background_area, + const GdkRectangle* cell_area, GtkCellRendererState flags); + +static void surface_cell_renderer_finalize(GObject* object) +{ + SurfaceCellRenderer* cellsurface = SURFACE_CELL_RENDERER(object); + + if (cellsurface->surface) + cairo_surface_destroy(cellsurface->surface); + + G_OBJECT_CLASS(surface_cell_renderer_parent_class)->finalize(object); +} + +static void surface_cell_renderer_get_preferred_width(GtkCellRenderer* cell, GtkWidget* widget, + gint* minimum_size, gint* natural_size) +{ + if (!surface_cell_renderer_get_preferred_size(cell, GTK_ORIENTATION_HORIZONTAL, minimum_size, + natural_size)) + { + // fallback to parent if we're empty + GTK_CELL_RENDERER_CLASS(surface_cell_renderer_parent_class) + ->get_preferred_width(cell, widget, minimum_size, natural_size); + } +} + +static void surface_cell_renderer_get_preferred_height(GtkCellRenderer* cell, GtkWidget* widget, + gint* minimum_size, gint* natural_size) +{ + if (!surface_cell_renderer_get_preferred_size(cell, GTK_ORIENTATION_VERTICAL, minimum_size, + natural_size)) + { + // fallback to parent if we're empty + GTK_CELL_RENDERER_CLASS(surface_cell_renderer_parent_class) + ->get_preferred_height(cell, widget, minimum_size, natural_size); + } +} + +static void surface_cell_renderer_get_preferred_height_for_width(GtkCellRenderer* cell, + GtkWidget* widget, gint /*width*/, + gint* minimum_height, + gint* natural_height) +{ + gtk_cell_renderer_get_preferred_height(cell, widget, minimum_height, natural_height); +} + +static void surface_cell_renderer_get_preferred_width_for_height(GtkCellRenderer* cell, + GtkWidget* widget, gint /*height*/, + gint* minimum_width, + gint* natural_width) +{ + gtk_cell_renderer_get_preferred_width(cell, widget, minimum_width, natural_width); +} + +void surface_cell_renderer_class_init(SurfaceCellRendererClass* klass) +{ + GtkCellRendererClass* cell_class = GTK_CELL_RENDERER_CLASS(klass); + GObjectClass* object_class = G_OBJECT_CLASS(klass); + + /* Hook up functions to set and get our custom cell renderer properties */ + object_class->get_property = surface_cell_renderer_get_property; + object_class->set_property = surface_cell_renderer_set_property; + + surface_cell_renderer_parent_class = g_type_class_peek_parent(klass); + object_class->finalize = surface_cell_renderer_finalize; + + cell_class->get_preferred_width = surface_cell_renderer_get_preferred_width; + cell_class->get_preferred_height = surface_cell_renderer_get_preferred_height; + cell_class->get_preferred_width_for_height + = surface_cell_renderer_get_preferred_width_for_height; + cell_class->get_preferred_height_for_width + = surface_cell_renderer_get_preferred_height_for_width; + + cell_class->snapshot = surface_cell_renderer_snapshot; + + g_object_class_install_property( + object_class, PROP_SURFACE, + g_param_spec_boxed("surface", "Surface", "The cairo surface to render", + CAIRO_GOBJECT_TYPE_SURFACE, G_PARAM_READWRITE)); +} + +GtkCellRenderer* surface_cell_renderer_new() +{ + return GTK_CELL_RENDERER(g_object_new(SURFACE_TYPE_CELL_RENDERER, nullptr)); +} + +static void get_surface_size(cairo_surface_t* pSurface, int& rWidth, int& rHeight) +{ + double x1, x2, y1, y2; + cairo_t* cr = cairo_create(pSurface); + cairo_clip_extents(cr, &x1, &y1, &x2, &y2); + cairo_destroy(cr); + + rWidth = x2 - x1; + rHeight = y2 - y1; +} + +bool surface_cell_renderer_get_preferred_size(GtkCellRenderer* cell, GtkOrientation orientation, + gint* minimum_size, gint* natural_size) +{ + SurfaceCellRenderer* cellsurface = SURFACE_CELL_RENDERER(cell); + + int nWidth = 0; + int nHeight = 0; + + if (cellsurface->surface) + get_surface_size(cellsurface->surface, nWidth, nHeight); + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + { + if (minimum_size) + *minimum_size = nWidth; + + if (natural_size) + *natural_size = nWidth; + } + else + { + if (minimum_size) + *minimum_size = nHeight; + + if (natural_size) + *natural_size = nHeight; + } + + return true; +} + +void surface_cell_renderer_render(GtkCellRenderer* cell, cairo_t* cr, GtkWidget* /*widget*/, + const GdkRectangle* /*background_area*/, + const GdkRectangle* cell_area, GtkCellRendererState /*flags*/) +{ + SurfaceCellRenderer* cellsurface = SURFACE_CELL_RENDERER(cell); + if (!cellsurface->surface) + return; + + int nWidth, nHeight; + get_surface_size(cellsurface->surface, nWidth, nHeight); + int nXOffset = (cell_area->width - nWidth) / 2; + int nYOffset = (cell_area->height - nHeight) / 2; + + cairo_set_source_surface(cr, cellsurface->surface, cell_area->x + nXOffset, + cell_area->y + nYOffset); + cairo_paint(cr); +} + +static void surface_cell_renderer_snapshot(GtkCellRenderer* cell, GtkSnapshot* snapshot, + GtkWidget* widget, const GdkRectangle* background_area, + const GdkRectangle* cell_area, + GtkCellRendererState flags) +{ + graphene_rect_t rect = GRAPHENE_RECT_INIT( + static_cast(cell_area->x), static_cast(cell_area->y), + static_cast(cell_area->width), static_cast(cell_area->height)); + cairo_t* cr = gtk_snapshot_append_cairo(GTK_SNAPSHOT(snapshot), &rect); + surface_cell_renderer_render(cell, cr, widget, background_area, cell_area, flags); + cairo_destroy(cr); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/surfacecellrenderer.hxx b/vcl/unx/gtk4/surfacecellrenderer.hxx new file mode 100644 index 0000000000..d908c04ceb --- /dev/null +++ b/vcl/unx/gtk4/surfacecellrenderer.hxx @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +struct _SurfaceCellRenderer +{ + GtkCellRenderer parent; + cairo_surface_t* surface; +}; + +/* + Provide a mechanism to support rendering a cairo surface in a GtkComboBox +*/ + +G_DECLARE_FINAL_TYPE(SurfaceCellRenderer, surface_cell_renderer, SURFACE, CELL_RENDERER, + GtkCellRenderer) + +#define SURFACE_TYPE_CELL_RENDERER (surface_cell_renderer_get_type()) + +#define SURFACE_CELL_RENDERER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), SURFACE_TYPE_CELL_RENDERER, SurfaceCellRenderer)) + +#define SURFACE_IS_CELL_RENDERER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), SURFACE_TYPE_CELL_RENDERER)) + +GtkCellRenderer* surface_cell_renderer_new(); + +G_END_DECLS + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/surfacepaintable.cxx b/vcl/unx/gtk4/surfacepaintable.cxx new file mode 100644 index 0000000000..2e8aa98b26 --- /dev/null +++ b/vcl/unx/gtk4/surfacepaintable.cxx @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "surfacepaintable.hxx" + +struct _SurfacePaintable +{ + GObject parent_instance; + int width; + int height; + cairo_surface_t* surface; +}; + +namespace +{ +struct _SurfacePaintableClass : public GObjectClass +{ +}; +} + +static void surface_paintable_snapshot(GdkPaintable* paintable, GdkSnapshot* snapshot, double width, + double height) +{ + graphene_rect_t rect + = GRAPHENE_RECT_INIT(0.0f, 0.0f, static_cast(width), static_cast(height)); + SurfacePaintable* self = SURFACE_PAINTABLE(paintable); + cairo_t* cr = gtk_snapshot_append_cairo(GTK_SNAPSHOT(snapshot), &rect); + cairo_set_source_surface(cr, self->surface, 0, 0); + cairo_paint(cr); + cairo_destroy(cr); +} + +static GdkPaintableFlags surface_paintable_get_flags(GdkPaintable* /*paintable*/) +{ + return static_cast(GDK_PAINTABLE_STATIC_SIZE + | GDK_PAINTABLE_STATIC_CONTENTS); +} + +static int surface_paintable_get_intrinsic_width(GdkPaintable* paintable) +{ + SurfacePaintable* self = SURFACE_PAINTABLE(paintable); + return self->width; +} + +static int surface_paintable_get_intrinsic_height(GdkPaintable* paintable) +{ + SurfacePaintable* self = SURFACE_PAINTABLE(paintable); + return self->height; +} + +static void surface_paintable_init_interface(GdkPaintableInterface* iface) +{ + iface->snapshot = surface_paintable_snapshot; + iface->get_flags = surface_paintable_get_flags; + iface->get_intrinsic_width = surface_paintable_get_intrinsic_width; + iface->get_intrinsic_height = surface_paintable_get_intrinsic_height; +} + +G_DEFINE_TYPE_WITH_CODE(SurfacePaintable, surface_paintable, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(GDK_TYPE_PAINTABLE, + surface_paintable_init_interface)); + +static void surface_paintable_init(SurfacePaintable* self) +{ + self->width = 0; + self->height = 0; + self->surface = nullptr; + + // prevent loplugin:unreffun firing on macro generated function + (void)surface_paintable_get_instance_private(self); +} + +static void surface_paintable_dispose(GObject* object) +{ + SurfacePaintable* self = SURFACE_PAINTABLE(object); + cairo_surface_destroy(self->surface); + G_OBJECT_CLASS(surface_paintable_parent_class)->dispose(object); +} + +static void surface_paintable_class_init(SurfacePaintableClass* klass) +{ + GObjectClass* object_class = G_OBJECT_CLASS(klass); + object_class->dispose = surface_paintable_dispose; +} + +void surface_paintable_set_source(SurfacePaintable* pPaintable, cairo_surface_t* pSource, + int nWidth, int nHeight) +{ + pPaintable->surface = pSource; + pPaintable->width = nWidth; + pPaintable->height = nHeight; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/surfacepaintable.hxx b/vcl/unx/gtk4/surfacepaintable.hxx new file mode 100644 index 0000000000..3e8c79f8ac --- /dev/null +++ b/vcl/unx/gtk4/surfacepaintable.hxx @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +/* + Provide a mechanism to allow continuing to use cairo surface where a GdkPaintable + is required +*/ + +G_DECLARE_FINAL_TYPE(SurfacePaintable, surface_paintable, SURFACE, PAINTABLE, GObject) + +/* + Set the surface to paint, takes ownership of pSource +*/ +void surface_paintable_set_source(SurfacePaintable* pPaintable, cairo_surface_t* pSource, + int nWidth, int nHeight); + +G_END_DECLS + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/transferableprovider.cxx b/vcl/unx/gtk4/transferableprovider.cxx new file mode 100644 index 0000000000..554b80c0d4 --- /dev/null +++ b/vcl/unx/gtk4/transferableprovider.cxx @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include +#include +#include "transferableprovider.hxx" + +#define TRANSFERABLE_CONTENT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), transerable_content_get_type(), TransferableContent)) + +struct _TransferableContent +{ + GdkContentProvider parent; + VclToGtkHelper* m_pConversionHelper; + css::datatransfer::XTransferable* m_pContents; + Link m_aDetachClipboardLink; +}; + +namespace +{ +struct _TransferableContentClass : public GdkContentProviderClass +{ +}; +} + +G_DEFINE_TYPE(TransferableContent, transerable_content, GDK_TYPE_CONTENT_PROVIDER) + +static void transerable_content_write_mime_type_async(GdkContentProvider* provider, + const char* mime_type, GOutputStream* stream, + int io_priority, GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + TransferableContent* self = TRANSFERABLE_CONTENT(provider); + if (!self->m_pContents) + return; + // tdf#129809 take a reference in case m_aContents is replaced during this + // call + css::uno::Reference xCurrentContents(self->m_pContents); + self->m_pConversionHelper->setSelectionData(xCurrentContents, provider, mime_type, stream, + io_priority, cancellable, callback, user_data); +} + +static gboolean transerable_content_write_mime_type_finish(GdkContentProvider*, + GAsyncResult* result, GError** error) +{ + return g_task_propagate_boolean(G_TASK(result), error); +} + +static GdkContentFormats* transerable_content_ref_formats(GdkContentProvider* provider) +{ + TransferableContent* self = TRANSFERABLE_CONTENT(provider); + css::uno::Reference xCurrentContents(self->m_pContents); + if (!xCurrentContents) + return nullptr; + + auto aFormats = xCurrentContents->getTransferDataFlavors(); + std::vector aGtkTargets(self->m_pConversionHelper->FormatsToGtk(aFormats)); + + GdkContentFormatsBuilder* pBuilder = gdk_content_formats_builder_new(); + for (const auto& rFormat : aGtkTargets) + gdk_content_formats_builder_add_mime_type(pBuilder, rFormat.getStr()); + return gdk_content_formats_builder_free_to_formats(pBuilder); +} + +static void transerable_content_detach_clipboard(GdkContentProvider* provider, GdkClipboard*) +{ + TransferableContent* self = TRANSFERABLE_CONTENT(provider); + self->m_aDetachClipboardLink.Call(nullptr); +} + +static void transerable_content_class_init(TransferableContentClass* klass) +{ + GdkContentProviderClass* provider_class = GDK_CONTENT_PROVIDER_CLASS(klass); + + provider_class->ref_formats = transerable_content_ref_formats; + provider_class->detach_clipboard = transerable_content_detach_clipboard; + provider_class->write_mime_type_async = transerable_content_write_mime_type_async; + provider_class->write_mime_type_finish = transerable_content_write_mime_type_finish; +} + +static void transerable_content_init(TransferableContent* self) +{ + self->m_pConversionHelper = nullptr; + self->m_pContents = nullptr; + // prevent loplugin:unreffun firing on macro generated function + (void)transerable_content_get_instance_private(self); +} + +void transerable_content_set_transferable(TransferableContent* pContent, + css::datatransfer::XTransferable* pTransferable) +{ + pContent->m_pContents = pTransferable; +} + +void transerable_content_set_detach_clipboard_link(TransferableContent* pContent, + const Link& rDetachClipboardLink) +{ + pContent->m_aDetachClipboardLink = rDetachClipboardLink; +} + +GdkContentProvider* transerable_content_new(VclToGtkHelper* pConversionHelper, + css::datatransfer::XTransferable* pTransferable) +{ + TransferableContent* content + = TRANSFERABLE_CONTENT(g_object_new(transerable_content_get_type(), nullptr)); + content->m_pConversionHelper = pConversionHelper; + content->m_pContents = pTransferable; + return GDK_CONTENT_PROVIDER(content); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/gtk4/transferableprovider.hxx b/vcl/unx/gtk4/transferableprovider.hxx new file mode 100644 index 0000000000..df4d643d9f --- /dev/null +++ b/vcl/unx/gtk4/transferableprovider.hxx @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include + +#include + +struct VclToGtkHelper; + +namespace com::sun::star::datatransfer +{ +class XTransferable; +} + +G_BEGIN_DECLS + +/* + Provide a mechanism to provide data from a LibreOffice XTransferable via a + GdkContentProvider for gtk clipboard or dnd +*/ + +G_DECLARE_FINAL_TYPE(TransferableContent, transerable_content, TRANSFERABLE, CONTENT, + GdkContentProvider) + +GdkContentProvider* transerable_content_new(VclToGtkHelper* pConversionHelper, + css::datatransfer::XTransferable* pTransferable); + +/* + Change to a new XTransferable + */ +void transerable_content_set_transferable(TransferableContent* pContent, + css::datatransfer::XTransferable* pTransferable); + +/* + If the GdkContentProvider is used by a clipboard call rDetachClipboardLink on losing + ownership of the clipboard +*/ +void transerable_content_set_detach_clipboard_link(TransferableContent* pContent, + const Link& rDetachClipboardLink); + +G_END_DECLS + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/kf5/KFFilePicker.cxx b/vcl/unx/kf5/KFFilePicker.cxx new file mode 100644 index 0000000000..8d0344b4cb --- /dev/null +++ b/vcl/unx/kf5/KFFilePicker.cxx @@ -0,0 +1,179 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "KFFilePicker.hxx" +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include + +using namespace ::com::sun::star; +using ::com::sun::star::ui::dialogs::ExtendedFilePickerElementIds::CHECKBOX_AUTOEXTENSION; + +namespace +{ +uno::Sequence FilePicker_getSupportedServiceNames() +{ + return { "com.sun.star.ui.dialogs.FilePicker", "com.sun.star.ui.dialogs.SystemFilePicker", + "com.sun.star.ui.dialogs.KFFilePicker", "com.sun.star.ui.dialogs.KFFolderPicker" }; +} +} + +// KFFilePicker + +KFFilePicker::KFFilePicker(css::uno::Reference const& context, + QFileDialog::FileMode eMode) + // Native KF5/KF6 filepicker does not add file extension automatically + : QtFilePicker(context, eMode, true) + , _layout(new QGridLayout(m_pExtraControls)) +{ + // only columns 0 and 1 are used by controls (s. QtFilePicker::addCustomControl); + // set stretch for (unused) column 2 in order for the controls to only take the space + // they actually need and avoid empty space in between + _layout->setColumnStretch(2, 1); + + // set layout so custom widgets show up in our native file dialog + setCustomControlWidgetLayout(_layout.get()); + + m_pFileDialog->setSupportedSchemes({ + QStringLiteral("file"), QStringLiteral("http"), QStringLiteral("https"), + QStringLiteral("webdav"), QStringLiteral("webdavs"), QStringLiteral("smb"), + QStringLiteral(""), // this makes removable devices shown + }); + + // used to set the custom controls + qApp->installEventFilter(this); +} + +// XFilePickerControlAccess +void SAL_CALL KFFilePicker::setValue(sal_Int16 controlId, sal_Int16 nControlAction, + const uno::Any& value) +{ + if (CHECKBOX_AUTOEXTENSION == controlId) + // We ignore this one and rely on QFileDialog to provide the functionality + return; + + QtFilePicker::setValue(controlId, nControlAction, value); +} + +uno::Any SAL_CALL KFFilePicker::getValue(sal_Int16 controlId, sal_Int16 nControlAction) +{ + SolarMutexGuard g; + auto* pSalInst(GetQtInstance()); + assert(pSalInst); + if (!pSalInst->IsMainThread()) + { + uno::Any ret; + pSalInst->RunInMainThread([&ret, this, controlId, nControlAction]() { + ret = getValue(controlId, nControlAction); + }); + return ret; + } + + if (CHECKBOX_AUTOEXTENSION == controlId) + // We ignore this one and rely on QFileDialog to provide the function. + // Always return false, to pretend we do not support this, otherwise + // LO core would try to be smart and cut the extension in some places, + // interfering with QFileDialog's handling of it. QFileDialog also + // saves the value of the setting, so LO core is not needed for that either. + return uno::Any(false); + + return QtFilePicker::getValue(controlId, nControlAction); +} + +void SAL_CALL KFFilePicker::enableControl(sal_Int16 controlId, sal_Bool enable) +{ + if (CHECKBOX_AUTOEXTENSION == controlId) + // We ignore this one and rely on QFileDialog to provide the functionality + return; + + QtFilePicker::enableControl(controlId, enable); +} + +void SAL_CALL KFFilePicker::setLabel(sal_Int16 controlId, const OUString& label) +{ + if (CHECKBOX_AUTOEXTENSION == controlId) + // We ignore this one and rely on QFileDialog to provide the functionality + return; + + QtFilePicker::setLabel(controlId, label); +} + +OUString SAL_CALL KFFilePicker::getLabel(sal_Int16 controlId) +{ + // We ignore this one and rely on QFileDialog to provide the functionality + if (CHECKBOX_AUTOEXTENSION == controlId) + return ""; + + return QtFilePicker::getLabel(controlId); +} + +void KFFilePicker::addCustomControl(sal_Int16 controlId) +{ + // native KF5/KF6 filepicker has its own autoextension checkbox, + // therefore avoid adding yet another one + if (controlId == CHECKBOX_AUTOEXTENSION) + return; + + QtFilePicker::addCustomControl(controlId); +} + +// XServiceInfo +OUString SAL_CALL KFFilePicker::getImplementationName() +{ + return "com.sun.star.ui.dialogs.KFFilePicker"; +} + +sal_Bool SAL_CALL KFFilePicker::supportsService(const OUString& ServiceName) +{ + return cppu::supportsService(this, ServiceName); +} + +uno::Sequence SAL_CALL KFFilePicker::getSupportedServiceNames() +{ + return FilePicker_getSupportedServiceNames(); +} + +bool KFFilePicker::eventFilter(QObject* o, QEvent* e) +{ + if (e->type() == QEvent::Show && o->isWidgetType()) + { + auto* w = static_cast(o); + if (!w->parentWidget() && w->isModal()) + { + if (auto* fileWidget = w->findChild({}, Qt::FindDirectChildrenOnly)) + { + fileWidget->setCustomWidget(m_pExtraControls); + // remove event filter again; the only purpose was to set the custom widget here + qApp->removeEventFilter(this); + } + } + } + return QObject::eventFilter(o, e); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/kf5/KFFilePicker.hxx b/vcl/unx/kf5/KFFilePicker.hxx new file mode 100644 index 0000000000..cbbf50792c --- /dev/null +++ b/vcl/unx/kf5/KFFilePicker.hxx @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include +#include + +class QGridLayout; + +class KFFilePicker final : public QtFilePicker +{ + Q_OBJECT + +private: + //layout for extra custom controls + std::unique_ptr _layout; + +public: + explicit KFFilePicker(css::uno::Reference const& context, + QFileDialog::FileMode); + + // XFilePickerControlAccess functions + virtual void SAL_CALL setValue(sal_Int16 nControlId, sal_Int16 nControlAction, + const css::uno::Any& rValue) override; + virtual css::uno::Any SAL_CALL getValue(sal_Int16 nControlId, + sal_Int16 nControlAction) override; + virtual void SAL_CALL enableControl(sal_Int16 nControlId, sal_Bool bEnable) override; + virtual void SAL_CALL setLabel(sal_Int16 nControlId, const OUString& rLabel) override; + virtual OUString SAL_CALL getLabel(sal_Int16 nControlId) override; + + // XServiceInfo + virtual OUString SAL_CALL getImplementationName() override; + virtual sal_Bool SAL_CALL supportsService(const OUString& rServiceName) override; + virtual css::uno::Sequence SAL_CALL getSupportedServiceNames() override; + +private: + //add a custom control widget to the file dialog + void addCustomControl(sal_Int16 controlId) override; + bool eventFilter(QObject* watched, QEvent* event) override; + +private Q_SLOTS: + // the KF5/KF6 file picker has its own automatic extension handling + void updateAutomaticFileExtension() override {} +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/kf5/KFSalInstance.cxx b/vcl/unx/kf5/KFSalInstance.cxx new file mode 100644 index 0000000000..cc280e9d99 --- /dev/null +++ b/vcl/unx/kf5/KFSalInstance.cxx @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include + +#include + +#include +#include +#include + +#include + +#include + +#include "KFFilePicker.hxx" +#include "KFSalInstance.hxx" + +using namespace com::sun::star; + +KFSalInstance::KFSalInstance(std::unique_ptr& pQApp) + : QtInstance(pQApp) +{ + ImplSVData* pSVData = ImplGetSVData(); + const OUString sToolkit = u"kf" + OUString::number(QT_VERSION_MAJOR); + pSVData->maAppData.mxToolkitName = constructToolkitID(sToolkit); +} + +bool KFSalInstance::GetUseReducedAnimation() +{ + // since Qt doesn not have a standard way to disable animations for the toolkit + // use the animation speed setting when on KDE Plasma, in accordance with how kde-gtk-config + // sets the Gtk setting based on that: + // https://invent.kde.org/plasma/kde-gtk-config/-/blob/881ae01ad361a03396f7f327365f225ef87688e8/kded/configvalueprovider.cpp#L239 + // (ideally, this should probably be done in the desktop backend rather than directly + // in the VCL plugin) + const OUString sDesktop = Application::GetDesktopEnvironment(); + if (sDesktop == "PLASMA5" || sDesktop == "PLASMA6") + { + KSharedConfigPtr pSharedConfig = KSharedConfig::openConfig(); + KConfigGroup aGeneralConfig = pSharedConfig->group(QStringLiteral("KDE")); + const qreal fAnimationSpeedModifier + = qMax(0.0, aGeneralConfig.readEntry("AnimationDurationFactor", 1.0)); + return qFuzzyIsNull(fAnimationSpeedModifier); + } + + return QtInstance::GetUseReducedAnimation(); +} + +bool KFSalInstance::hasNativeFileSelection() const +{ + const OUString sDesktop = Application::GetDesktopEnvironment(); + if (sDesktop == "PLASMA5" || sDesktop == "PLASMA6") + return true; + return QtInstance::hasNativeFileSelection(); +} + +rtl::Reference +KFSalInstance::createPicker(css::uno::Reference const& context, + QFileDialog::FileMode eMode) +{ + if (!IsMainThread()) + { + SolarMutexGuard g; + rtl::Reference pPicker; + RunInMainThread([&, this]() { pPicker = createPicker(context, eMode); }); + assert(pPicker); + return pPicker; + } + + // In order to insert custom controls, KFFilePicker currently relies on KFileWidget + // being used in the native file picker, which is only the case for KDE Plasma. + // Therefore, return the plain qt5/qt6 one in order to not lose custom controls otherwise. + const OUString sDesktop = Application::GetDesktopEnvironment(); + if (sDesktop == "PLASMA5" || sDesktop == "PLASMA6") + return new KFFilePicker(context, eMode); + return QtInstance::createPicker(context, eMode); +} + +extern "C" { +VCLPLUG_KF_PUBLIC SalInstance* create_SalInstance() +{ + std::unique_ptr pFakeArgv; + std::unique_ptr pFakeArgc; + std::vector aFakeArgvFreeable; + QtInstance::AllocFakeCmdlineArgs(pFakeArgv, pFakeArgc, aFakeArgvFreeable); + + std::unique_ptr pQApp + = QtInstance::CreateQApplication(*pFakeArgc, pFakeArgv.get()); + + KFSalInstance* pInstance = new KFSalInstance(pQApp); + pInstance->MoveFakeCmdlineArgs(pFakeArgv, pFakeArgc, aFakeArgvFreeable); + + new QtData(); + + return pInstance; +} +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/kf5/KFSalInstance.hxx b/vcl/unx/kf5/KFSalInstance.hxx new file mode 100644 index 0000000000..86795a1ba9 --- /dev/null +++ b/vcl/unx/kf5/KFSalInstance.hxx @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#pragma once + +#include + +class KFSalInstance final : public QtInstance +{ + bool hasNativeFileSelection() const override; + rtl::Reference + createPicker(css::uno::Reference const& context, + QFileDialog::FileMode) override; + + virtual bool GetUseReducedAnimation() override; + +public: + explicit KFSalInstance(std::unique_ptr& pQApp); +}; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/kf6/KFFilePicker.cxx b/vcl/unx/kf6/KFFilePicker.cxx new file mode 100644 index 0000000000..17672c8345 --- /dev/null +++ b/vcl/unx/kf6/KFFilePicker.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../kf5/KFFilePicker.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/kf6/KFFilePicker.hxx b/vcl/unx/kf6/KFFilePicker.hxx new file mode 100644 index 0000000000..b0c790552d --- /dev/null +++ b/vcl/unx/kf6/KFFilePicker.hxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../kf5/KFFilePicker.hxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/kf6/KFSalInstance.cxx b/vcl/unx/kf6/KFSalInstance.cxx new file mode 100644 index 0000000000..7595c93bd2 --- /dev/null +++ b/vcl/unx/kf6/KFSalInstance.cxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../kf5/KFSalInstance.cxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/kf6/KFSalInstance.hxx b/vcl/unx/kf6/KFSalInstance.hxx new file mode 100644 index 0000000000..9c648d9336 --- /dev/null +++ b/vcl/unx/kf6/KFSalInstance.hxx @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../kf5/KFSalInstance.hxx" + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/x11/x11sys.cxx b/vcl/unx/x11/x11sys.cxx new file mode 100644 index 0000000000..8f9478eb3b --- /dev/null +++ b/vcl/unx/x11/x11sys.cxx @@ -0,0 +1,98 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include +#include +#include +#include + +#include + +#include + +#include +#include + +SalSystem* X11SalInstance::CreateSalSystem() +{ + return new X11SalSystem(); +} + +X11SalSystem::~X11SalSystem() +{ +} + +// for the moment only handle xinerama case +unsigned int X11SalSystem::GetDisplayScreenCount() +{ + SalDisplay* pSalDisp = vcl_sal::getSalDisplay(GetGenericUnixSalData()); + return pSalDisp->IsXinerama() ? pSalDisp->GetXineramaScreens().size() : + pSalDisp->GetXScreenCount(); +} + +unsigned int X11SalSystem::GetDisplayBuiltInScreen() +{ + SalDisplay* pSalDisp = vcl_sal::getSalDisplay(GetGenericUnixSalData()); + return pSalDisp->GetDefaultXScreen().getXScreen(); +} + +AbsoluteScreenPixelRectangle X11SalSystem::GetDisplayScreenPosSizePixel( unsigned int nScreen ) +{ + AbsoluteScreenPixelRectangle aRet; + SalDisplay* pSalDisp = vcl_sal::getSalDisplay(GetGenericUnixSalData()); + if( pSalDisp->IsXinerama() ) + { + const std::vector< AbsoluteScreenPixelRectangle >& rScreens = pSalDisp->GetXineramaScreens(); + + // we shouldn't be able to pick a screen > number of screens available + assert(nScreen < rScreens.size() ); + + if( nScreen < rScreens.size() ) + aRet = rScreens[nScreen]; + } + else + { + const SalDisplay::ScreenData& rScreen = + pSalDisp->getDataForScreen( SalX11Screen( nScreen ) ); + aRet = AbsoluteScreenPixelRectangle( AbsoluteScreenPixelPoint( 0, 0 ), rScreen.m_aSize ); + } + + return aRet; +} + +int X11SalSystem::ShowNativeDialog( const OUString& rTitle, const OUString& rMessage, const std::vector< OUString >& rButtons ) +{ + ImplSVData* pSVData = ImplGetSVData(); + if( pSVData->mpIntroWindow ) + pSVData->mpIntroWindow->Hide(); + + std::unique_ptr xWarn(Application::CreateMessageDialog(nullptr, + VclMessageType::Warning, VclButtonsType::NONE, + rMessage)); + xWarn->set_title(rTitle); + + sal_uInt16 nButton = 0; + for (auto const& button : rButtons) + xWarn->add_button(button, nButton++); + xWarn->set_default_response(0); + + return xWarn->run(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/unx/x11/xlimits.cxx b/vcl/unx/x11/xlimits.cxx new file mode 100644 index 0000000000..b8509cbd24 --- /dev/null +++ b/vcl/unx/x11/xlimits.cxx @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include +#include + +Pixmap limitXCreatePixmap(Display *display, Drawable d, unsigned int width, unsigned int height, unsigned int depth) +{ + // The X protocol request CreatePixmap puts an upper bound + // of 16 bit to the size. And in practice some drivers + // fall over with values close to the max. + + // see, e.g. moz#424333, fdo#48961, rhbz#1086714 + // we've a duplicate of this in canvas :-( + if (width > SAL_MAX_INT16-10 || height > SAL_MAX_INT16-10) + { + SAL_WARN("vcl", "overlarge pixmap: " << width << " x " << height); + return None; + } + return XCreatePixmap(display, d, width, height, depth); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ -- cgit v1.2.3