From 8ed7dfc869b536d52a23345c18de641011efedcb Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Fri, 30 Oct 2026 14:50:41 +0100 Subject: [PATCH 01/13] [gtk4canv] introduce GTK4-based canvas implementation It uses GTK4 classes for implement pad painting --- .../root-ci-config/buildconfig/global.txt | 1 + cmake/modules/RootBuildOptions.cmake | 2 + etc/plugins/TCanvasImp/P030_TGtk4Canvas.C | 5 + .../TGuiFactory/P030_TGtk4GuiFactory.C | 5 + gui/CMakeLists.txt | 4 + gui/gtk4canvas/CMakeLists.txt | 44 ++ gui/gtk4canvas/inc/LinkDef.h | 21 + gui/gtk4canvas/inc/TGtk4Canvas.h | 84 +++ gui/gtk4canvas/inc/TGtk4GuiFactory.h | 41 ++ gui/gtk4canvas/inc/TGtk4PadPainter.h | 102 ++++ gui/gtk4canvas/index.md | 22 + gui/gtk4canvas/src/Gtk4CanvasWindow.cxx | 160 +++++ gui/gtk4canvas/src/Gtk4CanvasWindow.h | 60 ++ gui/gtk4canvas/src/Gtk4DrawArea.cxx | 212 +++++++ gui/gtk4canvas/src/Gtk4DrawArea.h | 51 ++ gui/gtk4canvas/src/Gtk4MethodDialog.cxx | 334 +++++++++++ gui/gtk4canvas/src/Gtk4MethodDialog.h | 68 +++ gui/gtk4canvas/src/TGtk4Canvas.cxx | 391 ++++++++++++ gui/gtk4canvas/src/TGtk4ContextMenu.cxx | 389 ++++++++++++ gui/gtk4canvas/src/TGtk4ContextMenu.h | 52 ++ gui/gtk4canvas/src/TGtk4GuiFactory.cxx | 69 +++ gui/gtk4canvas/src/TGtk4PadPainter.cxx | 554 ++++++++++++++++++ 22 files changed, 2671 insertions(+) create mode 100644 etc/plugins/TCanvasImp/P030_TGtk4Canvas.C create mode 100644 etc/plugins/TGuiFactory/P030_TGtk4GuiFactory.C create mode 100644 gui/gtk4canvas/CMakeLists.txt create mode 100644 gui/gtk4canvas/inc/LinkDef.h create mode 100644 gui/gtk4canvas/inc/TGtk4Canvas.h create mode 100644 gui/gtk4canvas/inc/TGtk4GuiFactory.h create mode 100644 gui/gtk4canvas/inc/TGtk4PadPainter.h create mode 100644 gui/gtk4canvas/index.md create mode 100644 gui/gtk4canvas/src/Gtk4CanvasWindow.cxx create mode 100644 gui/gtk4canvas/src/Gtk4CanvasWindow.h create mode 100644 gui/gtk4canvas/src/Gtk4DrawArea.cxx create mode 100644 gui/gtk4canvas/src/Gtk4DrawArea.h create mode 100644 gui/gtk4canvas/src/Gtk4MethodDialog.cxx create mode 100644 gui/gtk4canvas/src/Gtk4MethodDialog.h create mode 100644 gui/gtk4canvas/src/TGtk4Canvas.cxx create mode 100644 gui/gtk4canvas/src/TGtk4ContextMenu.cxx create mode 100644 gui/gtk4canvas/src/TGtk4ContextMenu.h create mode 100644 gui/gtk4canvas/src/TGtk4GuiFactory.cxx create mode 100644 gui/gtk4canvas/src/TGtk4PadPainter.cxx 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/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..e340fd04d3642 --- /dev/null +++ b/gui/gtk4canvas/CMakeLists.txt @@ -0,0 +1,44 @@ +# 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 + SOURCES + Gtk4DrawArea.cxx + Gtk4CanvasWindow.cxx + Gtk4MethodDialog.cxx + TGtk4ContextMenu.cxx + TGtk4Canvas.cxx + TGtk4GuiFactory.cxx + TGtk4PadPainter.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..1ddbde0866e73 --- /dev/null +++ b/gui/gtk4canvas/inc/LinkDef.h @@ -0,0 +1,21 @@ +// 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+; + +#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; ///. + */ + +#include "Gtk4CanvasWindow.h" + +#include +#include + +#include "TROOT.h" +#include "TTimer.h" +#include "TApplication.h" + + +/** \class Gtk4CanvasWindow + \ingroup gtk4canvas +*/ + + +Gtk4CanvasWindow::Gtk4CanvasWindow(unsigned width, unsigned height) +: m_Box(Gtk::Orientation::VERTICAL) +{ + 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. + + m_action_group = Gio::SimpleActionGroup::create(); + + m_action_group->add_action("new", []() { std::cout << "New canvas Clicked!\n"; }); + m_action_group->add_action("open", []() { std::cout << "Open File Clicked!\n"; }); + + m_action_group->add_action("close", [this]() { close(); }); + m_action_group->add_action("quit", []() { + + printf("Quit ROOT action\n"); + gROOT->SetInterrupt(kTRUE); + + if (gApplication) + TTimer::SingleShot(100, "TApplication", gApplication, "Terminate()"); + }); + + // Bind actions to an identifier prefix name string ("win.new", "win.copy", etc.) + insert_action_group("win", m_action_group); + + // 3. Construct the Abstract Menu Tree Model + auto menu_model = Gio::Menu::create(); + + // --- SECTION A: The "File" Top Level Dropdown Menu --- + + auto sec1 = Gio::Menu::create(); + sec1->append("New Canvas", "win.new"); + sec1->append("Open", "win.open"); + sec1->append("Close Canvas", "win.close"); + + auto sec2 = Gio::Menu::create(); + sec2->append("Save", "win.save"); + sec2->append("Save as", "win.saveas"); + + auto sec3 = Gio::Menu::create(); + sec3->append("Quit ROOT", "win.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 B: The "Edit" Top Level Dropdown Menu --- + auto edit_menu = Gio::Menu::create(); + edit_menu->append("Clear pad", "win.clearpad"); + edit_menu->append("Clear canvas", "win.clearcanvas"); + + // CREATE A SUB-MENU ITEM (Nested Layer) + auto special_paste_submenu = Gio::Menu::create(); + special_paste_submenu->append("Paste Text Only", "win.paste"); + special_paste_submenu->append("Paste with Formatting", "win.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); + + // 4. Instantiate the PopoverMenuBar from our structured menu tree + // Gtk::PopoverMenuBar requires a Glib::RefPtr + auto menu_bar = Gtk::make_managed(menu_model); + + // 5. Append the layout widget to the window canvas + m_Box.append(*menu_bar); + + + m_DrawArea.set_hexpand(true); + m_DrawArea.set_vexpand(true); + m_Box.append(m_DrawArea); + + + unsigned int context_id = m_StatusBar.get_context_id("main_status"); + m_StatusBar.push("Ready to draw", context_id); + m_Box.append(m_StatusBar); +} + +Gtk4CanvasWindow::~Gtk4CanvasWindow() +{ +} + +/* + +void Gtk4CanvasWindow::on_action_file_quit() +{ + close(); +} + +void Gtk4CanvasWindow::on_action_toggle() +{ + std::cout << "The toggle menu item was selected." << std::endl; + + bool active = false; + m_refActionRain->get_state(active); + + //The toggle action's state does not change automatically: + active = !active; + m_refActionRain->change_state(active); + + Glib::ustring message; + if(active) + message = "Toggle is active."; + else + message = "Toggle is not active"; + + std::cout << message << std::endl; + +} + +*/ \ No newline at end of file diff --git a/gui/gtk4canvas/src/Gtk4CanvasWindow.h b/gui/gtk4canvas/src/Gtk4CanvasWindow.h new file mode 100644 index 0000000000000..469e58855e5e2 --- /dev/null +++ b/gui/gtk4canvas/src/Gtk4CanvasWindow.h @@ -0,0 +1,60 @@ +// 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" + +class Gtk4CanvasWindow : public Gtk::Window +{ +public: + Gtk4CanvasWindow(unsigned width = 400, unsigned height = 200); + virtual ~Gtk4CanvasWindow(); + + Gtk4DrawArea *GetDrawArea() { return &m_DrawArea; } + +private: + //Signal handlers: + //void on_action_file_new(); + //void on_action_file_quit(); + //void on_action_others(); + //void on_action_toggle(); + + + //Child widgets: + Gtk::Box m_Box; + + Glib::RefPtr m_action_group; + + Gtk4DrawArea m_DrawArea; + Gtk::Statusbar m_StatusBar; + +}; + +#endif //GTKMM_Gtk4CanvasWindow_H diff --git a/gui/gtk4canvas/src/Gtk4DrawArea.cxx b/gui/gtk4canvas/src/Gtk4DrawArea.cxx new file mode 100644 index 0000000000000..90ea81d96cf4b --- /dev/null +++ b/gui/gtk4canvas/src/Gtk4DrawArea.cxx @@ -0,0 +1,212 @@ +// 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); +} + +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 != fCanvas->GetPadWidth()) || (height != fCanvas->GetPadHeight())) { + fCanvas->Resize(); + } + + + // clear + cr->set_source_rgb(1., 1., 1.); + cr->paint(); + + fContext = cr.get(); + + // TODO: call canvas->paint instead + fCanvas->Paint(); + + fContext = nullptr; +} + +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) +{ + 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); +} + +bool Gtk4DrawArea::on_mouse_scroll(double dx, double dy) +{ + if (fCanvas) + fCanvas->HandleInput(dy < 0 ? kWheelUp : kWheelDown, (int) fLastMouseX, (int) fLastMouseY); + + return true; +} + + +void Gtk4DrawArea::ShowContextMenu(Glib::RefPtr menu_model, int x, int y) +{ + auto menu = Gtk::make_managed(menu_model); + menu->set_parent(*this); + menu->set_has_arrow(false); // Removes the little popover triangle point + + 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(); + +} diff --git a/gui/gtk4canvas/src/Gtk4DrawArea.h b/gui/gtk4canvas/src/Gtk4DrawArea.h new file mode 100644 index 0000000000000..0e99c20928a65 --- /dev/null +++ b/gui/gtk4canvas/src/Gtk4DrawArea.h @@ -0,0 +1,51 @@ +// 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; } + + Cairo::Context *GetContext() const { return fContext; } + + void ShowContextMenu(Glib::RefPtr menu_model, int x, int y); + +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; + + 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); + + 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..7c6bf4f1ad774 --- /dev/null +++ b/gui/gtk4canvas/src/Gtk4MethodDialog.cxx @@ -0,0 +1,334 @@ +// 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 "TAttLine.h" +#include "TAttFill.h" +#include "TAttText.h" +#include "TAttMarker.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(20); + set_child(*m_main_box); +} + +//////////////////////////////////////////////////////////////////////////////// +/// add line with color button + +void Gtk4MethodDialog::addColorInput(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 + 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("Color", 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 + +void 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"); + button1->signal_clicked().connect(sigc::mem_fun(*this, &Gtk4MethodDialog::on_ok_button_clicked)); + + auto button2 = Gtk::make_managed("Cancel"); + button2->signal_clicked().connect([this]() { close(); }); + + hbox->append(*button1); + hbox->append(*button2); +} + +//////////////////////////////////////////////////////////////////////////////// +/// 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()); + } + } + + addOkCancelButtons(); + + present(); + + fMenu = menu; + fObject = object; + fFunc = func; +} + + +void Gtk4MethodDialog::attLineDialog(TAttLine *att) +{ + addColorInput(att->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(att->GetLineStyle()); + + addLine("Line style", m_line_style); + + auto adjustment = Gtk::Adjustment::create(/* value */ att->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 + + addLine("Line width", m_line_width); + + addOkCancelButtons(); + + present(); + + fAttLine = att; +} + + +void Gtk4MethodDialog::attFillDialog(TAttFill *att) +{ + addColorInput(att->GetFillColor()); + + addOkCancelButtons(); + + present(); + + fAttFill = att; +} + +void Gtk4MethodDialog::attTextDialog(TAttText *att) +{ + addColorInput(att->GetTextColor()); + + addOkCancelButtons(); + + present(); + + fAttText = att; + +} + +void Gtk4MethodDialog::attMarkerDialog(TAttMarker *att) +{ + addColorInput(att->GetMarkerColor()); + + addOkCancelButtons(); + + present(); + + fAttMarker = att; +} + + +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); + + return; + } + + Gdk::RGBA selected_color; + Color_t newColorIdx = 0; + if (m_color_button) { + auto selected_color = m_color_button->get_rgba(); + newColorIdx = TColor::GetColor(selected_color.get_red(), + selected_color.get_green(), + selected_color.get_blue(), + selected_color.get_alpha()); + } + + if (fAttLine) { + fAttLine->SetLineColor(newColorIdx); + fAttLine->SetLineStyle(m_line_style->get_selected()); + fAttLine->SetLineWidth(m_line_width->get_value()); + } else if (fAttFill) { + fAttFill->SetFillColor(newColorIdx); + } else if (fAttText) { + fAttText->SetTextColor(newColorIdx); + } else if (fAttMarker) { + fAttMarker->SetMarkerColor(newColorIdx); + } + + close(); + + if (gPad) + gPad->ModifiedUpdate(); + +} diff --git a/gui/gtk4canvas/src/Gtk4MethodDialog.h b/gui/gtk4canvas/src/Gtk4MethodDialog.h new file mode 100644 index 0000000000000..58c7483bd9101 --- /dev/null +++ b/gui/gtk4canvas/src/Gtk4MethodDialog.h @@ -0,0 +1,68 @@ +// 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 TAttLine; +class TAttFill; +class TAttText; +class TAttMarker; + +class Gtk4MethodDialog: public Gtk::Window { + + public: + Gtk4MethodDialog(unsigned width = 400, unsigned height = 200); + + void methodDialog(TContextMenu *menu, TObject *object, TFunction* func); + + void attLineDialog(TAttLine *att); + void attFillDialog(TAttFill *att); + void attTextDialog(TAttText *att); + void attMarkerDialog(TAttMarker *att); + + protected: + + void on_ok_button_clicked(); + + void addLine(const char *txt, Gtk::Widget *widget); + + void addColorInput(int colindx); + + void addArg(const char *argname, const char *value, const char *type); + + void addOkCancelButtons(); + + Gtk::Box *m_main_box = nullptr; + + Gtk::ColorDialogButton* m_color_button = nullptr; + + std::vector fArgs; + TContextMenu *fMenu = nullptr; + TObject *fObject = nullptr; + TFunction *fFunc = nullptr; + + TAttLine *fAttLine = nullptr; + TAttFill *fAttFill = nullptr; + TAttText *fAttText = nullptr; + TAttMarker *fAttMarker = nullptr; + + Gtk::DropDown *m_line_style = nullptr; + Gtk::SpinButton *m_line_width = nullptr; +}; + +#endif diff --git a/gui/gtk4canvas/src/TGtk4Canvas.cxx b/gui/gtk4canvas/src/TGtk4Canvas.cxx new file mode 100644 index 0000000000000..0e11f66847767 --- /dev/null +++ b/gui/gtk4canvas/src/TGtk4Canvas.cxx @@ -0,0 +1,391 @@ +// 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; +} + +bool IsAnyModified(TPad *pad) +{ + if (!pad) + return kFALSE; + if (pad->IsModified()) + return kTRUE; + TIter next(pad->GetListOfPrimitives()); + while (auto obj = next()) + if (IsAnyModified(dynamic_cast(obj))) + return kTRUE; + return kFALSE; +} + + +////////////////////////////////////////////////////////////////////////////////////////// +/// if canvas or any subpad was modified, +/// invoke gtk4 queue_draw() which will trigger redraw area + +Bool_t TGtk4Canvas::PerformUpdate(Bool_t async) +{ + if (!IsAnyModified(Canvas()) || !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(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..7d2e11998ace7 --- /dev/null +++ b/gui/gtk4canvas/src/TGtk4ContextMenu.cxx @@ -0,0 +1,389 @@ +// 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 & parameter) { + 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; + }; + + // Add a title + TString buffer = fContextMenu->CreatePopupTitle(object); + addMenuAction(buffer, -1, nullptr); + addSeparator(); + 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, 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/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..496ef4faeacca --- /dev/null +++ b/gui/gtk4canvas/src/TGtk4PadPainter.cxx @@ -0,0 +1,554 @@ +// 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 "TPad.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 TTF glyphs 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); + + // 2. Wrap your raw memory pointer into a Cairo Surface. + // This borrows the memory pointer without making a deep copy. + Cairo::RefPtr surface = + Cairo::ImageSurface::create(argbBuffer, Cairo::Surface::Format::ARGB32, width, height, stride); + + // 3. Set the surface as the source pattern in the context + // (0, 0) specifies the top-left offset to place the image + ctx->set_source(surface, px, py); + + // 4. Paint the pattern onto the context canvas + ctx->paint(); +} From a79e360902ab4dff69c4f090be4fb3cdedd02241 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Fri, 30 Oct 2026 16:21:39 +0100 Subject: [PATCH 02/13] [gtk4canv] introduce TGtk4GedEditor Implemented virtual pad editor interface --- .../TVirtualPadEditor/P030_TGtk4GedEditor.C | 5 + gui/gtk4canvas/CMakeLists.txt | 2 + gui/gtk4canvas/inc/LinkDef.h | 1 + gui/gtk4canvas/inc/TGtk4GedEditor.h | 105 +++++++ gui/gtk4canvas/src/Gtk4MethodDialog.cxx | 127 ++------ gui/gtk4canvas/src/Gtk4MethodDialog.h | 31 +- gui/gtk4canvas/src/TGtk4ContextMenu.cxx | 6 +- gui/gtk4canvas/src/TGtk4GedEditor.cxx | 281 ++++++++++++++++++ 8 files changed, 428 insertions(+), 130 deletions(-) create mode 100644 etc/plugins/TVirtualPadEditor/P030_TGtk4GedEditor.C create mode 100644 gui/gtk4canvas/inc/TGtk4GedEditor.h create mode 100644 gui/gtk4canvas/src/TGtk4GedEditor.cxx 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/gtk4canvas/CMakeLists.txt b/gui/gtk4canvas/CMakeLists.txt index e340fd04d3642..a0e2e9d0e2439 100644 --- a/gui/gtk4canvas/CMakeLists.txt +++ b/gui/gtk4canvas/CMakeLists.txt @@ -28,6 +28,7 @@ ROOT_STANDARD_LIBRARY_PACKAGE(ROOTGtk4Canvas TGtk4Canvas.h TGtk4GuiFactory.h TGtk4PadPainter.h + TGtk4GedEditor.h SOURCES Gtk4DrawArea.cxx Gtk4CanvasWindow.cxx @@ -36,6 +37,7 @@ ROOT_STANDARD_LIBRARY_PACKAGE(ROOTGtk4Canvas TGtk4Canvas.cxx TGtk4GuiFactory.cxx TGtk4PadPainter.cxx + TGtk4GedEditor.cxx LIBRARIES PkgConfig::GTKMM DEPENDENCIES diff --git a/gui/gtk4canvas/inc/LinkDef.h b/gui/gtk4canvas/inc/LinkDef.h index 1ddbde0866e73..bacef72c08f31 100644 --- a/gui/gtk4canvas/inc/LinkDef.h +++ b/gui/gtk4canvas/inc/LinkDef.h @@ -17,5 +17,6 @@ #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/TGtk4GedEditor.h b/gui/gtk4canvas/inc/TGtk4GedEditor.h new file mode 100644 index 0000000000000..d79b639c12e59 --- /dev/null +++ b/gui/gtk4canvas/inc/TGtk4GedEditor.h @@ -0,0 +1,105 @@ +// 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. * + *************************************************************************/ + +#ifndef ROOT_TGtk4GedEditor +#define ROOT_TGtk4GedEditor + +#include "TVirtualPadEditor.h" +#include "TQObject.h" + +#include + +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/src/Gtk4MethodDialog.cxx b/gui/gtk4canvas/src/Gtk4MethodDialog.cxx index 7c6bf4f1ad774..47b3ba69b8aed 100644 --- a/gui/gtk4canvas/src/Gtk4MethodDialog.cxx +++ b/gui/gtk4canvas/src/Gtk4MethodDialog.cxx @@ -21,12 +21,6 @@ #include "TContextMenu.h" #include "TColor.h" -#include "TAttLine.h" -#include "TAttFill.h" -#include "TAttText.h" -#include "TAttMarker.h" - - #include /** \class Gtk4MethodDialog @@ -44,14 +38,23 @@ Gtk4MethodDialog::Gtk4MethodDialog(unsigned width, unsigned height) set_default_size(width, height); m_main_box = Gtk::make_managed(Gtk::Orientation::VERTICAL, 10); - m_main_box->set_margin(20); + 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 -void Gtk4MethodDialog::addColorInput(Int_t colindx) +Gtk::ColorDialogButton *Gtk4MethodDialog::addColorInput(const char *title, Int_t colindx) { TColor *rootColor = gROOT->GetColor(colindx); Gdk::RGBA initial_color; @@ -65,13 +68,15 @@ void Gtk4MethodDialog::addColorInput(Int_t colindx) color_dialog->set_modal(true); // Instantiate the specific ColorDialogButton using our dialog rules - m_color_button = Gtk::make_managed(color_dialog); + 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("Color", m_color_button); + addLine(title, m_color_button); + + return m_color_button; } //////////////////////////////////////////////////////////////////////////////// @@ -114,7 +119,7 @@ void Gtk4MethodDialog::addArg(const char *argname, const char *value, const char //////////////////////////////////////////////////////////////////////////////// /// Add Ok and Cancel buttons -void Gtk4MethodDialog::addOkCancelButtons() +Gtk::Button *Gtk4MethodDialog::addOkCancelButtons() { auto hbox = Gtk::make_managed(); hbox->set_hexpand(true); @@ -122,13 +127,14 @@ void Gtk4MethodDialog::addOkCancelButtons() m_main_box->append(*hbox); auto button1 = Gtk::make_managed("Ok"); - button1->signal_clicked().connect(sigc::mem_fun(*this, &Gtk4MethodDialog::on_ok_button_clicked)); auto button2 = Gtk::make_managed("Cancel"); button2->signal_clicked().connect([this]() { close(); }); hbox->append(*button1); hbox->append(*button2); + + return button1; } //////////////////////////////////////////////////////////////////////////////// @@ -215,7 +221,9 @@ void Gtk4MethodDialog::methodDialog(TContextMenu *menu, TObject *object, TFuncti } } - addOkCancelButtons(); + auto okBtn = addOkCancelButtons(); + + okBtn->signal_clicked().connect(sigc::mem_fun(*this, &Gtk4MethodDialog::on_ok_button_clicked)); present(); @@ -225,67 +233,6 @@ void Gtk4MethodDialog::methodDialog(TContextMenu *menu, TObject *object, TFuncti } -void Gtk4MethodDialog::attLineDialog(TAttLine *att) -{ - addColorInput(att->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(att->GetLineStyle()); - - addLine("Line style", m_line_style); - - auto adjustment = Gtk::Adjustment::create(/* value */ att->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 - - addLine("Line width", m_line_width); - - addOkCancelButtons(); - - present(); - - fAttLine = att; -} - - -void Gtk4MethodDialog::attFillDialog(TAttFill *att) -{ - addColorInput(att->GetFillColor()); - - addOkCancelButtons(); - - present(); - - fAttFill = att; -} - -void Gtk4MethodDialog::attTextDialog(TAttText *att) -{ - addColorInput(att->GetTextColor()); - - addOkCancelButtons(); - - present(); - - fAttText = att; - -} - -void Gtk4MethodDialog::attMarkerDialog(TAttMarker *att) -{ - addColorInput(att->GetMarkerColor()); - - addOkCancelButtons(); - - present(); - - fAttMarker = att; -} - - void Gtk4MethodDialog::on_ok_button_clicked() { if (fMenu && fObject) { @@ -300,35 +247,7 @@ void Gtk4MethodDialog::on_ok_button_clicked() close(); fMenu->Execute(fObject, fFunc, &argslist); - - return; - } - - Gdk::RGBA selected_color; - Color_t newColorIdx = 0; - if (m_color_button) { - auto selected_color = m_color_button->get_rgba(); - newColorIdx = TColor::GetColor(selected_color.get_red(), - selected_color.get_green(), - selected_color.get_blue(), - selected_color.get_alpha()); - } - - if (fAttLine) { - fAttLine->SetLineColor(newColorIdx); - fAttLine->SetLineStyle(m_line_style->get_selected()); - fAttLine->SetLineWidth(m_line_width->get_value()); - } else if (fAttFill) { - fAttFill->SetFillColor(newColorIdx); - } else if (fAttText) { - fAttText->SetTextColor(newColorIdx); - } else if (fAttMarker) { - fAttMarker->SetMarkerColor(newColorIdx); + } else { + close(); } - - close(); - - if (gPad) - gPad->ModifiedUpdate(); - } diff --git a/gui/gtk4canvas/src/Gtk4MethodDialog.h b/gui/gtk4canvas/src/Gtk4MethodDialog.h index 58c7483bd9101..049d0d55017bc 100644 --- a/gui/gtk4canvas/src/Gtk4MethodDialog.h +++ b/gui/gtk4canvas/src/Gtk4MethodDialog.h @@ -18,10 +18,6 @@ class TContextMenu; class TObject; class TFunction; -class TAttLine; -class TAttFill; -class TAttText; -class TAttMarker; class Gtk4MethodDialog: public Gtk::Window { @@ -30,39 +26,26 @@ class Gtk4MethodDialog: public Gtk::Window { void methodDialog(TContextMenu *menu, TObject *object, TFunction* func); - void attLineDialog(TAttLine *att); - void attFillDialog(TAttFill *att); - void attTextDialog(TAttText *att); - void attMarkerDialog(TAttMarker *att); - - protected: - - void on_ok_button_clicked(); + void clearAllElements(); void addLine(const char *txt, Gtk::Widget *widget); - void addColorInput(int colindx); + Gtk::ColorDialogButton *addColorInput(const char *title, int colindx); void addArg(const char *argname, const char *value, const char *type); - void addOkCancelButtons(); + Gtk::Button *addOkCancelButtons(); - Gtk::Box *m_main_box = nullptr; + protected: - Gtk::ColorDialogButton* m_color_button = nullptr; + void on_ok_button_clicked(); + + Gtk::Box *m_main_box = nullptr; std::vector fArgs; TContextMenu *fMenu = nullptr; TObject *fObject = nullptr; TFunction *fFunc = nullptr; - - TAttLine *fAttLine = nullptr; - TAttFill *fAttFill = nullptr; - TAttText *fAttText = nullptr; - TAttMarker *fAttMarker = nullptr; - - Gtk::DropDown *m_line_style = nullptr; - Gtk::SpinButton *m_line_width = nullptr; }; #endif diff --git a/gui/gtk4canvas/src/TGtk4ContextMenu.cxx b/gui/gtk4canvas/src/TGtk4ContextMenu.cxx index 7d2e11998ace7..128ec063ef856 100644 --- a/gui/gtk4canvas/src/TGtk4ContextMenu.cxx +++ b/gui/gtk4canvas/src/TGtk4ContextMenu.cxx @@ -348,6 +348,7 @@ void TGtk4ContextMenu::executeMenu(int id) if (id < kToggleStart) { auto m = (TMethod *) ud; + /* if (!strcmp(m->GetName(), "SetLineAttributes")) { auto attline = dynamic_cast(fContextMenu->GetSelectedObject()); if (attline) { @@ -373,8 +374,9 @@ void TGtk4ContextMenu::executeMenu(int id) widget->attMarkerDialog(attmark); } } else { - fContextMenu->Action(m); - } + */ + fContextMenu->Action(m); + } else if (id >= kToggleStart && id < kToggleListStart) { TToggle *t = (TToggle *) ud; fContextMenu->Action(t); 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(); +} From 4f77d42316c063e57e62768331c59802a11724f8 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 2 Nov 2026 09:59:34 +0100 Subject: [PATCH 03/13] [gtk4canv] correctly check repaint condition There is now special TCanvas method which indicate when canvas has to be repaint --- gui/gtk4canvas/src/Gtk4DrawArea.cxx | 44 ++++++++++++++--------------- gui/gtk4canvas/src/TGtk4Canvas.cxx | 16 +---------- 2 files changed, 22 insertions(+), 38 deletions(-) diff --git a/gui/gtk4canvas/src/Gtk4DrawArea.cxx b/gui/gtk4canvas/src/Gtk4DrawArea.cxx index 90ea81d96cf4b..2c922e7c0d814 100644 --- a/gui/gtk4canvas/src/Gtk4DrawArea.cxx +++ b/gui/gtk4canvas/src/Gtk4DrawArea.cxx @@ -68,34 +68,32 @@ Gtk4DrawArea::Gtk4DrawArea() 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 != fCanvas->GetPadWidth()) || (height != fCanvas->GetPadHeight())) { - fCanvas->Resize(); - } + 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 != fCanvas->GetPadWidth()) || (height != fCanvas->GetPadHeight())) + fCanvas->Resize(); - // clear - cr->set_source_rgb(1., 1., 1.); - cr->paint(); + // clear + cr->set_source_rgb(1., 1., 1.); + cr->paint(); - fContext = cr.get(); + // main place where painting happens - only here fContext pointer is set + fContext = cr.get(); - // TODO: call canvas->paint instead - fCanvas->Paint(); + fCanvas->Paint(); - fContext = nullptr; + fContext = nullptr; } void Gtk4DrawArea::on_mouse_click(int n_press, double x, double y) diff --git a/gui/gtk4canvas/src/TGtk4Canvas.cxx b/gui/gtk4canvas/src/TGtk4Canvas.cxx index 0e11f66847767..fc1a4533fe4d1 100644 --- a/gui/gtk4canvas/src/TGtk4Canvas.cxx +++ b/gui/gtk4canvas/src/TGtk4Canvas.cxx @@ -260,27 +260,13 @@ UInt_t TGtk4Canvas::GetWindowGeometry(Int_t &x, Int_t &y, UInt_t &w, UInt_t &h) return 0; } -bool IsAnyModified(TPad *pad) -{ - if (!pad) - return kFALSE; - if (pad->IsModified()) - return kTRUE; - TIter next(pad->GetListOfPrimitives()); - while (auto obj = next()) - if (IsAnyModified(dynamic_cast(obj))) - return kTRUE; - return kFALSE; -} - - ////////////////////////////////////////////////////////////////////////////////////////// /// if canvas or any subpad was modified, /// invoke gtk4 queue_draw() which will trigger redraw area Bool_t TGtk4Canvas::PerformUpdate(Bool_t async) { - if (!IsAnyModified(Canvas()) || !fDrawArea) + if (!Canvas()->IsAnyNeedRepaint() || !fDrawArea) return kTRUE; fDrawArea->queue_draw(); From ad0b9c7680b6a3d7024137bec938bf382b27af35 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 2 Nov 2026 10:45:50 +0100 Subject: [PATCH 04/13] [gtk4canv] add status_changed signal in draw area Use this signal to display status in the canvas --- gui/gtk4canvas/src/Gtk4CanvasWindow.cxx | 12 +++++++++--- gui/gtk4canvas/src/Gtk4CanvasWindow.h | 5 +++++ gui/gtk4canvas/src/Gtk4DrawArea.cxx | 22 ++++++++++++++++++++++ gui/gtk4canvas/src/Gtk4DrawArea.h | 5 +++++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx b/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx index bdae7b305d702..ffa15afb2540a 100644 --- a/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx +++ b/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx @@ -55,7 +55,6 @@ Gtk4CanvasWindow::Gtk4CanvasWindow(unsigned width, unsigned height) m_action_group->add_action("close", [this]() { close(); }); m_action_group->add_action("quit", []() { - printf("Quit ROOT action\n"); gROOT->SetInterrupt(kTRUE); if (gApplication) @@ -119,9 +118,10 @@ Gtk4CanvasWindow::Gtk4CanvasWindow(unsigned width, unsigned height) m_DrawArea.set_vexpand(true); m_Box.append(m_DrawArea); + m_DrawArea.status_changed().connect(sigc::mem_fun(*this, &Gtk4CanvasWindow::on_status_changed)); - unsigned int context_id = m_StatusBar.get_context_id("main_status"); - m_StatusBar.push("Ready to draw", context_id); + fContextId = m_StatusBar.get_context_id("main_status"); + m_StatusBar.push("Ready to draw", fContextId); m_Box.append(m_StatusBar); } @@ -129,6 +129,12 @@ Gtk4CanvasWindow::~Gtk4CanvasWindow() { } +void Gtk4CanvasWindow::on_status_changed(const char *msg) +{ + m_StatusBar.remove_all_messages(fContextId); + m_StatusBar.push(msg, fContextId); +} + /* void Gtk4CanvasWindow::on_action_file_quit() diff --git a/gui/gtk4canvas/src/Gtk4CanvasWindow.h b/gui/gtk4canvas/src/Gtk4CanvasWindow.h index 469e58855e5e2..be5507afffe77 100644 --- a/gui/gtk4canvas/src/Gtk4CanvasWindow.h +++ b/gui/gtk4canvas/src/Gtk4CanvasWindow.h @@ -41,6 +41,10 @@ class Gtk4CanvasWindow : public Gtk::Window private: //Signal handlers: + + + void on_status_changed(const char *msg); + //void on_action_file_new(); //void on_action_file_quit(); //void on_action_others(); @@ -54,6 +58,7 @@ class Gtk4CanvasWindow : public Gtk::Window Gtk4DrawArea m_DrawArea; Gtk::Statusbar m_StatusBar; + unsigned int fContextId = 0; }; diff --git a/gui/gtk4canvas/src/Gtk4DrawArea.cxx b/gui/gtk4canvas/src/Gtk4DrawArea.cxx index 2c922e7c0d814..bf308edb70c75 100644 --- a/gui/gtk4canvas/src/Gtk4DrawArea.cxx +++ b/gui/gtk4canvas/src/Gtk4DrawArea.cxx @@ -182,8 +182,30 @@ void Gtk4DrawArea::on_mouse_move(double x, double y) } 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) { if (fCanvas) diff --git a/gui/gtk4canvas/src/Gtk4DrawArea.h b/gui/gtk4canvas/src/Gtk4DrawArea.h index 0e99c20928a65..d240cc30c3e30 100644 --- a/gui/gtk4canvas/src/Gtk4DrawArea.h +++ b/gui/gtk4canvas/src/Gtk4DrawArea.h @@ -27,6 +27,8 @@ class Gtk4DrawArea : public Gtk::DrawingArea { void ShowContextMenu(Glib::RefPtr menu_model, 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); @@ -35,6 +37,7 @@ class Gtk4DrawArea : public Gtk::DrawingArea { Cairo::Context *fContext = nullptr; double fLastMouseX = 0; double fLastMouseY = 0; + bool fShowEventStatus = true; void on_mouse_click(int n_press, double x, double y); void on_mouse_released(int n_press, double x, double y); @@ -43,6 +46,8 @@ class Gtk4DrawArea : public Gtk::DrawingArea { void on_mouse_leave(); bool on_mouse_scroll(double dx, double dy); + sigc::signal m_status_changed; + Glib::RefPtr m_click_gesture; Glib::RefPtr m_motion_controller; Glib::RefPtr m_scroll_controller; From e2132052d7d54190aaecb2a6ef7d1e6b22c44ab0 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 2 Nov 2026 10:56:56 +0100 Subject: [PATCH 05/13] [gtk4canv] introduce canvas toolbar Let toggle visibility state via correspondent menu items --- gui/gtk4canvas/src/Gtk4CanvasWindow.cxx | 335 +++++++++++++++++++++--- gui/gtk4canvas/src/Gtk4CanvasWindow.h | 18 +- gui/gtk4canvas/src/Gtk4DrawArea.h | 1 + gui/gtk4canvas/src/TGtk4Canvas.cxx | 2 +- 4 files changed, 311 insertions(+), 45 deletions(-) diff --git a/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx b/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx index ffa15afb2540a..b8c05786d3925 100644 --- a/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx +++ b/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx @@ -31,22 +31,102 @@ #include "TROOT.h" #include "TTimer.h" +#include "TCanvas.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(unsigned width, unsigned height) -: m_Box(Gtk::Orientation::VERTICAL) +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_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() +{ +} + +void Gtk4CanvasWindow::fillMenuBar(TCanvas *canv) +{ m_action_group = Gio::SimpleActionGroup::create(); m_action_group->add_action("new", []() { std::cout << "New canvas Clicked!\n"; }); @@ -54,51 +134,73 @@ Gtk4CanvasWindow::Gtk4CanvasWindow(unsigned width, unsigned height) 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("win", m_action_group); + insert_action_group("canvas", m_action_group); // 3. Construct the Abstract Menu Tree Model auto menu_model = Gio::Menu::create(); - // --- SECTION A: The "File" Top Level Dropdown Menu --- + + // --- SECTION "File" auto sec1 = Gio::Menu::create(); - sec1->append("New Canvas", "win.new"); - sec1->append("Open", "win.open"); - sec1->append("Close Canvas", "win.close"); + sec1->append("New Canvas", "canvas.new"); + sec1->append("Open", "canvas.open"); + sec1->append("Close Canvas", "canvas.close"); auto sec2 = Gio::Menu::create(); - sec2->append("Save", "win.save"); - sec2->append("Save as", "win.saveas"); + sec2->append("Save", "canvas.save"); + sec2->append("Save as", "canvas.saveas"); auto sec3 = Gio::Menu::create(); - sec3->append("Quit ROOT", "win.quit"); + 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 B: The "Edit" Top Level Dropdown Menu --- + + // --- SECTION "Edit" auto edit_menu = Gio::Menu::create(); - edit_menu->append("Clear pad", "win.clearpad"); - edit_menu->append("Clear canvas", "win.clearcanvas"); + 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", "win.paste"); - special_paste_submenu->append("Paste with Formatting", "win.paste"); + 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); @@ -106,27 +208,149 @@ Gtk4CanvasWindow::Gtk4CanvasWindow(unsigned width, unsigned height) // 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); - // 5. Append the layout widget to the window canvas + menu_bar->set_visible(isShowMenuBar); + m_Box.append(*menu_bar); +} - m_DrawArea.set_hexpand(true); - m_DrawArea.set_vexpand(true); - m_Box.append(m_DrawArea); +Gtk::Image* create_resized_xpm_image(const char *filename) +{ + // 2. Initialize a pointer for our final texture image + Glib::RefPtr texture; - m_DrawArea.status_changed().connect(sigc::mem_fun(*this, &Gtk4CanvasWindow::on_status_changed)); + try { + // Load the original XPM file safely into a Pixbuf + auto original_pixbuf = Gdk::Pixbuf::create_from_file(filename); - fContextId = m_StatusBar.get_context_id("main_status"); - m_StatusBar.push("Ready to draw", fContextId); - m_Box.append(m_StatusBar); + // 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; } -Gtk4CanvasWindow::~Gtk4CanvasWindow() + +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) @@ -135,32 +359,57 @@ void Gtk4CanvasWindow::on_status_changed(const char *msg) 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); -void Gtk4CanvasWindow::on_action_file_quit() + // 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) { - close(); + 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_action_toggle() +void Gtk4CanvasWindow::on_show_tooltip_changed(const Glib::VariantBase &value) { - std::cout << "The toggle menu item was selected." << std::endl; + bool is_checked = Glib::VariantBase::cast_dynamic>(value).get(); - bool active = false; - m_refActionRain->get_state(active); + // m_StatusBar.set_visible(is_checked); - //The toggle action's state does not change automatically: - active = !active; - m_refActionRain->change_state(active); + // 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); + } - Glib::ustring message; - if(active) - message = "Toggle is active."; - else - message = "Toggle is not active"; +} - std::cout << message << std::endl; +void Gtk4CanvasWindow::on_openRootFile() +{ +} +void Gtk4CanvasWindow::on_saveCanvasAs() +{ } -*/ \ No newline at end of file +void Gtk4CanvasWindow::on_printCanvas() +{ +} diff --git a/gui/gtk4canvas/src/Gtk4CanvasWindow.h b/gui/gtk4canvas/src/Gtk4CanvasWindow.h index be5507afffe77..3e1723d4f10df 100644 --- a/gui/gtk4canvas/src/Gtk4CanvasWindow.h +++ b/gui/gtk4canvas/src/Gtk4CanvasWindow.h @@ -31,10 +31,12 @@ #include "Gtk4DrawArea.h" +class TCanvas; + class Gtk4CanvasWindow : public Gtk::Window { public: - Gtk4CanvasWindow(unsigned width = 400, unsigned height = 200); + Gtk4CanvasWindow(TCanvas *canv = nullptr, unsigned width = 400, unsigned height = 200); virtual ~Gtk4CanvasWindow(); Gtk4DrawArea *GetDrawArea() { return &m_DrawArea; } @@ -44,22 +46,36 @@ class Gtk4CanvasWindow : public Gtk::Window 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_openRootFile(); + 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 fillMenuBar(TCanvas *canv); + + void fillToolBar(); + + void clickToolButton(int id); + //Child widgets: Gtk::Box m_Box; Glib::RefPtr m_action_group; + 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.h b/gui/gtk4canvas/src/Gtk4DrawArea.h index d240cc30c3e30..05085f771420b 100644 --- a/gui/gtk4canvas/src/Gtk4DrawArea.h +++ b/gui/gtk4canvas/src/Gtk4DrawArea.h @@ -22,6 +22,7 @@ class Gtk4DrawArea : public Gtk::DrawingArea { virtual ~Gtk4DrawArea() = default; void SetCanvas(TCanvas *c) { fCanvas = c; } + TCanvas *GetCanvas() const { return fCanvas; } Cairo::Context *GetContext() const { return fContext; } diff --git a/gui/gtk4canvas/src/TGtk4Canvas.cxx b/gui/gtk4canvas/src/TGtk4Canvas.cxx index fc1a4533fe4d1..08e831e0dc5a7 100644 --- a/gui/gtk4canvas/src/TGtk4Canvas.cxx +++ b/gui/gtk4canvas/src/TGtk4Canvas.cxx @@ -331,7 +331,7 @@ TCanvasImp *TGtk4Canvas::NewCanvas(TCanvas *c, const char *name, Int_t x, Int_t timer->TurnOn(); } - auto widget = new Gtk4CanvasWindow(width, height); + auto widget = new Gtk4CanvasWindow(c, width, height); auto draw_area = widget->GetDrawArea(); From ce1d47cb84653c874e9c60938a9f7bd3b3816b99 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 2 Nov 2026 17:07:24 +0100 Subject: [PATCH 06/13] [gtk4canv] implement tooltips --- gui/gtk4canvas/src/Gtk4CanvasWindow.cxx | 5 ++-- gui/gtk4canvas/src/Gtk4DrawArea.cxx | 33 ++++++++++++++++++++++++- gui/gtk4canvas/src/Gtk4DrawArea.h | 1 + 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx b/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx index b8c05786d3925..ce9ef1bf96045 100644 --- a/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx +++ b/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx @@ -108,7 +108,7 @@ Gtk4CanvasWindow::Gtk4CanvasWindow(TCanvas *canv, unsigned width, unsigned heigh 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); @@ -148,7 +148,6 @@ void Gtk4CanvasWindow::fillMenuBar(TCanvas *canv) 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); @@ -391,7 +390,7 @@ void Gtk4CanvasWindow::on_show_tooltip_changed(const Glib::VariantBase &value) { bool is_checked = Glib::VariantBase::cast_dynamic>(value).get(); - // m_StatusBar.set_visible(is_checked); + 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"); diff --git a/gui/gtk4canvas/src/Gtk4DrawArea.cxx b/gui/gtk4canvas/src/Gtk4DrawArea.cxx index bf308edb70c75..a965a3cd650dc 100644 --- a/gui/gtk4canvas/src/Gtk4DrawArea.cxx +++ b/gui/gtk4canvas/src/Gtk4DrawArea.cxx @@ -64,6 +64,9 @@ Gtk4DrawArea::Gtk4DrawArea() ); 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) @@ -81,7 +84,7 @@ void Gtk4DrawArea::on_draw(const Cairo::RefPtr& cr, int width, i return; } - if ((width != fCanvas->GetPadWidth()) || (height != fCanvas->GetPadHeight())) + if ((width != (int) fCanvas->GetPadWidth()) || (height != (int) fCanvas->GetPadHeight())) fCanvas->Resize(); // clear @@ -123,6 +126,8 @@ void Gtk4DrawArea::on_mouse_click(int n_press, double x, double y) void Gtk4DrawArea::on_mouse_released(int n_press, double x, double y) { + (void) n_press; + if (!fCanvas) return; @@ -208,6 +213,7 @@ sigc::signal Gtk4DrawArea::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); @@ -230,3 +236,28 @@ void Gtk4DrawArea::ShowContextMenu(Glib::RefPtr menu_model, int x, in 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 index 05085f771420b..28582418c9fac 100644 --- a/gui/gtk4canvas/src/Gtk4DrawArea.h +++ b/gui/gtk4canvas/src/Gtk4DrawArea.h @@ -46,6 +46,7 @@ class Gtk4DrawArea : public Gtk::DrawingArea { 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; From 8dcc1f55adc28a31868eaaf9004868e1a6a2566a Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Mon, 2 Nov 2026 17:34:05 +0100 Subject: [PATCH 07/13] [gtk4canv] implement open file dialog --- gui/gtk4canvas/src/Gtk4CanvasWindow.cxx | 48 ++++++++++++++++++++++++- gui/gtk4canvas/src/Gtk4CanvasWindow.h | 3 ++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx b/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx index ce9ef1bf96045..cb5b428379e6d 100644 --- a/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx +++ b/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx @@ -31,6 +31,7 @@ #include "TROOT.h" #include "TTimer.h" +#include "TFile.h" #include "TCanvas.h" #include "TApplication.h" @@ -130,7 +131,7 @@ 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", []() { std::cout << "Open File Clicked!\n"; }); + m_action_group->add_action("open", [this]() { on_openRootFile(); }); m_action_group->add_action("close", [this]() { close(); }); m_action_group->add_action("quit", []() { @@ -403,6 +404,51 @@ void Gtk4CanvasWindow::on_show_tooltip_changed(const Glib::VariantBase &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)); +} + +void Gtk4CanvasWindow::on_file_selected(const Glib::RefPtr &result) +{ + try { + // Retrieve the selected file reference + Glib::RefPtr file = m_file_dialog->open_finish(result); + + std::cout << "Selected File Path: " << file->get_path() << std::endl; + + 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; + } } void Gtk4CanvasWindow::on_saveCanvasAs() diff --git a/gui/gtk4canvas/src/Gtk4CanvasWindow.h b/gui/gtk4canvas/src/Gtk4CanvasWindow.h index 3e1723d4f10df..648f729cd784d 100644 --- a/gui/gtk4canvas/src/Gtk4CanvasWindow.h +++ b/gui/gtk4canvas/src/Gtk4CanvasWindow.h @@ -49,6 +49,7 @@ class Gtk4CanvasWindow : public Gtk::Window 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_openRootFile(); void on_saveCanvasAs(); void on_printCanvas(); @@ -70,6 +71,8 @@ class Gtk4CanvasWindow : public Gtk::Window Glib::RefPtr m_action_group; + Glib::RefPtr m_file_dialog; + Gtk::Box m_ToolBar; Gtk4DrawArea m_DrawArea; Gtk::Statusbar m_StatusBar; From e11561dd6876b77d1af9928bfd274d4fce83b321 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Tue, 3 Nov 2026 10:30:27 +0100 Subject: [PATCH 08/13] [gtk4canv] provide save as dialog --- gui/gtk4canvas/src/Gtk4CanvasWindow.cxx | 116 +++++++++++++++++++++++- gui/gtk4canvas/src/Gtk4CanvasWindow.h | 65 ++++++------- 2 files changed, 146 insertions(+), 35 deletions(-) diff --git a/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx b/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx index cb5b428379e6d..6898b167d2293 100644 --- a/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx +++ b/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx @@ -27,16 +27,18 @@ #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, @@ -132,6 +134,7 @@ void Gtk4CanvasWindow::fillMenuBar(TCanvas *canv) 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(); }); m_action_group->add_action("close", [this]() { close(); }); m_action_group->add_action("quit", []() { @@ -435,14 +438,15 @@ void Gtk4CanvasWindow::on_openRootFile() 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); - std::cout << "Selected File Path: " << file->get_path() << std::endl; - TFile::Open(file->get_path().c_str(), "update"); } catch (const Gtk::DialogError &err) { @@ -451,10 +455,116 @@ void Gtk4CanvasWindow::on_file_selected(const Glib::RefPtr &re } } +//////////////////////////////////////////////////////////////////////////////// +/// 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; +} + + +//////////////////////////////////////////////////////////////////////////////// +/// 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 index 648f729cd784d..17e88efd11f98 100644 --- a/gui/gtk4canvas/src/Gtk4CanvasWindow.h +++ b/gui/gtk4canvas/src/Gtk4CanvasWindow.h @@ -30,55 +30,56 @@ #include #include "Gtk4DrawArea.h" +#include +#include class TCanvas; -class Gtk4CanvasWindow : public Gtk::Window -{ +class Gtk4CanvasWindow : public Gtk::Window { public: - Gtk4CanvasWindow(TCanvas *canv = nullptr, unsigned width = 400, unsigned height = 200); - virtual ~Gtk4CanvasWindow(); + Gtk4CanvasWindow(TCanvas *canv = nullptr, unsigned width = 400, unsigned height = 200); + virtual ~Gtk4CanvasWindow(); - Gtk4DrawArea *GetDrawArea() { return &m_DrawArea; } + Gtk4DrawArea *GetDrawArea() { return &m_DrawArea; } private: - //Signal handlers: + 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_openRootFile(); - void on_saveCanvasAs(); - void on_printCanvas(); + 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_saveCanvasAs(); + void on_printCanvas(); - //void on_action_file_new(); - //void on_action_file_quit(); - //void on_action_others(); - //void on_action_toggle(); + // void on_action_file_new(); + // void on_action_file_quit(); + // void on_action_others(); + // void on_action_toggle(); - void fillMenuBar(TCanvas *canv); + void fillMenuBar(TCanvas *canv); - void fillToolBar(); + void fillToolBar(); - void clickToolButton(int id); + void clickToolButton(int id); + // Child widgets: + Gtk::Box m_Box; - //Child widgets: - Gtk::Box m_Box; - - Glib::RefPtr m_action_group; - - Glib::RefPtr m_file_dialog; - - Gtk::Box m_ToolBar; - Gtk4DrawArea m_DrawArea; - Gtk::Statusbar m_StatusBar; - unsigned int fContextId = 0; + Glib::RefPtr m_action_group; + 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 From 6582c5f645767b2bbee1b32c76dc173a07d5b649 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Tue, 3 Nov 2026 12:29:17 +0100 Subject: [PATCH 09/13] [gtk4canv] use cairo image as cache for drawing Allows to keep full canvas for any screenshots --- gui/gtk4canvas/src/Gtk4DrawArea.cxx | 20 ++++++++++++++++---- gui/gtk4canvas/src/Gtk4DrawArea.h | 1 + 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/gui/gtk4canvas/src/Gtk4DrawArea.cxx b/gui/gtk4canvas/src/Gtk4DrawArea.cxx index a965a3cd650dc..1d2798454f82f 100644 --- a/gui/gtk4canvas/src/Gtk4DrawArea.cxx +++ b/gui/gtk4canvas/src/Gtk4DrawArea.cxx @@ -87,16 +87,28 @@ void Gtk4DrawArea::on_draw(const Cairo::RefPtr& cr, int width, i if ((width != (int) fCanvas->GetPadWidth()) || (height != (int) fCanvas->GetPadHeight())) fCanvas->Resize(); - // clear - cr->set_source_rgb(1., 1., 1.); - cr->paint(); + 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 - fContext = cr.get(); + // 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) diff --git a/gui/gtk4canvas/src/Gtk4DrawArea.h b/gui/gtk4canvas/src/Gtk4DrawArea.h index 28582418c9fac..8005cbddae556 100644 --- a/gui/gtk4canvas/src/Gtk4DrawArea.h +++ b/gui/gtk4canvas/src/Gtk4DrawArea.h @@ -39,6 +39,7 @@ class Gtk4DrawArea : public Gtk::DrawingArea { 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); From 1f7aef23b0b388a47e747f239617753e49bc2684 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Tue, 3 Nov 2026 12:53:32 +0100 Subject: [PATCH 10/13] [gtk4canv] implement FillImageFromPad Using cached cairo image, exract full image or just pad as ASImage. So one implements missing screenshot functionality of gtk4 --- gui/gtk4canvas/inc/TGtk4PadPainter.h | 2 + gui/gtk4canvas/src/Gtk4DrawArea.h | 4 ++ gui/gtk4canvas/src/TGtk4PadPainter.cxx | 77 ++++++++++++++++++++++---- 3 files changed, 72 insertions(+), 11 deletions(-) diff --git a/gui/gtk4canvas/inc/TGtk4PadPainter.h b/gui/gtk4canvas/inc/TGtk4PadPainter.h index 20507ce86e9d5..65fbc98dc3deb 100644 --- a/gui/gtk4canvas/inc/TGtk4PadPainter.h +++ b/gui/gtk4canvas/inc/TGtk4PadPainter.h @@ -84,6 +84,8 @@ friend class TGtk4Canvas; 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: diff --git a/gui/gtk4canvas/src/Gtk4DrawArea.h b/gui/gtk4canvas/src/Gtk4DrawArea.h index 8005cbddae556..012e94b9b9135 100644 --- a/gui/gtk4canvas/src/Gtk4DrawArea.h +++ b/gui/gtk4canvas/src/Gtk4DrawArea.h @@ -26,10 +26,14 @@ class Gtk4DrawArea : public Gtk::DrawingArea { Cairo::Context *GetContext() const { return fContext; } + Cairo::ImageSurface *GetCache() const { return fDrawingCache.get(); } + void ShowContextMenu(Glib::RefPtr menu_model, 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); diff --git a/gui/gtk4canvas/src/TGtk4PadPainter.cxx b/gui/gtk4canvas/src/TGtk4PadPainter.cxx index 496ef4faeacca..3883fc195982b 100644 --- a/gui/gtk4canvas/src/TGtk4PadPainter.cxx +++ b/gui/gtk4canvas/src/TGtk4PadPainter.cxx @@ -14,7 +14,7 @@ #include "TStyle.h" #include "TEnv.h" #include "TMath.h" -#include "TPad.h" +#include "TCanvas.h" #include "TPoint.h" #include "TROOT.h" #include "TColor.h" @@ -524,7 +524,7 @@ void TGtk4PadPainter::DrawTTFglyphs(Int_t px, Int_t py, TTFhandle &ttf, ETextMod //////////////////////////////////////////////////////////////////////////////// -/// Render TTF glyphs on drawable area +/// Render image on drawable area void TGtk4PadPainter::DrawImage(TImage *img, Int_t px, Int_t py, Int_t) { @@ -540,15 +540,70 @@ void TGtk4PadPainter::DrawImage(TImage *img, Int_t px, Int_t py, Int_t) int stride = Cairo::ImageSurface::format_stride_for_width(Cairo::Surface::Format::ARGB32, width); - // 2. Wrap your raw memory pointer into a Cairo Surface. - // This borrows the memory pointer without making a deep copy. - Cairo::RefPtr surface = - Cairo::ImageSurface::create(argbBuffer, Cairo::Surface::Format::ARGB32, width, height, stride); + 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; - // 3. Set the surface as the source pattern in the context - // (0, 0) specifies the top-left offset to place the image - ctx->set_source(surface, px, py); - // 4. Paint the pattern onto the context canvas - ctx->paint(); } From beeb0918e6310bfb12675f31874203300f51265a Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Tue, 3 Nov 2026 14:20:13 +0100 Subject: [PATCH 11/13] [gtk4canv] implement save menu Include all supported file formats Update items when menu state changed - in case when canvas name is changed Include only single entry for ps/eps formats --- gui/gtk4canvas/src/Gtk4CanvasWindow.cxx | 63 ++++++++++++++++++++++++- gui/gtk4canvas/src/Gtk4CanvasWindow.h | 4 ++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx b/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx index 6898b167d2293..c41dc7afaa4a2 100644 --- a/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx +++ b/gui/gtk4canvas/src/Gtk4CanvasWindow.cxx @@ -128,6 +128,34 @@ 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(); @@ -136,6 +164,17 @@ void Gtk4CanvasWindow::fillMenuBar(TCanvas *canv) 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); @@ -180,8 +219,11 @@ void Gtk4CanvasWindow::fillMenuBar(TCanvas *canv) sec1->append("Close Canvas", "canvas.close"); auto sec2 = Gio::Menu::create(); - sec2->append("Save", "canvas.save"); - sec2->append("Save as", "canvas.saveas"); + + 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"); @@ -227,6 +269,10 @@ void Gtk4CanvasWindow::fillMenuBar(TCanvas *canv) menu_bar->set_visible(isShowMenuBar); + menu_bar->signal_state_flags_changed().connect([this](Gtk::StateFlags) { + fillSaveMenu(m_DrawArea.GetCanvas()); + }); + m_Box.append(*menu_bar); } @@ -484,6 +530,19 @@ const std::vector &Gtk4CanvasWindow::GetSupportedFileFormats() cons } +//////////////////////////////////////////////////////////////////////////////// +/// 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 diff --git a/gui/gtk4canvas/src/Gtk4CanvasWindow.h b/gui/gtk4canvas/src/Gtk4CanvasWindow.h index 17e88efd11f98..cba611be94f44 100644 --- a/gui/gtk4canvas/src/Gtk4CanvasWindow.h +++ b/gui/gtk4canvas/src/Gtk4CanvasWindow.h @@ -54,6 +54,7 @@ class Gtk4CanvasWindow : public Gtk::Window { 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(); @@ -62,6 +63,7 @@ class Gtk4CanvasWindow : public Gtk::Window { // void on_action_others(); // void on_action_toggle(); + void fillSaveMenu(TCanvas *canv); void fillMenuBar(TCanvas *canv); void fillToolBar(); @@ -72,6 +74,8 @@ class Gtk4CanvasWindow : public Gtk::Window { 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; From 121fbe1fb7e80c8c49118a3df5ccfab1d507782d Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Wed, 4 Nov 2026 10:10:32 +0100 Subject: [PATCH 12/13] [gtk4canv] optimize context menu style Make items more compact, use custom header item --- gui/gtk4canvas/src/Gtk4DrawArea.cxx | 45 +++++++++++++++++++++++-- gui/gtk4canvas/src/Gtk4DrawArea.h | 2 +- gui/gtk4canvas/src/TGtk4ContextMenu.cxx | 11 +++--- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/gui/gtk4canvas/src/Gtk4DrawArea.cxx b/gui/gtk4canvas/src/Gtk4DrawArea.cxx index 1d2798454f82f..f6c680037e230 100644 --- a/gui/gtk4canvas/src/Gtk4DrawArea.cxx +++ b/gui/gtk4canvas/src/Gtk4DrawArea.cxx @@ -232,13 +232,54 @@ bool Gtk4DrawArea::on_mouse_scroll(double dx, double dy) return true; } +/////////////////////////////////////////////////////////////////////////////////////////// +/// Shows context menu with provided items and title at specified position -void Gtk4DrawArea::ShowContextMenu(Glib::RefPtr menu_model, int x, int y) +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 @@ -246,10 +287,8 @@ void Gtk4DrawArea::ShowContextMenu(Glib::RefPtr menu_model, int x, in // 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) diff --git a/gui/gtk4canvas/src/Gtk4DrawArea.h b/gui/gtk4canvas/src/Gtk4DrawArea.h index 012e94b9b9135..2a31f10e13f43 100644 --- a/gui/gtk4canvas/src/Gtk4DrawArea.h +++ b/gui/gtk4canvas/src/Gtk4DrawArea.h @@ -28,7 +28,7 @@ class Gtk4DrawArea : public Gtk::DrawingArea { Cairo::ImageSurface *GetCache() const { return fDrawingCache.get(); } - void ShowContextMenu(Glib::RefPtr menu_model, int x, int y); + void ShowContextMenu(Glib::RefPtr menu_model, const char *title, int x, int y); sigc::signal status_changed(); diff --git a/gui/gtk4canvas/src/TGtk4ContextMenu.cxx b/gui/gtk4canvas/src/TGtk4ContextMenu.cxx index 128ec063ef856..693f8351fca9d 100644 --- a/gui/gtk4canvas/src/TGtk4ContextMenu.cxx +++ b/gui/gtk4canvas/src/TGtk4ContextMenu.cxx @@ -124,7 +124,7 @@ void TGtk4ContextMenu::DisplayPopup(Int_t x, Int_t y) 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 & parameter) { + action->signal_activate().connect([action,this, id](const Glib::VariantBase &) { bool current; action->get_state(current); action->set_state(Glib::Variant::create(!current)); @@ -149,12 +149,9 @@ void TGtk4ContextMenu::DisplayPopup(Int_t x, Int_t y) fCustomArg[id] = arg; }; - // Add a title - TString buffer = fContextMenu->CreatePopupTitle(object); - addMenuAction(buffer, -1, nullptr); - addSeparator(); - bool last_separ = true; + TString title = fContextMenu->CreatePopupTitle(object); + bool last_separ = true; int entry = 0, toggle = kToggleStart, togglelist = kToggleListStart; int userfunction = kUserFunctionStart; @@ -277,7 +274,7 @@ void TGtk4ContextMenu::DisplayPopup(Int_t x, Int_t y) widget->insert_action_group("context", action_group); - widget->ShowContextMenu(menu_model, x, y); + widget->ShowContextMenu(menu_model, title.Data(), x, y); } //////////////////////////////////////////////////////////////////////////////// From 1d90f14c85c5548763b8aca565a2a24dd1d1627a Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Tue, 10 Nov 2026 08:09:17 +0100 Subject: [PATCH 13/13] [gtk4canv] Add short info about compilation on Windows --- gui/gtk4canvas/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gui/gtk4canvas/index.md b/gui/gtk4canvas/index.md index 5d5a207c53d88..1e6ce2479d313 100644 --- a/gui/gtk4canvas/index.md +++ b/gui/gtk4canvas/index.md @@ -20,3 +20,9 @@ To activate GTK4 canvas in ROOT application one should create .rootrc file with 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