diff --git a/.github/workflows/root-ci-config/buildconfig/global.txt b/.github/workflows/root-ci-config/buildconfig/global.txt index b6eedd626835b..1b0fc15e0ce63 100644 --- a/.github/workflows/root-ci-config/buildconfig/global.txt +++ b/.github/workflows/root-ci-config/buildconfig/global.txt @@ -71,6 +71,7 @@ pyroot=ON pythia8=OFF qt6web=OFF qt6canv=OFF +gtk4canv=OFF roofit=ON root7=ON rootbench=OFF diff --git a/cmake/modules/RootBuildOptions.cmake b/cmake/modules/RootBuildOptions.cmake index d50fccf104969..ab3e7129c4dec 100644 --- a/cmake/modules/RootBuildOptions.cmake +++ b/cmake/modules/RootBuildOptions.cmake @@ -155,6 +155,7 @@ ROOT_BUILD_OPTION(opengl ON "Enable support for OpenGL (requires libGL and libGL ROOT_BUILD_OPTION(pyroot ON "Enable support for automatic Python bindings (PyROOT)") ROOT_BUILD_OPTION(pythia8 OFF "Enable support for Pythia 8.x [GPL]") ROOT_BUILD_OPTION(qt6canv OFF "Enable Qt6 canvas (requires Qt6::Gui)") +ROOT_BUILD_OPTION(gtk4canv OFF "Enable GTK4 canvas (requires GTK4)") ROOT_BUILD_OPTION(qt6web OFF "Enable support for Qt6 web-based display (requires Qt6::WebEngineCore and Qt6::WebEngineWidgets)") ROOT_BUILD_OPTION(roofit ON "Build the advanced fitting package RooFit, and RooStats for statistical tests. If xml is available, also build HistFactory.") ROOT_BUILD_OPTION(root7 ON "Build ROOT 7 experimental components of ROOT") @@ -245,6 +246,7 @@ if(all) set(pythia8_defvalue ON) set(pyroot_defvalue ON) set(qt6canv_defvalue ON) + set(gtk4canv_defvalue ON) set(qt6web_defvalue ON) set(r_defvalue ON) set(roofit_defvalue ON) diff --git a/etc/plugins/TCanvasImp/P030_TGtk4Canvas.C b/etc/plugins/TCanvasImp/P030_TGtk4Canvas.C new file mode 100644 index 0000000000000..f9d6f6ea1b14e --- /dev/null +++ b/etc/plugins/TCanvasImp/P030_TGtk4Canvas.C @@ -0,0 +1,5 @@ +void P030_TGtk4Canvas() +{ + gPluginMgr->AddHandler("TCanvasImp", "TGtk4Canvas", "ROOT::Experimental::TGtk4Canvas", + "ROOTGtk4Canvas", "NewCanvas(TCanvas *, const char *, Int_t, Int_t, UInt_t, UInt_t)"); +} diff --git a/etc/plugins/TGuiFactory/P030_TGtk4GuiFactory.C b/etc/plugins/TGuiFactory/P030_TGtk4GuiFactory.C new file mode 100644 index 0000000000000..6ba1ec47adec0 --- /dev/null +++ b/etc/plugins/TGuiFactory/P030_TGtk4GuiFactory.C @@ -0,0 +1,5 @@ +void P030_TGtk4GuiFactory() +{ + gPluginMgr->AddHandler("TGuiFactory", "gtk4", "ROOT::Experimental::TGtk4GuiFactory", + "ROOTGtk4Canvas", "TGtk4GuiFactory()"); +} diff --git a/etc/plugins/TVirtualPadEditor/P030_TGtk4GedEditor.C b/etc/plugins/TVirtualPadEditor/P030_TGtk4GedEditor.C new file mode 100644 index 0000000000000..6ba40f9fc19f0 --- /dev/null +++ b/etc/plugins/TVirtualPadEditor/P030_TGtk4GedEditor.C @@ -0,0 +1,5 @@ +void P030_TGtk4GedEditor() +{ + gPluginMgr->AddHandler("TVirtualPadEditor", "gtk4", "ROOT::Experimental::TGtk4GedEditor", + "ROOTGtk4Canvas", "TGtk4GedEditor(TCanvas*)"); +} diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index c8ed70ae4f357..582d92606bca6 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -16,6 +16,10 @@ if(qt6canv) add_subdirectory(qt6canvas) endif() +if(gtk4canv) + add_subdirectory(gtk4canvas) +endif() + if(webgui) if(cefweb) add_subdirectory(cefdisplay) diff --git a/gui/gtk4canvas/CMakeLists.txt b/gui/gtk4canvas/CMakeLists.txt new file mode 100644 index 0000000000000..a0e2e9d0e2439 --- /dev/null +++ b/gui/gtk4canvas/CMakeLists.txt @@ -0,0 +1,46 @@ +# Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. +# All rights reserved. +# +# For the licensing terms see $ROOTSYS/LICENSE. +# For the list of contributors see $ROOTSYS/README/CREDITS. + +############################################################################ +# CMakeLists.txt file for building ROOT gui/gtk4canvas package +############################################################################ + +find_package(PkgConfig) + +pkg_check_modules(GTKMM REQUIRED IMPORTED_TARGET gtkmm-4.0) + +if(NOT GTKMM_FOUND) + if(fail-on-missing) + message(FATAL_ERROR "Could NOT find gtkmm-4.0 components") + else() + message(WARNING "GTKMM not found, disabling option 'gtk4canv'") + set(gtk4canv OFF CACHE BOOL "Disabled because GTKMM not found" FORCE) + return() + endif() +endif() + + +ROOT_STANDARD_LIBRARY_PACKAGE(ROOTGtk4Canvas + HEADERS + TGtk4Canvas.h + TGtk4GuiFactory.h + TGtk4PadPainter.h + TGtk4GedEditor.h + SOURCES + Gtk4DrawArea.cxx + Gtk4CanvasWindow.cxx + Gtk4MethodDialog.cxx + TGtk4ContextMenu.cxx + TGtk4Canvas.cxx + TGtk4GuiFactory.cxx + TGtk4PadPainter.cxx + TGtk4GedEditor.cxx + LIBRARIES + PkgConfig::GTKMM + DEPENDENCIES + Core + Gpad +) diff --git a/gui/gtk4canvas/inc/LinkDef.h b/gui/gtk4canvas/inc/LinkDef.h new file mode 100644 index 0000000000000..bacef72c08f31 --- /dev/null +++ b/gui/gtk4canvas/inc/LinkDef.h @@ -0,0 +1,22 @@ +// Author: Sergey Linev, GSI 12/08/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +#ifdef __CLING__ + +#pragma link off all globals; +#pragma link off all classes; +#pragma link off all functions; + +#pragma link C++ class ROOT::Experimental::TGtk4Canvas+; +#pragma link C++ class ROOT::Experimental::TGtk4PadPainter+; +#pragma link C++ class ROOT::Experimental::TGtk4GuiFactory+; +#pragma link C++ class ROOT::Experimental::TGtk4GedEditor+; + +#endif diff --git a/gui/gtk4canvas/inc/TGtk4Canvas.h b/gui/gtk4canvas/inc/TGtk4Canvas.h new file mode 100644 index 0000000000000..319d3869b02f1 --- /dev/null +++ b/gui/gtk4canvas/inc/TGtk4Canvas.h @@ -0,0 +1,84 @@ +// Author: Sergey Linev, GSI 12/08/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +#ifndef ROOT_TGtk4Canvas +#define ROOT_TGtk4Canvas + +#include "TCanvasImp.h" + +class Gtk4CanvasWindow; +class Gtk4DrawArea; + +namespace ROOT { +namespace Experimental { + + +class TGtk4Canvas : public TCanvasImp { + +protected: + + Gtk4CanvasWindow *fCanvasWindow = nullptr; + Gtk4DrawArea *fDrawArea = nullptr; + + Bool_t fFixedSize = kFALSE; /// + +class TVirtualPad; +class TAttMarker; +class TAttText; +class TAttLine; +class TAttFill; +class TClass; + +class Gtk4MethodDialog; + +namespace Gtk { + class DropDown; + class SpinButton; + class ColorDialogButton; +} + + +namespace ROOT { +namespace Experimental { + +class TGtk4GedEditor : public TVirtualPadEditor, public TObject, public TQObject { + + protected: + + TCanvas *fCanvas = nullptr; + TVirtualPad *fPad = nullptr; + TObject *fModel = nullptr; + Bool_t fGlobal = kTRUE; + + Gtk4MethodDialog *fDialog = nullptr; + + TAttLine *fAttLine = nullptr; + TAttFill *fAttFill = nullptr; + TAttText *fAttText = nullptr; + TAttMarker *fAttMarker = nullptr; + + Gtk::ColorDialogButton* m_line_color = nullptr; + Gtk::DropDown *m_line_style = nullptr; + Gtk::SpinButton *m_line_width = nullptr; + + Gtk::ColorDialogButton* m_fill_color = nullptr; + Gtk::ColorDialogButton* m_text_color = nullptr; + Gtk::ColorDialogButton* m_marker_color = nullptr; + + Int_t GetRootColor(Gtk::ColorDialogButton* btn); + + void FillDialogsElements(); + + void ModifiedPad(); + + void FillGed(TClass *cl); + + void on_ok_button_clicked(); + + public: + + TGtk4GedEditor(TCanvas *c = nullptr); + virtual ~TGtk4GedEditor(); + + Bool_t IsGlobal() const override { return fGlobal; } + void SetGlobal(Bool_t on) override { fGlobal = on; } + + TCanvas* GetCanvas() const override { return fCanvas; } + + void ConnectToCanvas(TCanvas *c); + void DisconnectFromCanvas(); + void SetCanvas(TCanvas *c); + + void Show() override; + void Hide() override; + + void RecursiveRemove(TObject* obj) override; + + virtual void SetModel(TVirtualPad* pad, TObject* obj, Int_t event); + + // specific editors + void AddTAttLine(TAttLine *); + void AddTAttFill(TAttFill *); + void AddTAttText(TAttText *); + void AddTAttMarker(TAttMarker *); + + ClassDefOverride(TGtk4GedEditor, 0) // Implementation for Ged Editor with GTK4 +}; + +} // namespace Experimental +} // namespace ROOT + +#endif diff --git a/gui/gtk4canvas/inc/TGtk4GuiFactory.h b/gui/gtk4canvas/inc/TGtk4GuiFactory.h new file mode 100644 index 0000000000000..af9abb4f0b917 --- /dev/null +++ b/gui/gtk4canvas/inc/TGtk4GuiFactory.h @@ -0,0 +1,41 @@ +// Author: Sergey Linev 12/08/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +#ifndef ROOT_TGtk4GuiFactory +#define ROOT_TGtk4GuiFactory + +#include "TGuiFactory.h" + +namespace ROOT { +namespace Experimental { + +class TGtk4GuiFactory : public TGuiFactory { + +public: + TGtk4GuiFactory(const char *name = "gtk4", const char *title = "ROOT Gtk4 GUI Factory"); + ~TGtk4GuiFactory() override {} + + Bool_t UseVirtualX() const override { return kFALSE; } + + TApplicationImp *CreateApplicationImp(const char *classname, int *argc, char **argv) override; + + TCanvasImp *CreateCanvasImp(TCanvas *c, const char *title, UInt_t width, UInt_t height) override; + TCanvasImp *CreateCanvasImp(TCanvas *c, const char *title, Int_t x, Int_t y, UInt_t width, UInt_t height) override; + + TContextMenuImp *CreateContextMenuImp(TContextMenu *c, const char *name, const char *title) override; + + ClassDefOverride(TGtk4GuiFactory,0) //Factory for Gtk4 GUI components +}; + +} // namespace Experimental +} // namespace ROOT + + +#endif diff --git a/gui/gtk4canvas/inc/TGtk4PadPainter.h b/gui/gtk4canvas/inc/TGtk4PadPainter.h new file mode 100644 index 0000000000000..65fbc98dc3deb --- /dev/null +++ b/gui/gtk4canvas/inc/TGtk4PadPainter.h @@ -0,0 +1,104 @@ +// Author: Sergey Linev, GSI 12/08/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +#ifndef ROOT_TGtk4PadPainter +#define ROOT_TGtk4PadPainter + +#include "TPadPainterBase.h" + + +class Gtk4DrawArea; +class TTFhandle; + +namespace ROOT { +namespace Experimental { + +class TGtk4Canvas; + +class TGtk4PadPainter : public TPadPainterBase { + +friend class TGtk4Canvas; + +protected: + + Gtk4DrawArea *fDrawArea = nullptr; + + int fCustomPattern = 0; + + void SetGtk4Color(Color_t id); + Bool_t SetLinePen(); + Bool_t SetFillBrush(); + void ApplyFillBrush(); + +public: + + TGtk4PadPainter(Gtk4DrawArea *widget = nullptr) { fDrawArea = widget; } + + Bool_t HasTTFonts() const override { return kTRUE; } + + Bool_t IsNative() const override { return kTRUE; } + + void SetOpacity(Int_t percent) override; + + //2. "Off-screen management" part. + // return non-zero value to let execute painting code + Int_t CreateDrawable(UInt_t, UInt_t) override { return 223344; } + void ClearDrawable() override {} + void CopyDrawable(Int_t, Int_t, Int_t) override {} + void DestroyDrawable(Int_t) override {} + void SelectDrawable(Int_t) override {} + void SetDoubleBuffer(Int_t /* device */, Int_t /* mode */) override {} + + //jpg, png, bmp, gif output. + void SaveImage(TVirtualPad *, const char *, Int_t) const override; + + //TASImage support (noop for a non-gl pad). + void DrawPixels(const unsigned char *pixelData, UInt_t width, UInt_t height, + Int_t dstX, Int_t dstY, Bool_t enableAlphaBlending) override; + + void DrawLine(Double_t x1, Double_t y1, Double_t x2, Double_t y2) override; + void DrawLineNDC(Double_t u1, Double_t v1, Double_t u2, Double_t v2) override; + + void DrawBox(Double_t x1, Double_t y1, Double_t x2, Double_t y2, EBoxMode mode) override; + //TPad needs double and float versions. + void DrawFillArea(Int_t n, const Double_t *x, const Double_t *y) override; + void DrawFillArea(Int_t n, const Float_t *x, const Float_t *y) override; + + //TPad needs both double and float versions of DrawPolyLine. + void DrawPolyLine(Int_t n, const Double_t *x, const Double_t *y) override; + void DrawPolyLine(Int_t n, const Float_t *x, const Float_t *y) override; + void DrawPolyLineNDC(Int_t n, const Double_t *u, const Double_t *v) override; + + //TPad needs both versions. + void DrawPolyMarker(Int_t n, const Double_t *x, const Double_t *y) override; + void DrawPolyMarker(Int_t n, const Float_t *x, const Float_t *y) override; + + void DrawTTFglyphs(Int_t x, Int_t y, TTFhandle &ttf, ETextMode mode) override; + + void DrawImage(TImage *img, Int_t x, Int_t y, Int_t flags = 0) override; + + Bool_t FillImageFromPad(TImage *image, TVirtualPad *pad) const override; + + Bool_t IsSupportAlpha() const override { return kTRUE; } + +private: + //Let's make this clear: + TGtk4PadPainter(const TGtk4PadPainter &rhs) = delete; + TGtk4PadPainter(TGtk4PadPainter && rhs) = delete; + TGtk4PadPainter & operator = (const TGtk4PadPainter &rhs) = delete; + TGtk4PadPainter & operator = (TGtk4PadPainter && rhs) = delete; + + ClassDefOverride(TGtk4PadPainter, 0) // Pad painter on Gtk4 canvas +}; + +} // namespace Experimental +} // namespace ROOT + +#endif diff --git a/gui/gtk4canvas/index.md b/gui/gtk4canvas/index.md new file mode 100644 index 0000000000000..1e6ce2479d313 --- /dev/null +++ b/gui/gtk4canvas/index.md @@ -0,0 +1,28 @@ +\defgroup gtk4canvas GTK4 Canvas Display +\ingroup gpad +\brief Classes for canvas display using GTK4 + +CAUTION! Highly experimental, constantly changing code + +Provides alternative graphics implementation in the ROOT. +Allows non-GL painting in GTK4. Implementes: + - painting via TVirtualPadPainter interface + - canvas menu + - status bar + - objects context menu + - basics graphical editors + +To use this class one need to compile ROOT with `-Dgtk4canv=ON` flag. +GTK4 library should be installed on the system. +To activate GTK4 canvas in ROOT application one should create .rootrc file with following entries: + +``` +Gui.Factory: gtk4 +Root.PadEditor: gtk4 +``` + + +For compilation on windows one can use `vcpkg` to download all necessary gtk4 libraries. +When calling cmake, one can provide following arguments: + + -DCMAKE_TOOLCHAIN_FILE="C:\git\vcpkg\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows \ No newline at end of file diff --git a/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx b/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx new file mode 100644 index 0000000000000..c41dc7afaa4a2 --- /dev/null +++ b/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx @@ -0,0 +1,629 @@ +// Author: Sergey Linev, GSI 12/08/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +/* gtkmm example Copyright (C) 2002 gtkmm development team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, see . + */ + +#include "Gtk4CanvasWindow.h" + +#include + +#include +#include + +#include "TROOT.h" +#include "TTimer.h" +#include "TFile.h" +#include "TCanvas.h" +#include "TError.h" +#include "TApplication.h" + + +enum { kFileNewCanvas, kFileOpen, kFileSaveAs, kFilePrint, kOptionInterrupt, kOptionRefresh, + kInspectRoot, kToolsBrowser, kToolModify, kToolArc, kToolLine, kToolArrow, kToolDiamond, + kToolEllipse, kToolPad, kToolPave, kToolPLabel, kToolPText, kToolPsText, kToolGraph, + kToolCurlyLine, kToolCurlyArc, kToolLatex, kToolMarker, kToolCutG }; + +struct ToolBarData_t { + const char *fPixmap; + const char *fTipText; + Bool_t fStayDown; + Int_t fId; + void *fButton; +}; + +static ToolBarData_t toolBarData[] = { + // { filename, tooltip, staydown, id, button} + { "newcanvas.xpm", "New", kFALSE, kFileNewCanvas, 0 }, + { "open.xpm", "Open", kFALSE, kFileOpen, 0 }, + { "save.xpm", "Save As", kFALSE, kFileSaveAs, 0 }, + { "printer.xpm", "Print", kFALSE, kFilePrint, 0 }, + { "", "", kFALSE, -1, 0 }, + { "interrupt.xpm", "Interrupt", kFALSE, kOptionInterrupt,0 }, + { "refresh2.xpm", "Refresh", kFALSE, kOptionRefresh, 0 }, + { "", "", kFALSE, -1, 0 }, + { "inspect.xpm", "Inspect", kFALSE, kInspectRoot, 0 }, +// { "browser.xpm", "Browser", kFALSE, kToolsBrowser, 0 }, + { "", "", kFALSE, -1, 0 }, + { "pointer.xpm", "Modify", kFALSE, kToolModify, 0 }, + { "arc.xpm", "Arc", kFALSE, kToolArc, 0 }, + { "line.xpm", "Line", kFALSE, kToolLine, 0 }, + { "arrow.xpm", "Arrow", kFALSE, kToolArrow, 0 }, + { "diamond.xpm", "Diamond", kFALSE, kToolDiamond, 0 }, + { "ellipse.xpm", "Ellipse", kFALSE, kToolEllipse, 0 }, + { "pad.xpm", "Pad", kFALSE, kToolPad, 0 }, + { "pave.xpm", "Pave", kFALSE, kToolPave, 0 }, + { "pavelabel.xpm", "Pave Label", kFALSE, kToolPLabel, 0 }, + { "pavetext.xpm", "Pave Text", kFALSE, kToolPText, 0 }, + { "pavestext.xpm", "Paves Text", kFALSE, kToolPsText, 0 }, + { "graph.xpm", "Graph", kFALSE, kToolGraph, 0 }, + { "curlyline.xpm", "Curly Line", kFALSE, kToolCurlyLine, 0 }, + { "curlyarc.xpm", "Curly Arc", kFALSE, kToolCurlyArc, 0 }, + { "latex.xpm", "Text/Latex", kFALSE, kToolLatex, 0 }, + { "marker.xpm", "Marker", kFALSE, kToolMarker, 0 }, + { "cut.xpm", "Graphical Cut", kFALSE, kToolCutG, 0 }, + { 0, 0, kFALSE, 0, 0 } +}; + + + +/** \class Gtk4CanvasWindow + \ingroup gtk4canvas +*/ + + +Gtk4CanvasWindow::Gtk4CanvasWindow(TCanvas *canv, unsigned width, unsigned height) +: m_Box(Gtk::Orientation::VERTICAL), + m_ToolBar(Gtk::Orientation::HORIZONTAL, 4) +{ + set_title("canvas widget"); + set_default_size(width, height); + + set_child(m_Box); //We can put a MenuBar at the top of the box and other stuff below it. + + // all actions used with canvas + + + fillMenuBar(canv); + + fillToolBar(); + + m_ToolBar.set_visible(canv->TestBit(TCanvas::kShowToolBar)); + m_Box.append(m_ToolBar); + + m_DrawArea.set_has_tooltip(canv->TestBit(TCanvas::kShowToolTips)); + m_DrawArea.set_hexpand(true); + m_DrawArea.set_vexpand(true); + m_Box.append(m_DrawArea); + + m_DrawArea.status_changed().connect(sigc::mem_fun(*this, &Gtk4CanvasWindow::on_status_changed)); + + fContextId = m_StatusBar.get_context_id("main_status"); + m_StatusBar.push("Ready to draw", fContextId); + m_StatusBar.set_visible(canv->TestBit(TCanvas::kShowEventStatus)); + m_Box.append(m_StatusBar); +} + +Gtk4CanvasWindow::~Gtk4CanvasWindow() +{ +} + +//////////////////////////////////////////////////////////////////////////////// +/// Fill or update canvas save menu +/// Only performed when canvas name is changed + +void Gtk4CanvasWindow::fillSaveMenu(TCanvas *canv) +{ + std::string name = canv ? canv->GetName() : ""; + if (name.empty()) + name = "c1"; + + if (fCanvasFileName == name) + return; + + fCanvasFileName = name; + + fSaveMenu->remove_all(); + + auto &fmts = GetSupportedFileFormats(); + R__ASSERT(fmts.size() % 3 == 0); + for (std::size_t p = 0; p < fmts.size(); p += 3) { + std::string ext = fmts[p+1]; + // cannot define filters with same extension - cannot distinguish which was selected + if ((ext != "C") && (ext != fmts[p + 2])) + continue; + fSaveMenu->append(fCanvasFileName + "." + ext, "canvas.save_" + ext); + } +} + +void Gtk4CanvasWindow::fillMenuBar(TCanvas *canv) +{ + m_action_group = Gio::SimpleActionGroup::create(); + + m_action_group->add_action("new", []() { std::cout << "New canvas Clicked!\n"; }); + m_action_group->add_action("open", [this]() { on_openRootFile(); }); + m_action_group->add_action("saveas", [this]() { on_saveCanvasAs(); }); + + auto &fmts = GetSupportedFileFormats(); + R__ASSERT(fmts.size() % 3 == 0); + for (std::size_t p = 0; p < fmts.size(); p += 3) { + std::string ext = fmts[p+1]; + std::string opt = fmts[p+2]; + // cannot define filters with same extension - cannot distinguish which was selected + if ((ext != "C") && (ext != opt)) + continue; + m_action_group->add_action("save_" + ext, [this, ext, opt]() { on_saveCanvas(ext, opt); }); + } + + m_action_group->add_action("close", [this]() { close(); }); + m_action_group->add_action("quit", []() { + gROOT->SetInterrupt(kTRUE); + + if (gApplication) + TTimer::SingleShot(100, "TApplication", gApplication, "Terminate()"); + }); + + bool isShowToolbar = canv->TestBit(TCanvas::kShowToolBar); + + bool isShowEventStatus = canv->TestBit(TCanvas::kShowEventStatus); + + bool isShowToolTips = canv->TestBit(TCanvas::kShowToolTips); + + bool isShowMenuBar = canv->TestBit(TCanvas::kMenuBar); + + auto toggle_toolbar = Gio::SimpleAction::create_bool("show-toolbar", isShowToolbar); + toggle_toolbar->signal_change_state().connect(sigc::mem_fun(*this, &Gtk4CanvasWindow::on_show_toolbar_changed)); + m_action_group->add_action(toggle_toolbar); + + auto toggle_statusbar = Gio::SimpleAction::create_bool("show-eventstatus", isShowEventStatus); + toggle_statusbar->signal_change_state().connect(sigc::mem_fun(*this, &Gtk4CanvasWindow::on_show_eventstatus_changed)); + m_action_group->add_action(toggle_statusbar); + + auto toggle_tooltip = Gio::SimpleAction::create_bool("show-tooltip", isShowToolTips); + toggle_tooltip->signal_change_state().connect(sigc::mem_fun(*this, &Gtk4CanvasWindow::on_show_tooltip_changed)); + m_action_group->add_action(toggle_tooltip); + + + // Bind actions to an identifier prefix name string ("win.new", "win.copy", etc.) + insert_action_group("canvas", m_action_group); + + // 3. Construct the Abstract Menu Tree Model + auto menu_model = Gio::Menu::create(); + + + // --- SECTION "File" + + auto sec1 = Gio::Menu::create(); + sec1->append("New Canvas", "canvas.new"); + sec1->append("Open", "canvas.open"); + sec1->append("Close Canvas", "canvas.close"); + + auto sec2 = Gio::Menu::create(); + + fSaveMenu = Gio::Menu::create(); + fillSaveMenu(canv); + sec2->append_submenu("Save", fSaveMenu); + sec2->append("Save as...", "canvas.saveas"); + + auto sec3 = Gio::Menu::create(); + sec3->append("Quit ROOT", "canvas.quit"); + + auto file_menu = Gio::Menu::create(); + file_menu->append_section(sec1); + file_menu->append_section(sec2); + file_menu->append_section(sec3); + + // Attach File structure onto root Menu Model Bar + menu_model->append_submenu("File", file_menu); + + + // --- SECTION "Edit" + auto edit_menu = Gio::Menu::create(); + edit_menu->append("Clear pad", "canvas.clearpad"); + edit_menu->append("Clear canvas", "canvas.clearcanvas"); + + // CREATE A SUB-MENU ITEM (Nested Layer) + auto special_paste_submenu = Gio::Menu::create(); + special_paste_submenu->append("Paste Text Only", "canvas.paste"); + special_paste_submenu->append("Paste with Formatting", "canvas.paste"); + + // Nest the submenu inside the parent Edit menu container + edit_menu->append_submenu("Paste Special...", special_paste_submenu); + + // Attach Edit structure onto root Menu Model Bar + menu_model->append_submenu("Edit", edit_menu); + + + // --- SECTION "View" + auto view_menu = Gio::Menu::create(); + view_menu->append("Editor", "canvas.show-editor"); + view_menu->append("Toolbar", "canvas.show-toolbar"); + view_menu->append("Event Status", "canvas.show-eventstatus"); + view_menu->append("Tooltip", "canvas.show-tooltip"); + + menu_model->append_submenu("View", view_menu); + + // 4. Instantiate the PopoverMenuBar from our structured menu tree + // Gtk::PopoverMenuBar requires a Glib::RefPtr + auto menu_bar = Gtk::make_managed(menu_model); + + menu_bar->set_visible(isShowMenuBar); + + menu_bar->signal_state_flags_changed().connect([this](Gtk::StateFlags) { + fillSaveMenu(m_DrawArea.GetCanvas()); + }); + + m_Box.append(*menu_bar); +} + + +Gtk::Image* create_resized_xpm_image(const char *filename) +{ + // 2. Initialize a pointer for our final texture image + Glib::RefPtr texture; + + try { + // Load the original XPM file safely into a Pixbuf + auto original_pixbuf = Gdk::Pixbuf::create_from_file(filename); + + // Force-resize the XPM image to precisely 16x16 using Bilinear interpolation + auto scaled_pixbuf = original_pixbuf->scale_simple(16, 16, Gdk::InterpType::BILINEAR); + + // Convert the scaled Pixbuf into a GTK4 Paintable Texture + texture = Gdk::Texture::create_for_pixbuf(scaled_pixbuf); + + } catch (const Glib::FileError& e) { + std::cerr << "File system error: " << e.what() << std::endl; + return nullptr; // Returns an empty image + } catch (const Gdk::PixbufError& e) { + std::cerr << "Failed to parse XPM image format: " << e.what() << std::endl; + return nullptr; // Returns an empty image + } + + // 3. Create the GTK4 Image widget directly from the validated texture + auto image = Gtk::make_managed(texture); + + return image; +} + + +void Gtk4CanvasWindow::fillToolBar() +{ + + auto css_provider = Gtk::CssProvider::create(); + + // Target the button widget structure to minimize interior padding spaces + css_provider->load_from_data("button { padding: 2px 2px; min-width: 0; min-height: 0; }"); + + for (int i = 0; toolBarData[i].fPixmap; i++) { + auto btn = &(toolBarData[i]); + if (strlen(btn->fPixmap) == 0) { + // fToolBar->addSeparator(); + auto separator = Gtk::make_managed(Gtk::Orientation::VERTICAL); + m_ToolBar.append(*separator); + continue; + } + + auto button = Gtk::make_managed(); + + TString iconname = TROOT::GetIconPath(); + iconname.Append("/"); + iconname.Append(btn->fPixmap); + + // 2. Load the icon from the file path + auto image = create_resized_xpm_image(iconname.Data()); + + if (image) + button->set_child(*image); + + button->get_style_context()->add_provider(css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + button->set_tooltip_text(btn->fTipText); + + button->signal_clicked().connect([this, btn]() { + clickToolButton(btn->fId); + }); + + + m_ToolBar.append(*button); + } +} + +void Gtk4CanvasWindow::clickToolButton(int id) +{ + auto canv = m_DrawArea.GetCanvas(); + switch (id) { + case kFileNewCanvas: + gROOT->MakeDefCanvas(); + break; + case kFileOpen: + on_openRootFile(); + break; + case kFileSaveAs: + on_saveCanvasAs(); + break; + case kFilePrint: + on_printCanvas(); + break; + case kOptionInterrupt: gROOT->SetInterrupt(); break; + case kOptionRefresh: + if (canv) { + canv->Modified(kTRUE); + // trigger update of the paint widget, paint will be called + canv->Update(); + } + break; + case kInspectRoot: + if (canv) { + canv->cd(); + gROOT->Inspect(); + canv->Update(); + } + break; + + case kToolsBrowser: break; + case kToolModify: gROOT->SetEditorMode(); break; + case kToolArc: gROOT->SetEditorMode("Arc"); break; + case kToolLine: gROOT->SetEditorMode("Line"); break; + case kToolArrow: gROOT->SetEditorMode("Arrow"); break; + case kToolDiamond: gROOT->SetEditorMode("Diamond"); break; + case kToolEllipse: gROOT->SetEditorMode("Ellipse"); break; + case kToolPad: gROOT->SetEditorMode("Pad"); break; + case kToolPave: gROOT->SetEditorMode("Pave"); break; + case kToolPLabel: gROOT->SetEditorMode("PaveLabel"); break; + case kToolPText: gROOT->SetEditorMode("PaveText"); break; + case kToolPsText: gROOT->SetEditorMode("PavesText"); break; + case kToolGraph: gROOT->SetEditorMode("PolyLine"); break; + case kToolCurlyLine: gROOT->SetEditorMode("CurlyLine"); break; + case kToolCurlyArc: gROOT->SetEditorMode("CurlyArc"); break; + case kToolLatex: gROOT->SetEditorMode("Text"); break; + case kToolMarker: gROOT->SetEditorMode("Marker"); break; + case kToolCutG: gROOT->SetEditorMode("CutG"); break; + } +} + +void Gtk4CanvasWindow::on_status_changed(const char *msg) +{ + m_StatusBar.remove_all_messages(fContextId); + m_StatusBar.push(msg, fContextId); +} + +void Gtk4CanvasWindow::on_show_toolbar_changed(const Glib::VariantBase& value) +{ + bool is_checked = Glib::VariantBase::cast_dynamic>(value).get(); + + m_ToolBar.set_visible(is_checked); + + // Remember to update the actual action state so the checkmark reflects visually + auto action = m_action_group->lookup_action("show-toolbar"); + if (action) { + auto simple_action = std::dynamic_pointer_cast(action); + simple_action->set_state(value); + } +} + +void Gtk4CanvasWindow::on_show_eventstatus_changed(const Glib::VariantBase &value) +{ + bool is_checked = Glib::VariantBase::cast_dynamic>(value).get(); + + m_StatusBar.set_visible(is_checked); + + // Remember to update the actual action state so the checkmark reflects visually + auto action = m_action_group->lookup_action("show-eventstatus"); + if (action) { + auto simple_action = std::dynamic_pointer_cast(action); + simple_action->set_state(value); + } +} + +void Gtk4CanvasWindow::on_show_tooltip_changed(const Glib::VariantBase &value) +{ + bool is_checked = Glib::VariantBase::cast_dynamic>(value).get(); + + m_DrawArea.set_has_tooltip(is_checked); + + // Remember to update the actual action state so the checkmark reflects visually + auto action = m_action_group->lookup_action("show-tooltip"); + if (action) { + auto simple_action = std::dynamic_pointer_cast(action); + simple_action->set_state(value); + } + +} + +void Gtk4CanvasWindow::on_openRootFile() +{ + if (!m_file_dialog) { + m_file_dialog = Gtk::FileDialog::create(); + m_file_dialog->set_title("Select a ROOT File"); + m_file_dialog->set_modal(true); + + auto filter_root_files = Gtk::FileFilter::create(); + filter_root_files->set_name("ROOT files (*.root)"); + filter_root_files->add_pattern("*.root"); + + auto filter_xml_files = Gtk::FileFilter::create(); + filter_xml_files->set_name("ROOT XML files (*.xml)"); + filter_xml_files->add_pattern("*.xml"); + + auto filter_all_files = Gtk::FileFilter::create(); + filter_all_files->set_name("All files (*.*)"); + filter_all_files->add_pattern("*.*"); + + auto filters_list = Gio::ListStore::create(); + filters_list->append(filter_root_files); + filters_list->append(filter_xml_files); + filters_list->append(filter_all_files); + + m_file_dialog->set_filters(filters_list); + + // Optional: Set which filter is selected by default (e.g., Images) + m_file_dialog->set_default_filter(filter_root_files); + } + + m_file_dialog->open(*this, sigc::mem_fun(*this, &Gtk4CanvasWindow::on_file_selected)); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Signal handler when open file dialog select file + +void Gtk4CanvasWindow::on_file_selected(const Glib::RefPtr &result) +{ + try { + // Retrieve the selected file reference + Glib::RefPtr file = m_file_dialog->open_finish(result); + + TFile::Open(file->get_path().c_str(), "update"); + + } catch (const Gtk::DialogError &err) { + // Triggers if the user cancels or closes the dialog window + // std::cout << "Dialog closed or dismissed: " << err.what() << std::endl; + } +} + +//////////////////////////////////////////////////////////////////////////////// +/// Return vector with supported file formats with description and print option +/// To be moved in base class + +const std::vector &Gtk4CanvasWindow::GetSupportedFileFormats() const +{ + static std::vector lst = { + "Post Script", "ps", "ps", + "Post Script Portrait", "ps", "Portrait", + "Post Script Landscape", "ps", "Landscape", + "Encapsulated Post Script", "eps", "eps", + "Encapsulated Post Script preview", "eps", "Preview", + "GIF format", "gif", "gif", + "PDF format", "pdf", "pdf", + "SVG format", "svg", "svg", + "XPM format", "xpm", "xpm", + "PNG format", "png", "png", + "JPG format", "jpg", "jpg", + "BMP format", "bmp", "bmp", + "TIFF format", "tiff", "tiff", + "C++ Macro", "C", "cxx", + "json file", "json", "json", + "html file", "html", "html", + "root file", "root", "root" + }; + return lst; +} + + +//////////////////////////////////////////////////////////////////////////////// +/// Save canvas with specified format + +void Gtk4CanvasWindow::on_saveCanvas(const std::string &fmt, const std::string &opt) +{ + auto canv = m_DrawArea.GetCanvas(); + if (!canv) + return; + std::string fname = fCanvasFileName + "." + fmt; + canv->Print(fname.c_str(), opt.c_str()); +} + + +//////////////////////////////////////////////////////////////////////////////// +/// Start Save as dialog + +void Gtk4CanvasWindow::on_saveCanvasAs() +{ + if (!m_save_dialog) { + m_save_dialog = Gtk::FileDialog::create(); + m_save_dialog->set_title("Save canvas as"); + m_save_dialog->set_modal(true); + + auto filters_list = Gio::ListStore::create(); + + auto &fmts = GetSupportedFileFormats(); + R__ASSERT(fmts.size() % 3 == 0); + for (std::size_t p = 0; p < fmts.size(); p += 3) { + std::string ext = fmts[p + 1]; + // cannot define filters with same extension - cannot distinguish which was selected + if ((ext != "C") && (ext != fmts[p + 2])) + continue; + auto name = TString::Format("%s (*.%s)", fmts[p].c_str(), ext.c_str()); + auto pattern = TString::Format("(*.%s)", ext.c_str()); + + auto filter = Gtk::FileFilter::create(); + filter->set_name(name.Data()); + filter->add_pattern(pattern.Data()); + filters_list->append(filter); + + if (fmts[p+1] == "png") + m_save_dialog->set_default_filter(filter); + } + + m_save_dialog->set_filters(filters_list); + } + + auto canv = m_DrawArea.GetCanvas(); + if (canv) { + std::string name = canv->GetName(); + m_save_dialog->set_initial_name(name + ".png"); + } + + m_save_dialog->save(*this, sigc::mem_fun(*this, &Gtk4CanvasWindow::on_save_selected)); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Signal handler when save as dialog select file + +void Gtk4CanvasWindow::on_save_selected(const Glib::RefPtr &result) +{ + auto canv = m_DrawArea.GetCanvas(); + if (!canv) + return; + + try { + Glib::RefPtr file = m_save_dialog->save_finish(result); + + std::string raw_path = file->get_path(); + std::filesystem::path sys_path(raw_path); + std::string extension = sys_path.extension().string(); // + std::string print_opt; + + auto &fmts = GetSupportedFileFormats(); + R__ASSERT(fmts.size() % 3 == 0); + for (std::size_t p = 0; p < fmts.size(); p += 3) { + if (extension == "." + fmts[p + 1]) { + print_opt = fmts[p + 2]; + break; + } + } + + if (print_opt.empty()) + ::Error("Gtk4CanvasWindow::on_save_selected", "Cannot detect file format from selected extension %s", extension.c_str()); + else { + printf("Store canvas %s as %s opt %s\n", canv->ClassName(), raw_path.c_str(), print_opt.c_str()); + canv->Print(raw_path.c_str(), print_opt.c_str()); + } + + } catch (const Gtk::DialogError &err) { + } +} + + +void Gtk4CanvasWindow::on_printCanvas() +{ +} diff --git a/gui/gtk4canvas/src/Gtk4CanvasWindow.h b/gui/gtk4canvas/src/Gtk4CanvasWindow.h new file mode 100644 index 0000000000000..cba611be94f44 --- /dev/null +++ b/gui/gtk4canvas/src/Gtk4CanvasWindow.h @@ -0,0 +1,89 @@ +// Author: Sergey Linev, GSI 12/08/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +/* gtkmm example Copyright (C) 2002 gtkmm development team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, see . + */ + +#ifndef ROOT_Gtk4CanvasWindow_H +#define ROOT_Gtk4CanvasWindow_H + +#include + +#include "Gtk4DrawArea.h" +#include +#include + +class TCanvas; + +class Gtk4CanvasWindow : public Gtk::Window { +public: + Gtk4CanvasWindow(TCanvas *canv = nullptr, unsigned width = 400, unsigned height = 200); + virtual ~Gtk4CanvasWindow(); + + Gtk4DrawArea *GetDrawArea() { return &m_DrawArea; } + +private: + const std::vector &GetSupportedFileFormats() const; + + // Signal handlers: + + void on_status_changed(const char *msg); + void on_show_toolbar_changed(const Glib::VariantBase &); + void on_show_eventstatus_changed(const Glib::VariantBase &); + void on_show_tooltip_changed(const Glib::VariantBase &); + void on_file_selected(const Glib::RefPtr &result); + void on_save_selected(const Glib::RefPtr &result); + void on_openRootFile(); + void on_saveCanvas(const std::string &fname, const std::string &opt); + void on_saveCanvasAs(); + void on_printCanvas(); + + // void on_action_file_new(); + // void on_action_file_quit(); + // void on_action_others(); + // void on_action_toggle(); + + void fillSaveMenu(TCanvas *canv); + void fillMenuBar(TCanvas *canv); + + void fillToolBar(); + + void clickToolButton(int id); + + // Child widgets: + Gtk::Box m_Box; + + Glib::RefPtr m_action_group; + std::string fCanvasFileName; + Glib::RefPtr fSaveMenu; + + Glib::RefPtr m_file_dialog; + Glib::RefPtr m_save_dialog; + + Gtk::Box m_ToolBar; + Gtk4DrawArea m_DrawArea; + Gtk::Statusbar m_StatusBar; + unsigned int fContextId = 0; +}; + +#endif //GTKMM_Gtk4CanvasWindow_H diff --git a/gui/gtk4canvas/src/Gtk4DrawArea.cxx b/gui/gtk4canvas/src/Gtk4DrawArea.cxx new file mode 100644 index 0000000000000..f6c680037e230 --- /dev/null +++ b/gui/gtk4canvas/src/Gtk4DrawArea.cxx @@ -0,0 +1,314 @@ +// Author: Sergey Linev, GSI 12/08/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +#include "Gtk4DrawArea.h" + +#include + +#include "TGtk4PadPainter.h" +#include "TCanvas.h" + +#include + +/** \class Gtk4DrawArea + \ingroup gtk4canvas +*/ + + +Gtk4DrawArea::Gtk4DrawArea() +{ + // Bind the member function as the draw function + set_draw_func(sigc::mem_fun(*this, &Gtk4DrawArea::on_draw)); + + set_expand(true); + + m_click_gesture = Gtk::GestureClick::create(); + m_click_gesture->set_button(0); + m_click_gesture->signal_pressed().connect( + sigc::mem_fun(*this, &Gtk4DrawArea::on_mouse_click) + ); + m_click_gesture->signal_released().connect( + sigc::mem_fun(*this, &Gtk4DrawArea::on_mouse_released) + ); + add_controller(m_click_gesture); + + // 2. Create and attach the motion event controller + m_motion_controller = Gtk::EventControllerMotion::create(); + m_motion_controller->signal_motion().connect( + sigc::mem_fun(*this, &Gtk4DrawArea::on_mouse_move) + ); + + m_motion_controller->signal_enter().connect( + sigc::mem_fun(*this, &Gtk4DrawArea::on_mouse_enter) + ); + + // 3. Connect the leave signal + m_motion_controller->signal_leave().connect( + sigc::mem_fun(*this, &Gtk4DrawArea::on_mouse_leave) + ); + add_controller(m_motion_controller); + + m_scroll_controller = Gtk::EventControllerScroll::create(); + m_scroll_controller->set_flags(Gtk::EventControllerScroll::Flags::VERTICAL); + + m_scroll_controller->signal_scroll().connect( + sigc::mem_fun(*this, &Gtk4DrawArea::on_mouse_scroll), + false // Run during the capture phase? False means bubble phase (standard) + ); + + add_controller(m_scroll_controller); + + + signal_query_tooltip().connect(sigc::mem_fun(*this, &Gtk4DrawArea::on_my_query_tooltip), true); +} + +void Gtk4DrawArea::on_draw(const Cairo::RefPtr& cr, int width, int height) +{ + if (!fCanvas) { + // only for debug purposes to see not empty widget + cr->set_source_rgb(0.15, 0.15, 0.15); + cr->paint(); + + // 2. Draw a blue circle in the center that scales with the window + double radius = std::min(width, height) / 4.0; + cr->set_source_rgb(0.2, 0.5, 0.8); + cr->arc(width / 2.0, height / 2.0, radius, 0.0, 2.0 * 3.1415927); + cr->fill(); + return; + } + + if ((width != (int) fCanvas->GetPadWidth()) || (height != (int) fCanvas->GetPadHeight())) + fCanvas->Resize(); + + if (!fDrawingCache || fDrawingCache->get_width() != width || fDrawingCache->get_height() != height) + fDrawingCache = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, width, height); + + // TODO: extend caching for each pad, making it very similar to double-buffer model of gVirtualX + + auto cache_context = Cairo::Context::create(fDrawingCache); + + cache_context->set_source_rgb(1., 1., 1.); + cache_context->paint(); + + // main place where painting happens - only here fContext pointer is set + // TODO: really repaint pad only when something changed + + fContext = cache_context.get(); + + fCanvas->Paint(); + + fContext = nullptr; + + // and now copy image to real context + cr->set_source(fDrawingCache, 0, 0); + cr->paint(); +} + +void Gtk4DrawArea::on_mouse_click(int n_press, double x, double y) +{ + if (!fCanvas) + return; + + auto evnt = kNoEvent; + + switch(m_click_gesture->get_current_button()) { + case GDK_BUTTON_PRIMARY: // 1, usually left + evnt = n_press > 1 ? kButton1Double : kButton1Down; + break; + case GDK_BUTTON_MIDDLE: // 2 + evnt = n_press > 1 ? kButton2Double : kButton2Down; + break; + case GDK_BUTTON_SECONDARY: // 3, usually right + evnt = n_press > 1 ? kButton3Double : kButton3Down; + break; + default: + break; + } + + if (evnt != kNoEvent) + fCanvas->HandleInput(evnt, (int) x, (int) y); +} + +void Gtk4DrawArea::on_mouse_released(int n_press, double x, double y) +{ + (void) n_press; + + if (!fCanvas) + return; + + auto evnt = kNoEvent; + + switch(m_click_gesture->get_current_button()) { + case GDK_BUTTON_PRIMARY: // 1, usually left + evnt = kButton1Up; + break; + case GDK_BUTTON_MIDDLE: // 2 + evnt = kButton2Up; + break; + case GDK_BUTTON_SECONDARY: // 3, usually right + evnt = kButton3Up; + break; + default: + break; + } + + if (evnt != kNoEvent) + fCanvas->HandleInput(evnt, (int) x, (int) y); +} + + +void Gtk4DrawArea::on_mouse_enter(double x, double y) +{ + fLastMouseX = x; + fLastMouseY = y; + + if (fCanvas) + fCanvas->HandleInput(kMouseEnter, (int) x, (int) y); +} + +void Gtk4DrawArea::on_mouse_leave() +{ + if (fCanvas) + fCanvas->HandleInput(kMouseLeave, 0, 0); +} + + +// Callback handles mouse movements +void Gtk4DrawArea::on_mouse_move(double x, double y) +{ + if (!fCanvas) + return; + + fLastMouseX = x; + fLastMouseY = y; + + Gdk::ModifierType modifiers = m_motion_controller->get_current_event_state(); + + auto evnt = kMouseMotion; + if ((modifiers & Gdk::ModifierType::BUTTON1_MASK) == Gdk::ModifierType::BUTTON1_MASK) { + evnt = kButton1Motion; + } else if ((modifiers & Gdk::ModifierType::BUTTON3_MASK) == Gdk::ModifierType::BUTTON3_MASK) { + evnt = kButton3Motion; + } + + fCanvas->HandleInput(evnt, (int) x, (int) y); + + if (fShowEventStatus) { + TObject *selected = fCanvas->GetSelected(); + Int_t px = fCanvas->GetEventX(); + Int_t py = fCanvas->GetEventY(); + TString buffer = ""; + if (selected) { + buffer = selected->GetName(); + buffer.Append(" "); + buffer.Append(selected->GetObjectInfo(px, py)); + } else { + buffer.Form("No selected object x = %d y = %d", px, py); + } + + m_status_changed.emit(buffer.Data()); + } +} + +sigc::signal Gtk4DrawArea::status_changed() +{ + return m_status_changed; +} + + +bool Gtk4DrawArea::on_mouse_scroll(double dx, double dy) +{ + (void) dx; + if (fCanvas) + fCanvas->HandleInput(dy < 0 ? kWheelUp : kWheelDown, (int) fLastMouseX, (int) fLastMouseY); + + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////// +/// Shows context menu with provided items and title at specified position + +void Gtk4DrawArea::ShowContextMenu(Glib::RefPtr menu_model, const char *title, int x, int y) +{ + if (title && *title) { + auto item = Gio::MenuItem::create("", ""); + item->set_attribute_value("custom", Glib::Variant::create("header")); + menu_model->prepend_item(item); + } + + auto menu = Gtk::make_managed(menu_model); + menu->set_parent(*this); + menu->set_has_arrow(false); // Removes the little popover triangle point + + menu->add_css_class("compact-menu"); + + static bool has_compact_style = false; + if (!has_compact_style) { + auto css_provider = Gtk::CssProvider::create(); + std::string css_data = + "popover.compact-menu contents modelbutton, " + "popover.compact-menu contents button.model { " + " padding-top: 1px; " + " padding-bottom: 0px; " + " margin-top: 0px; " + " margin-bottom: 0px; " + " min-height: 10px; " + "}"; + + css_provider->load_from_data(css_data); + + // 4. Inject into the display context + Gtk::StyleContext::add_provider_for_display( + Gdk::Display::get_default(), + css_provider, + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + ); + + has_compact_style = true; + } + + if (title && *title) { + auto label = Gtk::make_managed(title); + label->add_css_class("heading"); + menu->add_child(*label, "header"); + } + + Gdk::Rectangle rect(x, y, 1, 1); + + // 2. Snap the context menu anchor to this exact tiny point rectangle + menu->set_pointing_to(rect); + + // 3. Open the popup menu smoothly on screen + menu->popup(); +} + +bool Gtk4DrawArea::on_my_query_tooltip(int x, int y, bool keyboard_tooltip, const Glib::RefPtr& tooltip) +{ + if (keyboard_tooltip) + return false; // Skip if triggered by keyboard focus + + auto canv = GetCanvas(); + if (!canv) + return false; + + TObject *selected = canv->GetSelected(); + Int_t px = canv->GetEventX(); + Int_t py = canv->GetEventY(); + TString customText; + if (selected) + customText.Form("%s::%s\n%s\n%d, %d\n%s",selected->ClassName() ,selected->GetName(), + selected->GetTitle(), px, py, selected->GetObjectInfo(px, py)); + else + customText.Form("No selected object\n%d, %d", x, y); + + tooltip->set_text(customText.Data()); + + return true; // Return true to instruct GTK to show the tooltip +} diff --git a/gui/gtk4canvas/src/Gtk4DrawArea.h b/gui/gtk4canvas/src/Gtk4DrawArea.h new file mode 100644 index 0000000000000..2a31f10e13f43 --- /dev/null +++ b/gui/gtk4canvas/src/Gtk4DrawArea.h @@ -0,0 +1,63 @@ +// Author: Sergey Linev, GSI 12/08/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +#ifndef ROOT_Gtk4DrawArea_H +#define ROOT_Gtk4DrawArea_H + +#include +#include + +class TCanvas; + +class Gtk4DrawArea : public Gtk::DrawingArea { +public: + Gtk4DrawArea(); + virtual ~Gtk4DrawArea() = default; + + void SetCanvas(TCanvas *c) { fCanvas = c; } + TCanvas *GetCanvas() const { return fCanvas; } + + Cairo::Context *GetContext() const { return fContext; } + + Cairo::ImageSurface *GetCache() const { return fDrawingCache.get(); } + + void ShowContextMenu(Glib::RefPtr menu_model, const char *title, int x, int y); + + sigc::signal status_changed(); + + + +protected: + // The render function for gtkmm 4 + void on_draw(const Cairo::RefPtr& cr, int width, int height); + + TCanvas *fCanvas = nullptr; + Cairo::Context *fContext = nullptr; + double fLastMouseX = 0; + double fLastMouseY = 0; + bool fShowEventStatus = true; + Cairo::RefPtr fDrawingCache; + + void on_mouse_click(int n_press, double x, double y); + void on_mouse_released(int n_press, double x, double y); + void on_mouse_enter(double x, double y); + void on_mouse_move(double x, double y); + void on_mouse_leave(); + bool on_mouse_scroll(double dx, double dy); + bool on_my_query_tooltip(int x, int y, bool keyboard_tooltip, const Glib::RefPtr& tooltip); + + sigc::signal m_status_changed; + + Glib::RefPtr m_click_gesture; + Glib::RefPtr m_motion_controller; + Glib::RefPtr m_scroll_controller; +}; + +#endif diff --git a/gui/gtk4canvas/src/Gtk4MethodDialog.cxx b/gui/gtk4canvas/src/Gtk4MethodDialog.cxx new file mode 100644 index 0000000000000..47b3ba69b8aed --- /dev/null +++ b/gui/gtk4canvas/src/Gtk4MethodDialog.cxx @@ -0,0 +1,253 @@ +// Author: Sergey Linev, GSI 14/08/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +#include "Gtk4MethodDialog.h" + +#include "TString.h" +#include "TROOT.h" +#include "TVirtualPad.h" +#include "TError.h" +#include "TMethod.h" +#include "TMethodArg.h" +#include "TMethodCall.h" +#include "TObjString.h" +#include "TContextMenu.h" +#include "TColor.h" + +#include + +/** \class Gtk4MethodDialog + \ingroup gtk4canvas + +Specialized dialog to enter arguments for method exection +*/ + +//////////////////////////////////////////////////////////////////////////////// +/// Create method dialog + +Gtk4MethodDialog::Gtk4MethodDialog(unsigned width, unsigned height) +{ + set_title("method dialog"); + set_default_size(width, height); + + m_main_box = Gtk::make_managed(Gtk::Orientation::VERTICAL, 10); + m_main_box->set_margin(5); + set_child(*m_main_box); +} + +//////////////////////////////////////////////////////////////////////////////// +/// clear all elements inside dialog main box + +void Gtk4MethodDialog::clearAllElements() +{ + while (auto* child = m_main_box->get_first_child()) + m_main_box->remove(*child); +} + +//////////////////////////////////////////////////////////////////////////////// +/// add line with color button + +Gtk::ColorDialogButton *Gtk4MethodDialog::addColorInput(const char *title, Int_t colindx) +{ + TColor *rootColor = gROOT->GetColor(colindx); + Gdk::RGBA initial_color; + initial_color.set_rgba(0.0, 0.0, 0.0, 1.0); + + if (rootColor) + initial_color.set_rgba(rootColor->GetRed(), rootColor->GetGreen(), rootColor->GetBlue(), rootColor->GetAlpha()); + + auto color_dialog = Gtk::ColorDialog::create(); + color_dialog->set_title("Select a Custom Color"); + color_dialog->set_modal(true); + + // Instantiate the specific ColorDialogButton using our dialog rules + auto m_color_button = Gtk::make_managed(color_dialog); + m_color_button->set_halign(Gtk::Align::END); + + // Set an initial default color (e.g., solid Red) + m_color_button->set_rgba(initial_color); + + addLine(title, m_color_button); + + return m_color_button; +} + +//////////////////////////////////////////////////////////////////////////////// +/// Add single dialog line + +void Gtk4MethodDialog::addLine(const char *txt, Gtk::Widget *widget) +{ + auto layout_grid = Gtk::make_managed(); + layout_grid->set_column_spacing(15); + layout_grid->set_hexpand(true); + layout_grid->set_halign(Gtk::Align::FILL); + m_main_box->append(*layout_grid); + + auto label = Gtk::make_managed(txt); + label->set_halign(Gtk::Align::START); + label->set_hexpand(true); // Wants to expand + label->set_halign(Gtk::Align::FILL); // Stretch to fill its 50% allocation + label->set_xalign(0.0); + + + widget->set_hexpand(true); // Wants to expand equally with the label + widget->set_halign(Gtk::Align::FILL); + + layout_grid->attach(*label, 0, 0, 3, 1); + layout_grid->attach(*widget, 3, 0, 7, 1); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Add method argument + +void Gtk4MethodDialog::addArg(const char *argname, const char *value, const char *) +{ + auto entry = Gtk::make_managed(); + entry->set_text(value); + addLine(TString::Format("%s:", argname).Data(), entry); + fArgs.push_back(entry); +} + + +//////////////////////////////////////////////////////////////////////////////// +/// Add Ok and Cancel buttons + +Gtk::Button *Gtk4MethodDialog::addOkCancelButtons() +{ + auto hbox = Gtk::make_managed(); + hbox->set_hexpand(true); + hbox->set_halign(Gtk::Align::END); + m_main_box->append(*hbox); + + auto button1 = Gtk::make_managed("Ok"); + + auto button2 = Gtk::make_managed("Cancel"); + button2->signal_clicked().connect([this]() { close(); }); + + hbox->append(*button1); + hbox->append(*button2); + + return button1; +} + +//////////////////////////////////////////////////////////////////////////////// +/// Show method dialog + +void Gtk4MethodDialog::methodDialog(TContextMenu *menu, TObject *object, TFunction* func) +{ + if (!menu || !object || !func) + return; + + set_title(menu->CreateDialogTitle(object, func)); + + // iterate through all arguments and create appropriate input-data objects: + // inputlines, option menus... + TIter next(func->GetListOfMethodArgs()); + + while (auto argument = (TMethodArg *) next()) { + TString argTitle = menu->CreateArgumentTitle(argument); + TString type = argument->GetTypeName(); + TDataType *datatype = gROOT->GetType(type); + TString basictype; + + if (datatype) { + basictype = datatype->GetTypeName(); + } else { + if (type.CompareTo("enum") != 0) + std::cout << "*** Warning in Dialog(): data type is not basic type, assuming (int)\n"; + basictype = "int"; + } + + if (TString(argument->GetTitle()).Index("*") != kNPOS) { + basictype += "*"; + type = "char*"; + } + + TDataMember *m = argument->GetDataMember(); + if (m && m->GetterMethod()) { + + m->GetterMethod()->Init(object->IsA(), m->GetterMethod()->GetMethodName(), ""); + + // Get the current value and form it as a text: + + TString val; + + if (basictype == "char*") { + char *tdefval = nullptr; + m->GetterMethod()->Execute(object, "", &tdefval); + if (tdefval) val = tdefval; + } else + if ((basictype == "float") || + (basictype == "double")) { + Double_t ddefval = 0.; + m->GetterMethod()->Execute(object, "", ddefval); + val = TString::Format("%g", ddefval); + } else + if ((basictype == "char") || + (basictype == "int") || + (basictype == "bool") || + (basictype == "long") || + (basictype == "short")) { + Longptr_t ldefval = 0; + m->GetterMethod()->Execute(object, "", ldefval); + val = TString::Format("%ld", (long) ldefval); + } + + // Warn aout options ... + if (m->GetOptions()) + ::Warning("Gtk4MethodDialog::Dialog", "option menu not yet implemented"); + + addArg(argTitle.Data(), val.Data(), type.Data()); + } else { // if m not found ... + TString argDflt; + if (argument->GetDefault()) + argDflt = argument->GetDefault(); + + if ((argDflt.Length() > 1) && + (argDflt[0]=='\"') && (argDflt[argDflt.Length()-1]=='\"')) { + // cut "" from the string argument + argDflt.Remove(0,1); + argDflt.Remove(argDflt.Length()-1,1); + } + + addArg(argTitle.Data(), argDflt.Data(), type.Data()); + } + } + + auto okBtn = addOkCancelButtons(); + + okBtn->signal_clicked().connect(sigc::mem_fun(*this, &Gtk4MethodDialog::on_ok_button_clicked)); + + present(); + + fMenu = menu; + fObject = object; + fFunc = func; +} + + +void Gtk4MethodDialog::on_ok_button_clicked() +{ + if (fMenu && fObject) { + + TObjArray argslist(fArgs.size()); + + for (auto entry : fArgs) { + Glib::ustring v = entry->get_text(); + argslist.AddLast(new TObjString(v.c_str())); + } + + close(); + + fMenu->Execute(fObject, fFunc, &argslist); + } else { + close(); + } +} diff --git a/gui/gtk4canvas/src/Gtk4MethodDialog.h b/gui/gtk4canvas/src/Gtk4MethodDialog.h new file mode 100644 index 0000000000000..049d0d55017bc --- /dev/null +++ b/gui/gtk4canvas/src/Gtk4MethodDialog.h @@ -0,0 +1,51 @@ +// Author: Sergey Linev, GSI 14/08/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + + +#ifndef ROOT_Gtk4MethodDialog +#define ROOT_Gtk4MethodDialog + +#include +#include + +class TContextMenu; +class TObject; +class TFunction; + +class Gtk4MethodDialog: public Gtk::Window { + + public: + Gtk4MethodDialog(unsigned width = 400, unsigned height = 200); + + void methodDialog(TContextMenu *menu, TObject *object, TFunction* func); + + void clearAllElements(); + + void addLine(const char *txt, Gtk::Widget *widget); + + Gtk::ColorDialogButton *addColorInput(const char *title, int colindx); + + void addArg(const char *argname, const char *value, const char *type); + + Gtk::Button *addOkCancelButtons(); + + protected: + + void on_ok_button_clicked(); + + Gtk::Box *m_main_box = nullptr; + + std::vector fArgs; + TContextMenu *fMenu = nullptr; + TObject *fObject = nullptr; + TFunction *fFunc = nullptr; +}; + +#endif diff --git a/gui/gtk4canvas/src/TGtk4Canvas.cxx b/gui/gtk4canvas/src/TGtk4Canvas.cxx new file mode 100644 index 0000000000000..08e831e0dc5a7 --- /dev/null +++ b/gui/gtk4canvas/src/TGtk4Canvas.cxx @@ -0,0 +1,377 @@ +// Author: Sergey Linev, GSI 12/08/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +#include "TGtk4Canvas.h" + +#include "TGtk4PadPainter.h" + +#include "TSystem.h" +#include "TStyle.h" +#include "TCanvas.h" +#include "TROOT.h" +#include "TClass.h" + +#include +#include +#include +#include + +#include + +#include "Gtk4CanvasWindow.h" + +using namespace ROOT::Experimental; + +/** \class TGtk4Canvas + \ingroup gtk4canvas + \brief Basic TCanvasImp ABI implementation for Gtk4 +*/ + +//////////////////////////////////////////////////////////////////////////////// +/// Constructor + +TGtk4Canvas::TGtk4Canvas(TCanvas *c, const char *name, Int_t x, Int_t y, UInt_t width, UInt_t height) + : TCanvasImp(c, name, x, y, width, height) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +/// Destructor + +TGtk4Canvas::~TGtk4Canvas() +{ +} + +//////////////////////////////////////////////////////////////////////////////// +/// Initialize window for the Gtk4 canvas + +Int_t TGtk4Canvas::InitWindow() +{ + return 111222333; // should not be used at all +} + +//////////////////////////////////////////////////////////////////////////////// +/// Creates pad painter + +TVirtualPadPainter *TGtk4Canvas::CreatePadPainter() +{ + return new TGtk4PadPainter(fDrawArea); +} + +////////////////////////////////////////////////////////////////////////////////////////// +/// Close canvas - not implemented + +void TGtk4Canvas::Close() +{ +} + +////////////////////////////////////////////////////////////////////////////////////////// +/// Show canvas + +void TGtk4Canvas::Show() +{ + if (fCanvasWindow) + fCanvasWindow->present(); +} + +////////////////////////////////////////////////////////////////////////////////////////// +/// Returns kTRUE if web canvas has graphical editor + +Bool_t TGtk4Canvas::HasEditor() const +{ + return (fClientBits & TCanvas::kShowEditor) != 0; +} + +////////////////////////////////////////////////////////////////////////////////////////// +/// Returns kTRUE if web canvas has menu bar + +Bool_t TGtk4Canvas::HasMenuBar() const +{ + return (fClientBits & TCanvas::kMenuBar) != 0; +} + +////////////////////////////////////////////////////////////////////////////////////////// +/// Returns kTRUE if web canvas has status bar + +Bool_t TGtk4Canvas::HasStatusBar() const +{ + return (fClientBits & TCanvas::kShowEventStatus) != 0; +} + +////////////////////////////////////////////////////////////////////////////////////////// +/// Returns kTRUE if tooltips are activated in web canvas + +Bool_t TGtk4Canvas::HasToolTips() const +{ + return (fClientBits & TCanvas::kShowToolTips) != 0; +} + +////////////////////////////////////////////////////////////////////////////////////////// +/// Set window position of web canvas + +void TGtk4Canvas::SetWindowPosition(Int_t, Int_t) +{ + // not directly possible in gtk4 + //if (fCanvasWindow) + // fCanvasWindow->move(x, y); +} + +////////////////////////////////////////////////////////////////////////////////////////// +/// Set window size of web canvas + +void TGtk4Canvas::SetWindowSize(UInt_t w, UInt_t h) +{ + if (fCanvasWindow) + fCanvasWindow->set_default_size(w, h); +} + +////////////////////////////////////////////////////////////////////////////////////////// +/// Set window title of web canvas + +void TGtk4Canvas::SetWindowTitle(const char *newTitle) +{ + if (fCanvasWindow) + fCanvasWindow->set_title(newTitle); +} + +////////////////////////////////////////////////////////////////////////////////////////// +/// Set fixed canvas size - not supported yet in gtk + +void TGtk4Canvas::SetCanvasSize(UInt_t cw, UInt_t ch) +{ + fFixedSize = kTRUE; + if ((cw > 0) && (ch > 0)) { + // Canvas()->fCw = cw; + // Canvas()->fCh = ch; + } else { + // temporary value, will be reported back from client + // Canvas()->fCw = Canvas()->fWindowWidth; + // Canvas()->fCh = Canvas()->fWindowHeight; + } +} + +////////////////////////////////////////////////////////////////////////// +/// Set cursor + +void TGtk4Canvas::SetCursor(ECursor cursor) +{ + const char *id = nullptr; + switch(cursor) { + case kBottomLeft: id = "sw-resize"; break; + case kBottomRight: id = "se-resize"; break; + case kTopLeft: id = "nw-resize"; break; + case kTopRight: id = "ne-resize"; break; + case kBottomSide: id = "s-resize"; break; + case kLeftSide: id = "w-resize"; break; + case kTopSide: id = "n-resize"; break; + case kRightSide: id = "r-resize"; break; + case kMove: id = "move"; break; + case kCross: id = "crosshair"; break; + case kArrowHor: id = "ns-resize"; break; + case kArrowVer: id = "ew-resize"; break; + case kHand: id = "grab"; break; + case kRotate: id = "grab"; break; + case kPointer: id = "pointer"; break; + case kArrowRight: id = "ew-resize"; break; + case kCaret: id = "text"; break; + case kWatch: id = "wait"; break; + case kNoDrop: id = "no-drop"; break; + default: id = "default"; break; + } + if (id && fDrawArea) + fDrawArea->set_cursor(id); +} + + + +////////////////////////////////////////////////////////////////////////////////////////// +/// Iconify browser window + +void TGtk4Canvas::Iconify() +{ + if (fCanvasWindow) + fCanvasWindow->minimize(); +} + +////////////////////////////////////////////////////////////////////////////////////////// +/// Raise browser window + +void TGtk4Canvas::RaiseWindow() +{ + if (fCanvasWindow) + fCanvasWindow->present(); +} + +////////////////////////////////////////////////////////////////////////////////////////// +/// Assign clients bits + +void TGtk4Canvas::AssignStatusBits(UInt_t bits) +{ + fClientBits = bits; + Canvas()->SetBit(TCanvas::kShowEventStatus, bits & TCanvas::kShowEventStatus); + Canvas()->SetBit(TCanvas::kShowEditor, bits & TCanvas::kShowEditor); + Canvas()->SetBit(TCanvas::kShowToolTips, bits & TCanvas::kShowToolTips); + Canvas()->SetBit(TCanvas::kMenuBar, bits & TCanvas::kMenuBar); + Canvas()->SetBit(TCanvas::kShowToolBar, bits & TCanvas::kShowToolBar); +} + +////////////////////////////////////////////////////////////////////////////////////////// +/// Returns canvas geometry + +void TGtk4Canvas::GetCanvasGeometry(Int_t wid, UInt_t &w, UInt_t &h) +{ + (void) wid; + if (fDrawArea) { + w = fDrawArea->get_width(); + h = fDrawArea->get_height(); + } else { + w = 780; + h = 580; + } + // printf("CanvasGeometry %u %u\n", w, h); +} + + +////////////////////////////////////////////////////////////////////////////////////////// +/// Returns window geometry including borders and menus + +UInt_t TGtk4Canvas::GetWindowGeometry(Int_t &x, Int_t &y, UInt_t &w, UInt_t &h) +{ + if (fCanvasWindow) { + // gtk4 does not deliver position + x = y = 0; + w = fCanvasWindow->get_width(); + h = fCanvasWindow->get_height(); + } else { + x = y = 0; + w = 800; + h = 600; + } + + // printf("WindowGeometry %u %u\n", w, h); + + return 0; +} + +////////////////////////////////////////////////////////////////////////////////////////// +/// if canvas or any subpad was modified, +/// invoke gtk4 queue_draw() which will trigger redraw area + +Bool_t TGtk4Canvas::PerformUpdate(Bool_t async) +{ + if (!Canvas()->IsAnyNeedRepaint() || !fDrawArea) + return kTRUE; + + fDrawArea->queue_draw(); + + if (!async) { + auto display = fDrawArea->get_display(); + if (display) { + // Flushes all render requests and locks the local thread + // until the windowing manager acknowledges processing complete. + display->sync(); + } + } + + return kTRUE; +} + +////////////////////////////////////////////////////////////////////////////////////////// +/// Increment canvas version and force sending data to client - do not wait for reply + +void TGtk4Canvas::ForceUpdate() +{ + if (fDrawArea) + fDrawArea->queue_draw(); +} + + +Glib::RefPtr gt4k_context; +Glib::RefPtr gt4k_app; + +class TGtk4Timer : public TTimer { +public: + TGtk4Timer(Long_t milliSec, Bool_t mode) : + TTimer(milliSec, mode) {} + + void Timeout() override + { + // just flush all events, + // false - do not wait/sleep for next mouse events, just exit + while (gt4k_context->pending()) + gt4k_context->iteration(false); + } + +}; + + +////////////////////////////////////////////////////////////////////////////////////////////////// +/// Static method to create TGtk4Canvas instance +/// Used by plugin manager to directly create TGtk4Canvas without gui factory + +TCanvasImp *TGtk4Canvas::NewCanvas(TCanvas *c, const char *name, Int_t x, Int_t y, UInt_t width, UInt_t height) +{ + static TGtk4Timer *timer = nullptr; + if (!timer) { + + Glib::init(); + + gt4k_app = Gtk::Application::create("org.example.customloop"); + + gt4k_context = Glib::MainContext::get_default(); + + timer = new TGtk4Timer(5, kTRUE); + timer->TurnOn(); + } + + auto widget = new Gtk4CanvasWindow(c, width, height); + + auto draw_area = widget->GetDrawArea(); + + draw_area->queue_draw(); + + widget->present(); + + // workaround to let gtk4 time for first rendering of the widget + int cnt = 100; + while (cnt-- > 0 && (draw_area->get_width() == 0)) { + while (gt4k_context->pending()) + gt4k_context->iteration(false); + } + + + auto imp = new TGtk4Canvas(c, name, x, y, width, height); + + imp->fCanvasWindow = widget; + imp->fDrawArea = draw_area; + + // now can set canvas + draw_area->SetCanvas(c); + draw_area->queue_draw(); // request to update painting + + // set all internal dimensions + // c->Resize(); + + + // TODO: maybe apply same logic to adjust canvas dimension as in TRootCanvas + // Keep commented code here intentionally to be able find this place when + // search for correspondent class members + + // c->fWindowTopX = x; + // c->fWindowTopY = y; + // c->fWindowWidth = width; + // c->fWindowHeight = height; + // if (!gROOT->IsBatch() && (height > 25)) + // height -= 25; + // c->fCw = width; + // c->fCh = height; + + return imp; +} diff --git a/gui/gtk4canvas/src/TGtk4ContextMenu.cxx b/gui/gtk4canvas/src/TGtk4ContextMenu.cxx new file mode 100644 index 0000000000000..693f8351fca9d --- /dev/null +++ b/gui/gtk4canvas/src/TGtk4ContextMenu.cxx @@ -0,0 +1,388 @@ +// Author: Sergey Linev 13/08/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + + +/** \class TGtk4ContextMenu + \ingroup gtk4canvas + +This class provides an interface to context-sensitive popup menus. +These menus pop up when the user hits the right mouse button, and +are destroyed when the menu pops downs. +*/ + + +#include "TGtk4ContextMenu.h" + +#include "TROOT.h" +#include "TContextMenu.h" +#include "TCanvas.h" +#include "TColor.h" +#include "TMethod.h" +#include "TDataMember.h" +#include "TToggle.h" +#include "TClassMenuItem.h" +#include "TAttText.h" +#include "TAttMarker.h" + +#include "TGtk4Canvas.h" +#include "Gtk4DrawArea.h" +#include "Gtk4MethodDialog.h" + +#include + +enum EContextMenu { + kToggleStart = 1000, // first id of toggle menu items + kToggleListStart = 2000, // first id of toggle list menu items + kUserFunctionStart = 3000 // first id of user added functions/methods, etc... +}; + +//////////////////////////////////////////////////////////////////////////////// +/// Create context menu. + +TGtk4ContextMenu::TGtk4ContextMenu(TContextMenu *c, const char *) + : TObject(), TContextMenuImp(c) +{ + gROOT->GetListOfCleanups()->Add(this); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Delete a context menu. + +TGtk4ContextMenu::~TGtk4ContextMenu() +{ + gROOT->GetListOfCleanups()->Remove(this); + fTrash.Delete(); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Display context popup menu for currently selected object. + +void TGtk4ContextMenu::DisplayPopup(Int_t x, Int_t y) +{ + // add menu items to popup menu + // CreateMenu(fContextMenu->GetSelectedObject()); + + auto object = fContextMenu->GetSelectedObject(); + if (!object) + return; + + fCustomArg.clear(); + fTrash.Delete(); + + + auto canv = dynamic_cast(fContextMenu->GetSelectedCanvas()); + auto canvimp = dynamic_cast(canv->GetCanvasImp()); + auto widget = canvimp->GetDrawArea(); + +// QPoint screenPos = widget->mapToGlobal(widget->rect().topLeft()); + +// QMenu menu; +// QSignalMapper map; + +// QObject::connect(&map, &QSignalMapper::mappedInt, +// this, &TGtk4ContextMenu::executeMenu); + + auto action_group = Gio::SimpleActionGroup::create(); + + action_group->add_action_with_parameter( + "action", + Glib::VariantType("i"), // Expected parameter type (int32) + [this](const Glib::VariantBase& parameter) { + if (auto var = Glib::VariantBase::cast_dynamic>(parameter)) { + int value = var.get(); + std::cout << "Action triggered with integer value: " << value << std::endl; + executeMenu(value); + } + } + ); + + auto menu_model = Gio::Menu::create(); + + auto current_section = Gio::Menu::create(); + + + auto addSeparator = [&menu_model, ¤t_section]() { + menu_model->append_section(current_section); + current_section = Gio::Menu::create(); + }; + + auto addMenuAction = [¤t_section, &action_group, this](const TString &text, int id, void *arg = nullptr, int checked = 0) { + bool enabled = true; + if ((text == "DrawClone") || (text == "DrawClass") || (text == "Inspect") || + (text == "SetShowProjectionX") || (text == "SetShowProjectionY") || + (text == "DrawPanel") || (text == "FitPanel")) + enabled = false; + + + if (checked != 0) { + TString action_name = TString::Format("toggle-item-%d", id); + auto action = Gio::SimpleAction::create_bool(action_name.Data(), checked > 0); + action->signal_activate().connect([action,this, id](const Glib::VariantBase &) { + bool current; + action->get_state(current); + action->set_state(Glib::Variant::create(!current)); + std::cout << "Action triggered with integer value: " << id << std::endl; + executeMenu(id); + }); + action->set_enabled(enabled); + action_group->add_action(action); + + action_name.Prepend("context."); + auto item = Gio::MenuItem::create(text.Data(), action_name.Data()); + current_section->append_item(item); + } else if (enabled) { + auto item = Gio::MenuItem::create(text.Data(), ""); + item->set_action_and_target("context.action", Glib::Variant::create(id)); + current_section->append_item(item); + } else { + // just refer to non-existing action + current_section->append(text.Data(), "context.dummy"); + } + + fCustomArg[id] = arg; + }; + + TString title = fContextMenu->CreatePopupTitle(object); + + bool last_separ = true; + int entry = 0, toggle = kToggleStart, togglelist = kToggleListStart; + int userfunction = kUserFunctionStart; + + // Get list of menu items from the selected object's class + TList *menuItemList = object->IsA()->GetMenuList(); + + TIter nextItem(menuItemList); + + while (auto menuItem = (TClassMenuItem*) nextItem()) { + switch (menuItem->GetType()) { + case TClassMenuItem::kPopupSeparator: { + if (!last_separ) + addSeparator(); + last_separ = true; + break; + } + case TClassMenuItem::kPopupStandardList: { + // Standard list of class methods. Rebuild from scratch. + // Get linked list of objects menu items (i.e. member functions + // with the token *MENU in their comment fields. + TList *methodList = new TList; + object->IsA()->GetMenuItems(methodList); + + TMethod *method; + TClass *classPtr = nullptr; + TIter next(methodList); + Bool_t needSep = kFALSE; + + while ((method = (TMethod*) next())) { + if (classPtr != method->GetClass()) { + needSep = kTRUE; + classPtr = method->GetClass(); + } + + EMenuItemKind menuKind = method->IsMenuItem(); + TString last_component; + + switch (menuKind) { + case kMenuDialog: + // search for arguments to the MENU statement + if (needSep) { + addSeparator(); + needSep = kFALSE; + } + addMenuAction(method->GetName(), entry++, method); + break; + case kMenuSubMenu: + if (auto m = method->FindDataMember()) { + if (needSep) { + addSeparator(); + needSep = kFALSE; + } + + // TODO: implement selection menu + if (m->GetterMethod()) { + // QMenu *r = menu.addMenu(method->GetName()); + TIter nxt(m->GetOptions()); + while (auto it = (TOptionListItem*) nxt()) { + TToggle *t = new TToggle; + t->SetToggledObject(object, method); + t->SetOnValue(it->fValue); + fTrash.Add(t); + + // TODO: implement checked state + addMenuAction(it->fOptName.Data(), togglelist++, t, t->GetState() ? 1 : -1); + } + } else { + addMenuAction(method->GetName(), entry++, method); + } + } + break; + + case kMenuToggle: { + if (needSep) { + addSeparator(); + needSep = kFALSE; + } + + TToggle *t = new TToggle; + t->SetToggledObject(object, method); + t->SetOnValue(1); + fTrash.Add(t); + + addMenuAction(method->GetName(), toggle++, t, t->GetState() ? 1 : -1); + break; + } + default: + break; + } + } + delete methodList; + } + break; + case TClassMenuItem::kPopupUserFunction: { + const char* menuItemTitle = menuItem->GetTitle(); + if (menuItem->IsToggle()) { + TMethod* method = object->IsA()->GetMethodWithPrototype(menuItem->GetFunctionName(),menuItem->GetArgs()); + if (method) { + TToggle *t = new TToggle; + t->SetToggledObject(object, method); + t->SetOnValue(1); + fTrash.Add(t); + + if (strlen(menuItemTitle)==0) + menuItemTitle = method->GetName(); + addMenuAction(menuItemTitle, toggle++, t, t->GetState() ? 1 : -1); + } + } else { + if (strlen(menuItemTitle)==0) + menuItemTitle = menuItem->GetFunctionName(); + addMenuAction(menuItemTitle, userfunction++, menuItem); + } + break; + } + + default: + break; + } + } + + widget->insert_action_group("context", action_group); + + widget->ShowContextMenu(menu_model, title.Data(), x, y); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Create dialog object with OK and Cancel buttons. This dialog +/// prompts for the arguments of "method". + +void TGtk4ContextMenu::Dialog(TObject *object, TMethod *method) +{ + Dialog(object, (TFunction *)method); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Create dialog object with OK and Cancel buttons. This dialog +/// prompts for the arguments of "function". +/// function may be a global function or a method + +void TGtk4ContextMenu::Dialog(TObject * object, TFunction * func) +{ + auto widget = new Gtk4MethodDialog(300, 300); + + widget->methodDialog(fContextMenu, object, func); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Handle remove of some ROOT objects + +void TGtk4ContextMenu::RecursiveRemove(TObject *obj) +{ + if (obj == fContextMenu->GetSelectedCanvas()) + fContextMenu->SetCanvas(nullptr); + if (obj == fContextMenu->GetSelectedPad()) + fContextMenu->SetPad(nullptr); + if (obj == fContextMenu->GetSelectedObject()) { + // if the object being deleted is the one selected, + // ungrab the mouse pointer and terminate (close) the menu + fContextMenu->SetObject(nullptr); + } +} + + +//////////////////////////////////////////////////////////////////////////////// +/// Execute specified menu item + +void TGtk4ContextMenu::executeMenu(int id) +{ + if (id < 0) + return; + void *ud = fCustomArg[id]; + + if (ud) { + // retrieve the highlighted function + TFunction *function = nullptr; + if (id < kToggleStart) { + TMethod *m = (TMethod *)ud; + function = (TFunction *)m; + } else if (id >= kToggleStart && id < kUserFunctionStart) { + TToggle *t = (TToggle *)ud; + TMethodCall *mc = (TMethodCall *)t->GetSetter(); + function = (TFunction *)mc->GetMethod(); + } else { + TClassMenuItem *mi = (TClassMenuItem *)ud; + function = gROOT->GetGlobalFunctionWithPrototype(mi->GetFunctionName()); + } + if (function) + fContextMenu->SetMethod(function); + } + + if (id < kToggleStart) { + auto m = (TMethod *) ud; + + /* + if (!strcmp(m->GetName(), "SetLineAttributes")) { + auto attline = dynamic_cast(fContextMenu->GetSelectedObject()); + if (attline) { + auto widget = new Gtk4MethodDialog(300, 300); + widget->attLineDialog(attline); + } + } else if (!strcmp(m->GetName(), "SetFillAttributes")) { + auto attfill = dynamic_cast(fContextMenu->GetSelectedObject()); + if (attfill) { + auto widget = new Gtk4MethodDialog(300, 300); + widget->attFillDialog(attfill); + } + } else if (!strcmp(m->GetName(), "SetTextAttributes")) { + auto atttext = dynamic_cast(fContextMenu->GetSelectedObject()); + if (atttext) { + auto widget = new Gtk4MethodDialog(300, 300); + widget->attTextDialog(atttext); + } + } else if (!strcmp(m->GetName(), "SetMarkerAttributes")) { + auto attmark = dynamic_cast(fContextMenu->GetSelectedObject()); + if (attmark) { + auto widget = new Gtk4MethodDialog(300, 300); + widget->attMarkerDialog(attmark); + } + } else { + */ + fContextMenu->Action(m); + + } else if (id >= kToggleStart && id < kToggleListStart) { + TToggle *t = (TToggle *) ud; + fContextMenu->Action(t); + } else if (id >= kToggleListStart && id < kUserFunctionStart) { + TToggle *t = (TToggle *) ud; + if (t->GetState() == 0) + t->SetState(1); + } else { + TClassMenuItem *mi = (TClassMenuItem*)ud; + fContextMenu->Action(mi); + } +} diff --git a/gui/gtk4canvas/src/TGtk4ContextMenu.h b/gui/gtk4canvas/src/TGtk4ContextMenu.h new file mode 100644 index 0000000000000..d7d787717fe38 --- /dev/null +++ b/gui/gtk4canvas/src/TGtk4ContextMenu.h @@ -0,0 +1,52 @@ +// Author: Sergey Linev 13/08/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +#ifndef ROOT_TGtk4ContextMenu +#define ROOT_TGtk4ContextMenu + +#include "TContextMenuImp.h" +#include "TObject.h" +#include "TList.h" +#include + + +class TGtk4ContextMenu : public TObject, public TContextMenuImp { + +protected: + + TList fTrash; + std::map fCustomArg; + +/* + QAction* addMenuAction(QMenu *menu, QSignalMapper *map, const QString &text, int id, void *arg = nullptr); + + void AddColorElements(int colindx, QFormLayout *layout); + void UpdateColorElements(); + + void SetLineAttributesDialog(); + void SetFillAttributesDialog(); + void SetTextAttributesDialog(); + void SetMarkerAttributesDialog(); +*/ + + void executeMenu(int id); + +public: + TGtk4ContextMenu(TContextMenu *c = nullptr, const char *name = "ROOT Context Menu"); + ~TGtk4ContextMenu() override; + + void DisplayPopup(Int_t x, Int_t y) override; + void Dialog(TObject *object, TMethod *method) override; + void Dialog(TObject *object, TFunction *function) override; + + void RecursiveRemove(TObject *obj) override; +}; + +#endif diff --git a/gui/gtk4canvas/src/TGtk4GedEditor.cxx b/gui/gtk4canvas/src/TGtk4GedEditor.cxx new file mode 100644 index 0000000000000..54cc7dc3f1d9e --- /dev/null +++ b/gui/gtk4canvas/src/TGtk4GedEditor.cxx @@ -0,0 +1,281 @@ +// Author: Sergey Linev, GSI 30/10/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +#include "TGtk4GedEditor.h" + +#include "Gtk4MethodDialog.h" +#include "TGtk4Canvas.h" + +#include "TCanvas.h" +#include "TROOT.h" +#include "TColor.h" + +#include "TClass.h" +#include "TBaseClass.h" +#include "TMethod.h" +#include "TMethodCall.h" + +#include "TAttMarker.h" +#include "TAttLine.h" +#include "TAttFill.h" +#include "TAttText.h" + + + +#include + +using namespace ROOT::Experimental; + +/** \class TGtk4GedEditor + \ingroup gtk4canvas + \brief TVirtualPadEditor ABI implementation for GTK4 +*/ + +TGtk4GedEditor::TGtk4GedEditor(TCanvas *c) +{ + SetCanvas(c); +} + +TGtk4GedEditor::~TGtk4GedEditor() +{ +} + + +void TGtk4GedEditor::SetCanvas(TCanvas *newcan) +{ + if (fCanvas == newcan) return; + + DisconnectFromCanvas(); + fCanvas = newcan; + + if (!newcan) return; + + // SetWindowName(Form("%s_Editor", fCanvas->GetName())); + fPad = fCanvas->GetSelectedPad(); + if (!fPad) fPad = fCanvas; + ConnectToCanvas(fCanvas); +} + + +//////////////////////////////////////////////////////////////////////////////// +/// Connect this editor to the Selected signal of canvas 'c'. + +void TGtk4GedEditor::ConnectToCanvas(TCanvas *c) +{ + c->Connect("Selected(TVirtualPad*,TObject*,Int_t)", "ROOT::Experimental::TGtk4GedEditor", this, + "SetModel(TVirtualPad*,TObject*,Int_t)"); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Disconnect this editor from the Selected signal of fCanvas. + +void TGtk4GedEditor::DisconnectFromCanvas() +{ + if (fCanvas) + Disconnect(fCanvas, "Selected(TVirtualPad*,TObject*,Int_t)", this, "SetModel(TVirtualPad*,TObject*,Int_t)"); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Activate object editors according to the selected object. + +void TGtk4GedEditor::SetModel(TVirtualPad* pad, TObject* obj, Int_t event) +{ + if (event != kButton1Down) + return; + + auto prev = fModel; + + fPad = pad; + fModel = obj ? obj : pad; + + if (fModel != prev) + FillDialogsElements(); +} + +void TGtk4GedEditor::Show() +{ + if (gPad) + SetCanvas(gPad->GetCanvas()); + + if (fCanvas && fGlobal) + SetModel(fCanvas->GetClickSelectedPad(), fCanvas->GetClickSelected(), kButton1Down); + + if (!gROOT->GetListOfCleanups()->FindObject(this)) + gROOT->GetListOfCleanups()->Add(this); + + fDialog = new Gtk4MethodDialog(300, 300); + + /* reposition dialog + auto imp = dynamic_cast(fCanvas->GetCanvasImp()); + if (imp) { + auto widget = imp->GetCanvasWidget(); + fDialog->resize(200, widget->height()); + QPoint pos = widget->mapToGlobal(QPoint(0, 0)); + fDialog->move(pos.x() - fDialog->width(), pos.y()); + } + */ + + if (fModel) + FillDialogsElements(); +} + + +void TGtk4GedEditor::ModifiedPad() +{ + auto pad = fPad; + if (!pad) + pad = fCanvas; + if (pad) + pad->ModifiedUpdate(); +} + +void TGtk4GedEditor::AddTAttLine(TAttLine *attline) +{ + m_line_color = fDialog->addColorInput("Line col", attline->GetLineColor()); + + auto style_list = Gtk::StringList::create({"None", "Style 1", "Style 2", "Style 3", "Style 4", "Style 5", "Style 6", "Style 7", "Style 8", "Style 9"}); + m_line_style = Gtk::make_managed(style_list); + m_line_style->set_selected(attline->GetLineStyle()); + + fDialog->addLine("Line style", m_line_style); + + auto adjustment = Gtk::Adjustment::create(/* value */ attline->GetLineWidth(), /* lower */ 1, /* upper */ 20, + /* step_increment */ 1, /* page_increment */ 5.0); + + m_line_width = Gtk::make_managed(adjustment); + m_line_width->set_digits(0); // 0 decimal places -> integer display + + fDialog->addLine("Line width", m_line_width); + + fAttLine = attline; +} + +void TGtk4GedEditor::AddTAttFill(TAttFill *attfill) +{ + m_fill_color = fDialog->addColorInput("Fill col", attfill->GetFillColor()); + + fAttFill = attfill; +} + +void TGtk4GedEditor::AddTAttText(TAttText *atttext) +{ + m_text_color = fDialog->addColorInput("Text col", atttext->GetTextColor()); + + fAttText = atttext; +} + +void TGtk4GedEditor::AddTAttMarker(TAttMarker *attmark) +{ + m_marker_color = fDialog->addColorInput("Marker col", attmark->GetMarkerColor()); + + fAttMarker = attmark; +} + +void TGtk4GedEditor::FillGed(TClass *cl) +{ + TString method_name = TString::Format("Add%s", cl->GetName()); + + auto mtd = IsA()->GetMethodAny(method_name.Data()); + + if (mtd) { + void *obj = fModel->IsA()->DynamicCast(cl, fModel); + auto sarg = TString::Format("(%s*)0x%zx", cl->GetName(), (size_t)obj); + TMethodCall call(IsA(), method_name, sarg.Data()); + call.Execute(this); + } + + auto lst = cl->GetListOfBases(); + + TIter iter(lst); + while (auto base = (TBaseClass*) iter()) + FillGed(base->GetClassPointer()); +} + +void TGtk4GedEditor::FillDialogsElements() +{ + if (!fModel || !fDialog) + return; + + // first delete all previous elements !!!! + fDialog->clearAllElements(); + + fAttLine = nullptr; + fAttFill = nullptr; + fAttText = nullptr; + fAttMarker = nullptr; + m_line_color = m_fill_color = m_text_color = m_marker_color = nullptr; + m_line_style = nullptr; + m_line_width = nullptr; + + FillGed(fModel->IsA()); + + auto okBtn = fDialog->addOkCancelButtons(); + + okBtn->signal_clicked().connect(sigc::mem_fun(*this, &TGtk4GedEditor::on_ok_button_clicked)); + + fDialog->present(); +} + +Int_t TGtk4GedEditor::GetRootColor(Gtk::ColorDialogButton* btn) +{ + auto selected_color = btn->get_rgba(); + return TColor::GetColor(selected_color.get_red(), + selected_color.get_green(), + selected_color.get_blue(), + selected_color.get_alpha()); +} + + +void TGtk4GedEditor::on_ok_button_clicked() +{ + if (fAttLine) { + fAttLine->SetLineColor(GetRootColor(m_line_color)); + fAttLine->SetLineStyle(m_line_style->get_selected()); + fAttLine->SetLineWidth(m_line_width->get_value()); + } + + if (fAttFill) { + fAttFill->SetFillColor(GetRootColor(m_fill_color)); + } + + if (fAttText) { + fAttText->SetTextColor(GetRootColor(m_text_color)); + } + if (fAttMarker) { + fAttMarker->SetMarkerColor(GetRootColor(m_marker_color)); + } + + fDialog->close(); + fDialog = nullptr; + + if (gPad) + gPad->ModifiedUpdate(); + +} + + +void TGtk4GedEditor::Hide() +{ + gROOT->GetListOfCleanups()->Remove(this); + if (fDialog) { + fDialog->close(); + fDialog = nullptr; + } +} + +void TGtk4GedEditor::RecursiveRemove(TObject* obj) +{ + if (obj == fModel) { + SetModel(fPad, fPad, kButton1Down); + } else if (obj == fPad) { + SetModel(fCanvas, fCanvas, kButton1Down); + } else if (obj == fCanvas) + Hide(); +} diff --git a/gui/gtk4canvas/src/TGtk4GuiFactory.cxx b/gui/gtk4canvas/src/TGtk4GuiFactory.cxx new file mode 100644 index 0000000000000..5762eaa8ed255 --- /dev/null +++ b/gui/gtk4canvas/src/TGtk4GuiFactory.cxx @@ -0,0 +1,69 @@ +// Author: Sergey Linev 12/08/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + + +/** \class TGtk4GuiFactory + \ingroup gtk4canvas + +This class is a factory for ROOT GUI components. +For Gtk4 it provides specialization for TCanvasImp and TContextMenuImp classes +*/ + + +#include "TGtk4GuiFactory.h" + +#include "TGtk4Canvas.h" +#include "TGtk4ContextMenu.h" + +using namespace ROOT::Experimental; + +//////////////////////////////////////////////////////////////////////////////// +/// TGtk4GuiFactory ctor. + +TGtk4GuiFactory::TGtk4GuiFactory(const char *name, const char *title) + : TGuiFactory(name, title) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +/// Create a ROOT native GUI version of TApplicationImp + +TApplicationImp *TGtk4GuiFactory::CreateApplicationImp(const char *classname, + Int_t *argc, char **argv) +{ + return TGuiFactory::CreateApplicationImp(classname, argc, argv); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Create a ROOT native GUI version of TCanvasImp + +TCanvasImp *TGtk4GuiFactory::CreateCanvasImp(TCanvas *c, const char *title, + UInt_t width, UInt_t height) +{ + return TGtk4Canvas::NewCanvas(c, title, -1, -1, width, height); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Create a ROOT native GUI version of TCanvasImp + +TCanvasImp *TGtk4GuiFactory::CreateCanvasImp(TCanvas *c, const char *title, + Int_t x, Int_t y, UInt_t width, UInt_t height) +{ + return TGtk4Canvas::NewCanvas(c, title, x, y, width, height); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Create a ROOT native GUI version of TContextMenuImp + +TContextMenuImp *TGtk4GuiFactory::CreateContextMenuImp(TContextMenu *c, + const char *name, const char *) +{ + return new TGtk4ContextMenu(c, name); +} diff --git a/gui/gtk4canvas/src/TGtk4PadPainter.cxx b/gui/gtk4canvas/src/TGtk4PadPainter.cxx new file mode 100644 index 0000000000000..3883fc195982b --- /dev/null +++ b/gui/gtk4canvas/src/TGtk4PadPainter.cxx @@ -0,0 +1,609 @@ +// Author: Sergey Linev, GSI 12/08/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +#include "TGtk4PadPainter.h" +#include "TError.h" +#include "TSystem.h" +#include "TStyle.h" +#include "TEnv.h" +#include "TMath.h" +#include "TCanvas.h" +#include "TPoint.h" +#include "TROOT.h" +#include "TColor.h" +#include "RStipples.h" +#include "TTFhandle.h" +#include "TImage.h" + +#include +#include + +#include "Gtk4DrawArea.h" + +#include +#include +#include + + + +using namespace ROOT::Experimental; + +// to scale fonts to the same size as in the TTF +const Float_t kScale = 0.93376068; + +/** \class TGtk4PadPainter + \ingroup gtk4canvas + \brief Implement TVirtualPadPainter for Gtk4 graphics + + Uses Gtk4 DrawArea which creates Cairo::Context during update. +*/ + +////////////////////////////////////////////////////////////////////////// +/// Set opacity - similar to TVirtualPS usecase + +void TGtk4PadPainter::SetOpacity(Int_t percent) +{ + fAttFill.SetFillStyle(4000 + percent); +} + +//////////////////////////////////////////////////////////////////////////////// +///Noop, for non-gl pad TASImage calls gVirtualX->CopyArea. + +void TGtk4PadPainter::DrawPixels(const unsigned char * /*pixelData*/, UInt_t /*width*/, UInt_t /*height*/, + Int_t /*dstX*/, Int_t /*dstY*/, Bool_t /*enableAlphaBlending*/) +{ + +} + + +//////////////////////////////////////////////////////////////////////////////// +/// Assign color to cairo context + +void TGtk4PadPainter::SetGtk4Color(Color_t id) +{ + auto ctx = fDrawArea->GetContext(); + if (!ctx) + return; + + auto c = gROOT->GetColor(id); + if (c) + ctx->set_source_rgba(c->GetRed(), c->GetGreen(), c->GetBlue(), c->GetAlpha()); + else + ctx->set_source_rgb(0, 0, 0); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Asign pen attributes to draw line + +Bool_t TGtk4PadPainter::SetLinePen() +{ + auto ctx = fDrawArea->GetContext(); + if (!ctx) + return kFALSE; + + auto &att = GetAttLine(); + if (att.GetLineWidth() <= 0) + return kFALSE; + + SetGtk4Color(att.GetLineColor()); + ctx->set_line_width(att.GetLineWidth()); + + auto style = att.GetLineStyle(); + + TString patt; + std::vector pattern; + + if (style > 1) + patt = gStyle->GetLineStyleString(style); + + if (patt.Length() > 2) { + std::unique_ptr tokens(patt.Tokenize(" ")); + for (Int_t j = 0; j < tokens->GetEntries(); j++) { + int it = std::stoi(tokens->At(j)->GetName()); + pattern.emplace_back(0.1 * it * att.GetLineWidth()); + } + } + + if (pattern.size() > 1) + ctx->set_dash(pattern, 0); + else + ctx->unset_dash(); + + return kTRUE; +} + + +//////////////////////////////////////////////////////////////////////////////// +/// Asign fill attributes before drawin shape + +Bool_t TGtk4PadPainter::SetFillBrush() +{ + auto ctx = fDrawArea->GetContext(); + if (!ctx) + return kFALSE; + + auto &att = GetAttFill(); + fCustomPattern = 0; + + Int_t style = att.GetFillStyle() / 1000; + + if (style == 1) { + SetGtk4Color(att.GetFillColor()); + return kTRUE; + } + + if (style != 3) + return kFALSE; + + Int_t fasi = att.GetFillStyle() % 1000; + fCustomPattern = (fasi >= 1 && fasi <=25) ? fasi : 2; + + ctx->save(); + SetGtk4Color(att.GetFillColor()); + return kTRUE; +} + +//////////////////////////////////////////////////////////////////////////////// +/// Complete fill operation - which may include custom pattern handling + +void TGtk4PadPainter::ApplyFillBrush() +{ + auto ctx = fDrawArea->GetContext(); + + if (fCustomPattern > 0) { + + unsigned char *bitmask_bytes = (unsigned char *) gStipples[fCustomPattern]; + + const int width = 16, height = 16; + auto format = Cairo::Surface::Format::A1; + auto stride = Cairo::ImageSurface::format_stride_for_width(format, width); + + std::vector padded_bytes(stride * height, 0); + for (int row = 0; row < height; ++row) { + padded_bytes[row * stride] = bitmask_bytes[row * 2]; + padded_bytes[row * stride + 1] = bitmask_bytes[row * 2 + 1]; + } + + auto mask_surface = Cairo::ImageSurface::create(padded_bytes.data(), format, width, height, stride); + + auto pattern = Cairo::SurfacePattern::create(mask_surface); + pattern->set_extend(Cairo::Pattern::Extend::REPEAT); + + ctx->clip(); + + ctx->mask(pattern); + + ctx->restore(); + + fCustomPattern = 0; + } else { + + ctx->fill(); + } + +} + + + +//////////////////////////////////////////////////////////////////////////////// +/// Paint a simple line. + +void TGtk4PadPainter::DrawLine(Double_t x1, Double_t y1, Double_t x2, Double_t y2) +{ + if (!SetLinePen()) + return; + + const Int_t px1 = gPad->XtoAbsPixel(x1); + const Int_t py1 = gPad->YtoAbsPixel(y1); + const Int_t px2 = gPad->XtoAbsPixel(x2); + const Int_t py2 = gPad->YtoAbsPixel(y2); + + auto ctx = fDrawArea->GetContext(); + ctx->move_to(px1, py1); + ctx->line_to(px2, py2); + ctx->stroke(); +} + + +//////////////////////////////////////////////////////////////////////////////// +/// Paint a simple line in normalized coordinates. + +void TGtk4PadPainter::DrawLineNDC(Double_t u1, Double_t v1, Double_t u2, Double_t v2) +{ + if (!SetLinePen()) + return; + + const Int_t px1 = gPad->UtoAbsPixel(u1); + const Int_t py1 = gPad->VtoAbsPixel(v1); + const Int_t px2 = gPad->UtoAbsPixel(u2); + const Int_t py2 = gPad->VtoAbsPixel(v2); + + auto ctx = fDrawArea->GetContext(); + ctx->move_to(px1, py1); + ctx->line_to(px2, py2); + ctx->stroke(); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Paint a simple box. + +void TGtk4PadPainter::DrawBox(Double_t x1, Double_t y1, Double_t x2, Double_t y2, EBoxMode mode) +{ + const Int_t px1 = gPad->XtoAbsPixel(x1); + const Int_t py1 = gPad->YtoAbsPixel(y1); + const Int_t px2 = gPad->XtoAbsPixel(x2); + const Int_t py2 = gPad->YtoAbsPixel(y2); + + auto ctx = fDrawArea->GetContext(); + if (!ctx) + return; + + if (mode == TVirtualPadPainter::kHollow) { + if (SetLinePen()) { + ctx->rectangle(TMath::Min(px1, px2), TMath::Min(py1, py2), TMath::Abs(px2 - px1), TMath::Abs(py2 - py1)); + ctx->stroke(); + } + } else { + if (SetFillBrush()) { + ctx->rectangle(TMath::Min(px1, px2), TMath::Min(py1, py2), TMath::Abs(px2 - px1), TMath::Abs(py2 - py1)); + ApplyFillBrush(); + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +/// Paint filled area. + +void TGtk4PadPainter::DrawFillArea(Int_t nPoints, const Double_t *xs, const Double_t *ys) +{ + if ((nPoints < 3) || !SetFillBrush()) + return; + + auto ctx = fDrawArea->GetContext(); + + for (Int_t n = 0; n < nPoints; ++n) { + auto px = gPad->XtoAbsPixel(xs[n]); + auto py = gPad->YtoAbsPixel(ys[n]); + if (n == 0) + ctx->move_to(px, py); + else + ctx->line_to(px, py); + } + + ctx->close_path(); + + ApplyFillBrush(); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Paint filled area. + +void TGtk4PadPainter::DrawFillArea(Int_t nPoints, const Float_t *xs, const Float_t *ys) +{ + if ((nPoints < 3) || !SetFillBrush()) + return; + + auto ctx = fDrawArea->GetContext(); + + for (Int_t n = 0; n < nPoints; ++n) { + auto px = gPad->XtoAbsPixel(xs[n]); + auto py = gPad->YtoAbsPixel(ys[n]); + if (n == 0) + ctx->move_to(px, py); + else + ctx->line_to(px, py); + } + + ctx->close_path(); + + ApplyFillBrush(); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Paint Polyline. + +void TGtk4PadPainter::DrawPolyLine(Int_t nPoints, const Double_t *xs, const Double_t *ys) +{ + if ((nPoints < 2) || !SetLinePen()) + return; + + auto ctx = fDrawArea->GetContext(); + + for (Int_t n = 0; n < nPoints; ++n) { + auto px = gPad->XtoAbsPixel(xs[n]); + auto py = gPad->YtoAbsPixel(ys[n]); + if (n == 0) + ctx->move_to(px, py); + else + ctx->line_to(px, py); + } + + ctx->stroke(); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Paint polyline. + +void TGtk4PadPainter::DrawPolyLine(Int_t nPoints, const Float_t *xs, const Float_t *ys) +{ + if ((nPoints < 2) || !SetLinePen()) + return; + + auto ctx = fDrawArea->GetContext(); + + for (Int_t n = 0; n < nPoints; ++n) { + auto px = gPad->XtoAbsPixel(xs[n]); + auto py = gPad->YtoAbsPixel(ys[n]); + if (n == 0) + ctx->move_to(px, py); + else + ctx->line_to(px, py); + } + + ctx->stroke(); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Paint polyline in normalized coordinates. + +void TGtk4PadPainter::DrawPolyLineNDC(Int_t nPoints, const Double_t *u, const Double_t *v) +{ + if ((nPoints < 2) || !SetLinePen()) + return; + + auto ctx = fDrawArea->GetContext(); + + for (Int_t n = 0; n < nPoints; ++n) { + auto px = gPad->UtoAbsPixel(u[n]); + auto py = gPad->VtoAbsPixel(v[n]); + if (n == 0) + ctx->move_to(px, py); + else + ctx->line_to(px, py); + } + + ctx->stroke(); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Helper function to paint markers + +template +void drawMarkersHelper(Int_t nPoints, const T *x, const T *y, + const TAttMarker &attr, Cairo::Context *ctx) +{ + if (!ctx || nPoints < 1) + return; + + auto markerLineWidth = TAttMarker::GetMarkerLineWidth(attr.GetMarkerStyle()); + Int_t markerSize = 0; ///< size of simple markers + std::vector markerShape; ///< marker shape points + auto markerType = attr.GetMarkerShape(markerSize, markerShape); + + if ((markerType == TAttMarker::kShapeDot) || (markerLineWidth > 0)) { + ctx->set_line_width(markerLineWidth); + } else { + } + + for (Int_t n = 0; n < nPoints; ++n) { + Int_t px = gPad->XtoAbsPixel(x[n]); + Int_t py = gPad->YtoAbsPixel(y[n]); + Int_t cnt = 0; + + switch(markerType) { + case TAttMarker::kShapeDot: + ctx->rectangle(px + 0.5, py + 0.5, 1., 1.); + ctx->fill(); + break; + case TAttMarker::kShapeCircle: + ctx->arc(px, py, markerSize / 2, 0.0, 2.0 * TMath::Pi()); + ctx->stroke(); + break; + case TAttMarker::kShapeFilledCircle: + // hollow or filled circle + ctx->arc(px, py, markerSize / 2, 0.0, 2.0 * TMath::Pi()); + ctx->fill(); + break; + case TAttMarker::kShapePolyLine: + // hollow polygon + for (auto &pnt : markerShape) + if (cnt++) + ctx->line_to(px + pnt.fX, py + pnt.fY); + else + ctx->move_to(px + pnt.fX, py + pnt.fY); + ctx->stroke(); + break; + case TAttMarker::kShapeFilledArea: + // filled polygon + for (auto &pnt : markerShape) + if (cnt++) + ctx->line_to(px + pnt.fX, py + pnt.fY); + else + ctx->move_to(px + pnt.fX, py + pnt.fY); + ctx->close_path(); + ctx->fill(); + break; + case TAttMarker::kShapeSegments: + // segmented line + for (size_t i = 0; i < markerShape.size() - 1; i += 2) { + ctx->move_to(px + markerShape[i].fX, py + markerShape[i].fY); + ctx->line_to(px + markerShape[i+1].fX, py + markerShape[i+1].fY); + if (i == markerShape.size() - 2) + ctx->stroke(); + else + ctx->stroke_preserve(); + } + break; + case TAttMarker::kShapeTriangles: + // filled triangles + for (size_t i = 0; i < markerShape.size() - 2; i += 3) { + ctx->move_to(px + markerShape[i].fX, py + markerShape[i].fY); + ctx->line_to(px + markerShape[i+1].fX, py + markerShape[i+1].fY); + ctx->line_to(px + markerShape[i+2].fX, py + markerShape[i+2].fY); + ctx->close_path(); + if (i == markerShape.size() - 3) + ctx->fill(); + else + ctx->fill_preserve(); + } + break; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +/// Paint polymarker. + +void TGtk4PadPainter::DrawPolyMarker(Int_t nPoints, const Double_t *x, const Double_t *y) +{ + SetGtk4Color(GetAttMarker().GetMarkerColor()); + + drawMarkersHelper(nPoints, x, y, + GetAttMarker(), fDrawArea->GetContext()); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Paint polymarker. + +void TGtk4PadPainter::DrawPolyMarker(Int_t nPoints, const Float_t *x, const Float_t *y) +{ + SetGtk4Color(GetAttMarker().GetMarkerColor()); + + drawMarkersHelper(nPoints, x, y, + GetAttMarker(), fDrawArea->GetContext()); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Produce image + +void TGtk4PadPainter::SaveImage(TVirtualPad * /* pad */, const char * /* fileName */, Int_t /* gtype */) const +{ +} + +//////////////////////////////////////////////////////////////////////////////// +/// Render TTF glyphs on drawable area + +void TGtk4PadPainter::DrawTTFglyphs(Int_t px, Int_t py, TTFhandle &ttf, ETextMode /* mode */) +{ + // position inside the pad is provided, therefore shift it to global image + px += fPad->UtoAbsPixel(0); + py += fPad->VtoAbsPixel(1); + + auto ctx = fDrawArea->GetContext(); + + SetGtk4Color(GetAttText().GetTextColor()); + + for (UInt_t nglyph = 0; nglyph < ttf.GetNumGlyphs(); nglyph++) { + Int_t bx = 0, by = 0; + UChar_t *buffer = nullptr; + UInt_t width = 0, rows = 0, pitch = 0; + if (!ttf.GetGlyphData(nglyph, bx, by, buffer, width, rows, pitch)) + continue; + + // Cairo's A8 surfaces need their own row stride (padding), which + // usually differs from FreeType's bmp.pitch - copy row by row + auto stride = Cairo::ImageSurface::format_stride_for_width(Cairo::Surface::Format::A8, width); + std::vector data(stride * rows, 0); + + for (unsigned int row = 0; row < rows; ++row) + std::memcpy(data.data() + row * stride, buffer + row * pitch, width); + + auto surface = + Cairo::ImageSurface::create(data.data(), Cairo::Surface::Format::A8, width, rows, stride); + + ctx->mask(surface, px + bx, py + by); + } +} + + +//////////////////////////////////////////////////////////////////////////////// +/// Render image on drawable area + +void TGtk4PadPainter::DrawImage(TImage *img, Int_t px, Int_t py, Int_t) +{ + // position inside the pad is provided, therefore shift it to global image + px += fPad->UtoAbsPixel(0); + py += fPad->VtoAbsPixel(1); + + auto width = img->GetWidth(); + auto height = img->GetHeight(); + auto argbBuffer = (unsigned char*) img->GetArgbArray(); + + auto ctx = fDrawArea->GetContext(); + + int stride = Cairo::ImageSurface::format_stride_for_width(Cairo::Surface::Format::ARGB32, width); + + Cairo::RefPtr surface = + Cairo::ImageSurface::create(argbBuffer, Cairo::Surface::Format::ARGB32, width, height, stride); + + ctx->set_source(surface, px, py); + + ctx->paint(); +} + +//////////////////////////////////////////////////////////////////////////////// +/// extract image from recent drawing + +Bool_t TGtk4PadPainter::FillImageFromPad(TImage *image, TVirtualPad *pad) const +{ + auto canv = pad->GetCanvas(); + + if (canv != fDrawArea->GetCanvas()) { + ::Error("TGtk4PadPainter::FillImageFromPad", "Provided pad does not belong to the canvas"); + return kFALSE; + } + + auto cache = fDrawArea->GetCache(); + if (!cache) { + ::Error("TGtk4PadPainter::FillImageFromPad", "Canvas was not rendered yet"); + return kFALSE; + } + + if (cache->get_format() != Cairo::Surface::Format::ARGB32) { + ::Error("TGtk4PadPainter::FillImageFromPad", "Wrong internal format of cached image"); + return kFALSE; + } + + int width = cache->get_width(); + int height = cache->get_height(); + int stride = cache->get_stride(); + const uint8_t* raw_data = cache->get_data(); + + int x = 0, y = 0, w = width, h = height; + + if (pad != canv) { + x = pad->UtoAbsPixel(0); + y = pad->VtoAbsPixel(1); + w = pad->GetPadWidth(); + h = pad->GetPadHeight(); + } else if (stride == width * 4) { + // special case when raw data can be used as is + image->FromBitmap((const UChar_t *) raw_data, width, height); + return kTRUE; + } + + std::vector pixels(w * h * 4); + + for (int i = 0; i < h; ++i) { + // Find the start of the current row in the Cairo surface + const uint8_t* row_src = raw_data + (i + y) * stride + x * 4; + + // Find where to put it in our tightly packed output vector + auto row_dest = pixels.data() + i * w * 4; + + std::copy(row_src, row_src + w * 4, row_dest); + } + + image->FromBitmap(pixels.data(), w, h); + + return kTRUE; + + +}