From 75e78f9088dab4323ff978617c87b3a6637207f9 Mon Sep 17 00:00:00 2001
From: Jeff Young <jeff@rokeby.ie>
Date: Fri, 16 Jun 2023 15:29:31 +0100
Subject: [PATCH] Move bitmap2cmp and pcb_calculator to tool framework.

Fixes https://gitlab.com/kicad/code/kicad/-/issues/1939

Fixes https://gitlab.com/kicad/code/kicad/-/issues/7561
---
 bitmap2component/CMakeLists.txt           |   5 +-
 bitmap2component/bitmap2cmp_control.cpp   |  62 +++++++++
 bitmap2component/bitmap2cmp_control.h     |  64 +++++++++
 bitmap2component/bitmap2cmp_frame.cpp     | 124 +++++++++++++----
 bitmap2component/bitmap2cmp_frame.h       |  61 +--------
 bitmap2component/bitmap2cmp_panel.cpp     |  32 +++--
 bitmap2component/bitmap2cmp_panel.h       |  77 +++++++++--
 common/tool/action_menu.cpp               |  11 ++
 eeschema/tools/simulator_control.h        |   3 +-
 include/tool/action_menu.h                |  11 +-
 pcb_calculator/CMakeLists.txt             |   9 +-
 pcb_calculator/pcb_calculator_control.cpp |  54 ++++++++
 pcb_calculator/pcb_calculator_control.h   |  63 +++++++++
 pcb_calculator/pcb_calculator_frame.cpp   | 159 ++++++++++++++++++----
 pcb_calculator/pcb_calculator_frame.h     |  13 +-
 15 files changed, 603 insertions(+), 145 deletions(-)
 create mode 100644 bitmap2component/bitmap2cmp_control.cpp
 create mode 100644 bitmap2component/bitmap2cmp_control.h
 create mode 100644 pcb_calculator/pcb_calculator_control.cpp
 create mode 100644 pcb_calculator/pcb_calculator_control.h

diff --git a/bitmap2component/CMakeLists.txt b/bitmap2component/CMakeLists.txt
index 95f17e3fbc..8152e38406 100644
--- a/bitmap2component/CMakeLists.txt
+++ b/bitmap2component/CMakeLists.txt
@@ -11,11 +11,12 @@ include_directories( ${INC_AFTER} )
 
 set( BITMAP2COMPONENT_SRCS
     ${CMAKE_SOURCE_DIR}/common/single_top.cpp
+    bitmap2cmp_control.cpp
     bitmap2cmp_main.cpp
     bitmap2cmp_settings.cpp
     bitmap2component.cpp
-        bitmap2cmp_panel_base.cpp
-        bitmap2cmp_frame.cpp
+    bitmap2cmp_panel_base.cpp
+    bitmap2cmp_frame.cpp
     bitmap2cmp_panel.cpp
     bitmap2cmp_panel_base.cpp
     ../common/env_vars.cpp      # needed on MSW to avoid a link issue (a symbol not found)
diff --git a/bitmap2component/bitmap2cmp_control.cpp b/bitmap2component/bitmap2cmp_control.cpp
new file mode 100644
index 0000000000..0ac8968b21
--- /dev/null
+++ b/bitmap2component/bitmap2cmp_control.cpp
@@ -0,0 +1,62 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2023 KiCad Developers, see AUTHORS.txt for contributors.
+ *
+ * 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, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+#include <kiway.h>
+#include <tool/tool_manager.h>
+#include <tool/actions.h>
+#include <bitmap2cmp_frame.h>
+#include <bitmap2cmp_control.h>
+
+
+bool BITMAP2CMP_CONTROL::Init()
+{
+    Reset( MODEL_RELOAD );
+    return true;
+}
+
+
+void BITMAP2CMP_CONTROL::Reset( RESET_REASON aReason )
+{
+    m_frame = getEditFrame<BITMAP2CMP_FRAME>();
+}
+
+
+int BITMAP2CMP_CONTROL::Open( const TOOL_EVENT& aEvent )
+{
+    m_frame->OnLoadFile();
+    return 0;
+}
+
+
+int BITMAP2CMP_CONTROL::Close( const TOOL_EVENT& aEvent )
+{
+    m_frame->Close();
+    return 0;
+}
+
+
+void BITMAP2CMP_CONTROL::setTransitions()
+{
+    Go( &BITMAP2CMP_CONTROL::Open,                   ACTIONS::open.MakeEvent() );
+    Go( &BITMAP2CMP_CONTROL::Close,                  ACTIONS::quit.MakeEvent() );
+}
diff --git a/bitmap2component/bitmap2cmp_control.h b/bitmap2component/bitmap2cmp_control.h
new file mode 100644
index 0000000000..c39418fb85
--- /dev/null
+++ b/bitmap2component/bitmap2cmp_control.h
@@ -0,0 +1,64 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2023 KiCad Developers, see AUTHORS.txt for contributors.
+ *
+ * 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, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+
+#ifndef BITMAP2CMP_CONTROL_H
+#define BITMAP2CMP_CONTROL_H
+
+#include <tool/tool_interactive.h>
+
+class BITMAP2CMP_FRAME;
+
+
+/**
+ * Handle actions for the various symbol editor and viewers.
+ */
+class BITMAP2CMP_CONTROL : public wxEvtHandler, public TOOL_INTERACTIVE
+{
+public:
+    BITMAP2CMP_CONTROL() :
+            TOOL_INTERACTIVE( "bitmap2cmp.Control" ),
+            m_frame( nullptr )
+    { }
+
+    virtual ~BITMAP2CMP_CONTROL() { }
+
+    /// @copydoc TOOL_INTERACTIVE::Init()
+    bool Init() override;
+
+    /// @copydoc TOOL_INTERACTIVE::Reset()
+    void Reset( RESET_REASON aReason ) override;
+
+    int Open( const TOOL_EVENT& aEvent );
+    int Close( const TOOL_EVENT& aEvent );
+
+private:
+    ///< Set up handlers for various events.
+    void setTransitions() override;
+
+private:
+    BITMAP2CMP_FRAME* m_frame;
+};
+
+
+#endif // BITMAP2CMP_CONTROL_H
diff --git a/bitmap2component/bitmap2cmp_frame.cpp b/bitmap2component/bitmap2cmp_frame.cpp
index 55eee9c618..2be13cf7d9 100644
--- a/bitmap2component/bitmap2cmp_frame.cpp
+++ b/bitmap2component/bitmap2cmp_frame.cpp
@@ -31,7 +31,14 @@
 #include <common.h>
 #include <kiface_base.h>
 #include <pgm_base.h>
+#include <widgets/wx_menubar.h>
+#include <menus_helpers.h>
 #include <wildcards_and_files_ext.h>
+#include <tool/tool_manager.h>
+#include <tool/tool_dispatcher.h>
+#include <tool/common_control.h>
+#include <bitmap2cmp_control.h>
+#include <tool/actions.h>
 
 #include <wx/filedlg.h>
 #include <wx/msgdlg.h>
@@ -130,18 +137,14 @@ void IMAGE_SIZE::SetUnit( EDA_UNITS aUnit )
 }
 
 
-BEGIN_EVENT_TABLE( BITMAP2CMP_FRAME, KIWAY_PLAYER )
-    EVT_MENU( wxID_EXIT, BITMAP2CMP_FRAME::OnExit )
-    EVT_MENU( wxID_OPEN, BITMAP2CMP_FRAME::OnLoadFile )
-END_EVENT_TABLE()
-
-
 BITMAP2CMP_FRAME::BITMAP2CMP_FRAME( KIWAY* aKiway, wxWindow* aParent ) :
         KIWAY_PLAYER( aKiway, aParent, FRAME_BM2CMP, _( "Image Converter" ), wxDefaultPosition,
                       wxDefaultSize, wxDEFAULT_FRAME_STYLE, wxT( "bitmap2cmp" ), unityScale ),
         m_panel( nullptr ),
         m_statusBar( nullptr )
 {
+    m_aboutTitle = _HKI( "KiCad Image Converter" );
+
     // Give an icon
     wxIcon icon;
     wxIconBundle icon_bundle;
@@ -163,10 +166,20 @@ BITMAP2CMP_FRAME::BITMAP2CMP_FRAME( KIWAY* aKiway, wxWindow* aParent ) :
 
     m_statusBar = this->CreateStatusBar( 1, wxSTB_SIZEGRIP, wxID_ANY );
 
-    ReCreateMenuBar();
-
     LoadSettings( config() );
 
+    m_toolManager = new TOOL_MANAGER;
+    m_toolManager->SetEnvironment( nullptr, nullptr, nullptr, config(), this );
+
+    m_toolDispatcher = new TOOL_DISPATCHER( m_toolManager );
+
+    // Register tools
+    m_toolManager->RegisterTool( new COMMON_CONTROL );
+    m_toolManager->RegisterTool( new BITMAP2CMP_CONTROL );
+    m_toolManager->InitTools();
+
+    ReCreateMenuBar();
+
     GetSizer()->SetSizeHints( this );
 
     SetSize( m_framePos.x, m_framePos.y, m_frameSize.x, m_frameSize.y );
@@ -195,41 +208,95 @@ wxWindow* BITMAP2CMP_FRAME::GetToolCanvas() const
 
 void BITMAP2CMP_FRAME::doReCreateMenuBar()
 {
-    // wxWidgets handles the Mac Application menu behind the scenes, but that means
+    COMMON_CONTROL* tool = m_toolManager->GetTool<COMMON_CONTROL>();
+    EDA_BASE_FRAME* base_frame = dynamic_cast<EDA_BASE_FRAME*>( this );
+
+    // base_frame == nullptr should not happen, but it makes Coverity happy
+    wxCHECK( base_frame, /* void */ );
+
+    // wxWidgets handles the OSX Application menu behind the scenes, but that means
     // we always have to start from scratch with a new wxMenuBar.
-    wxMenuBar* oldMenuBar = GetMenuBar();
-    wxMenuBar* menuBar    = new wxMenuBar();
+    wxMenuBar*  oldMenuBar = base_frame->GetMenuBar();
+    WX_MENUBAR* menuBar    = new WX_MENUBAR();
 
-    wxMenu* fileMenu = new wxMenu;
+    //-- File menu -----------------------------------------------------------
+    //
+    ACTION_MENU* fileMenu = new ACTION_MENU( false, tool );
 
-    wxMenuItem* item = new wxMenuItem( fileMenu, wxID_OPEN, _( "Open..." ) + wxT( "\tCtrl+O" ),
-                                       _( "Load source image" ) );
+    fileMenu->Add( ACTIONS::open );
 
-    fileMenu->Append( item );
-
-#ifndef __WXMAC__
-    // Mac moves Quit to the App menu so we don't need a separator on Mac
     fileMenu->AppendSeparator();
-#endif
+    fileMenu->AddQuit( _( "Image Converter" ) );
 
-    item = new wxMenuItem( fileMenu, wxID_EXIT, _( "Quit" ) + wxT( "\tCtrl+Q" ),
-                           _( "Quit Image Converter" ) );
+    //-- Preferences menu -----------------------------------------------
+    //
+    ACTION_MENU* prefsMenu = new ACTION_MENU( false, tool );
 
-    if( Pgm().GetCommonSettings()->m_Appearance.use_icons_in_menus )
-        item->SetBitmap( KiBitmap( BITMAPS::exit ) );
+    // We can't use ACTIONS::showPreferences yet because wxWidgets moves this on
+    // Mac, and it needs the wxID_PREFERENCES id to find it.
+    prefsMenu->Add( _( "Preferences..." ) + "\tCtrl+,",
+                    _( "Show preferences for all open tools" ),
+                    wxID_PREFERENCES,
+                    BITMAPS::preference );
 
-    fileMenu->Append( item );
+    prefsMenu->AppendSeparator();
+    AddMenuLanguageList( prefsMenu, tool );
 
+
+    //-- Menubar -------------------------------------------------------------
+    //
     menuBar->Append( fileMenu, _( "&File" ) );
+    menuBar->Append( prefsMenu, _( "&Preferences" ) );
+    base_frame->AddStandardHelpMenu( menuBar );
 
-    SetMenuBar( menuBar );
+    base_frame->SetMenuBar( menuBar );
     delete oldMenuBar;
 }
 
 
-void BITMAP2CMP_FRAME::OnExit( wxCommandEvent& event )
+void BITMAP2CMP_FRAME::ShowChangedLanguage()
 {
-    Destroy();
+    EDA_BASE_FRAME::ShowChangedLanguage();
+
+    UpdateTitle();
+
+    SaveSettings( config() );
+    IMAGE_SIZE imageSizeX = m_panel->GetOutputSizeX();
+    IMAGE_SIZE imageSizeY = m_panel->GetOutputSizeY();
+    Freeze();
+
+    wxSizer* mainSizer = m_panel->GetContainingSizer();
+    mainSizer->Detach( m_panel );
+    m_panel->Destroy();
+
+    m_panel = new BITMAP2CMP_PANEL( this );
+    mainSizer->Add( m_panel, 1, wxEXPAND, 5 );
+    Layout();
+
+    if( !m_bitmapFileName.IsEmpty() )
+        OpenProjectFiles( std::vector<wxString>( 1, m_bitmapFileName ) );
+
+    LoadSettings( config() );
+    m_panel->SetOutputSize( imageSizeX, imageSizeY );
+
+    Thaw();
+    Refresh();
+}
+
+
+void BITMAP2CMP_FRAME::UpdateTitle()
+{
+    wxString title;
+
+    if( !m_bitmapFileName.IsEmpty() )
+    {
+        wxFileName filename( m_bitmapFileName );
+        title = filename.GetFullName() + wxT( " \u2014 " );
+    }
+
+    title += _( "Image Converter" );
+
+    SetTitle( title );
 }
 
 
@@ -263,7 +330,7 @@ void BITMAP2CMP_FRAME::SaveSettings( APP_SETTINGS_BASE* aCfg )
 }
 
 
-void BITMAP2CMP_FRAME::OnLoadFile( wxCommandEvent& event )
+void BITMAP2CMP_FRAME::OnLoadFile()
 {
     wxFileName  fn( m_bitmapFileName );
     wxString    path = fn.GetPath();
@@ -288,6 +355,7 @@ void BITMAP2CMP_FRAME::OnLoadFile( wxCommandEvent& event )
     fn = fullFilename;
     m_mruPath = fn.GetPath();
     SetStatusText( fullFilename );
+    UpdateTitle();
     Refresh();
 }
 
diff --git a/bitmap2component/bitmap2cmp_frame.h b/bitmap2component/bitmap2cmp_frame.h
index 737b821246..d639e17590 100644
--- a/bitmap2component/bitmap2cmp_frame.h
+++ b/bitmap2component/bitmap2cmp_frame.h
@@ -31,59 +31,6 @@
 
 class BITMAP2CMP_PANEL;
 
-class IMAGE_SIZE
-{
-public:
-    IMAGE_SIZE();
-
-    // Set the unit used for m_outputSize, and convert the old m_outputSize value
-    // to the value in new unit
-    void SetUnit( EDA_UNITS aUnit );
-
-    // Accessors:
-    void SetOriginalDPI( int aDPI )
-    {
-        m_originalDPI = aDPI;
-    }
-
-    void SetOriginalSizePixels( int aPixels )
-    {
-        m_originalSizePixels = aPixels;
-    }
-
-    double GetOutputSize()
-    {
-        return m_outputSize;
-    }
-
-    void SetOutputSize( double aSize, EDA_UNITS aUnit )
-    {
-        m_unit = aUnit;
-        m_outputSize = aSize;
-    }
-
-    int  GetOriginalSizePixels()
-    {
-        return m_originalSizePixels;
-    }
-
-    // Set the m_outputSize value from the m_originalSizePixels and the selected unit
-    void SetOutputSizeFromInitialImageSize();
-
-    /** @return the pixels per inch value to build the output image.
-     * It is used by potrace to build the polygonal image
-     */
-    int GetOutputDPI();
-
-private:
-    EDA_UNITS m_unit;                 // The units for m_outputSize (mm, inch, dpi)
-    double    m_outputSize;           // The size in m_unit of the output image, depending on
-                                      // the user settings. Set to the initial image size
-    int       m_originalDPI;          // The image DPI if specified in file, or 0 if unknown
-    int       m_originalSizePixels;   // The original image size read from file, in pixels
-};
-
-
 class BITMAP2CMP_FRAME : public KIWAY_PLAYER
 {
 public:
@@ -94,7 +41,7 @@ public:
     bool OpenProjectFiles( const std::vector<wxString>& aFilenames, int aCtl = 0 ) override;
 
     void OnExit( wxCommandEvent& event );
-    void OnLoadFile( wxCommandEvent& event );
+    void OnLoadFile();
 
     /**
      * Generate a schematic library which contains one component:
@@ -117,13 +64,14 @@ public:
      */
     void ExportLogo();
 
+    void UpdateTitle();
+    void ShowChangedLanguage() override;
+
     void LoadSettings( APP_SETTINGS_BASE* aCfg ) override;
     void SaveSettings( APP_SETTINGS_BASE* aCfg ) override;
 
     wxWindow* GetToolCanvas() const override;
 
-DECLARE_EVENT_TABLE()
-
 protected:
     void doReCreateMenuBar() override;
 
@@ -134,4 +82,5 @@ private:
     wxString          m_bitmapFileName;
     wxString          m_convertedFileName;
 };
+
 #endif// BITMOP2CMP_GUI_H_
diff --git a/bitmap2component/bitmap2cmp_panel.cpp b/bitmap2component/bitmap2cmp_panel.cpp
index c53f4ec46c..7f19a78d52 100644
--- a/bitmap2component/bitmap2cmp_panel.cpp
+++ b/bitmap2component/bitmap2cmp_panel.cpp
@@ -44,7 +44,8 @@
 
 BITMAP2CMP_PANEL::BITMAP2CMP_PANEL( BITMAP2CMP_FRAME* aParent ) :
         BITMAP2CMP_PANEL_BASE( aParent ),
-        m_parentFrame( aParent )
+        m_parentFrame( aParent ), m_negative( false ),
+        m_aspectRatio( 1.0 )
 {
     for( wxString unit : { _( "mm" ), _( "Inch" ), _( "DPI" ) } )
         m_PixelUnit->Append( unit );
@@ -84,10 +85,10 @@ void BITMAP2CMP_PANEL::LoadSettings( BITMAP2CMP_SETTINGS* cfg )
 
     m_sliderThreshold->SetValue( cfg->m_Threshold );
 
-    m_Negative = cfg->m_Negative;
+    m_negative = cfg->m_Negative;
     m_checkNegative->SetValue( cfg->m_Negative );
 
-    m_AspectRatio = 1.0;
+    m_aspectRatio = 1.0;
     m_aspectRatioCheckbox->SetValue( true );
 
     int format = cfg->m_LastFormat;
@@ -177,7 +178,7 @@ void BITMAP2CMP_PANEL::OnPaintBW( wxPaintEvent& event )
 
 void BITMAP2CMP_PANEL::OnLoadFile( wxCommandEvent& event )
 {
-    m_parentFrame->OnLoadFile( event );
+    m_parentFrame->OnLoadFile();
 }
 
 
@@ -217,7 +218,7 @@ bool BITMAP2CMP_PANEL::OpenProjectFiles( const std::vector<wxString>& aFileSet,
 
     int h  = m_Pict_Bitmap.GetHeight();
     int w  = m_Pict_Bitmap.GetWidth();
-    m_AspectRatio = (double) w / h;
+    m_aspectRatio = (double) w / h;
 
     m_outputSizeX.SetOriginalDPI( imageDPIx );
     m_outputSizeX.SetOriginalSizePixels( w );
@@ -253,7 +254,7 @@ bool BITMAP2CMP_PANEL::OpenProjectFiles( const std::vector<wxString>& aFileSet,
         }
     }
 
-    if( m_Negative )
+    if( m_negative )
         NegateGreyscaleImage( );
 
     m_Greyscale_Bitmap = wxBitmap( m_Greyscale_Image );
@@ -326,7 +327,7 @@ void BITMAP2CMP_PANEL::OnSizeChangeX( wxCommandEvent& event )
     {
         if( m_aspectRatioCheckbox->GetValue() )
         {
-            double calculatedY = new_size / m_AspectRatio;
+            double calculatedY = new_size / m_aspectRatio;
 
             if( getUnitFromSelection() == EDA_UNITS::UNSCALED )
             {
@@ -355,7 +356,7 @@ void BITMAP2CMP_PANEL::OnSizeChangeY( wxCommandEvent& event )
     {
         if( m_aspectRatioCheckbox->GetValue() )
         {
-            double calculatedX = new_size * m_AspectRatio;
+            double calculatedX = new_size * m_aspectRatio;
 
             if( getUnitFromSelection() == EDA_UNITS::UNSCALED )
             {
@@ -387,6 +388,17 @@ void BITMAP2CMP_PANEL::OnSizeUnitChange( wxCommandEvent& event )
 }
 
 
+void BITMAP2CMP_PANEL::SetOutputSize( const IMAGE_SIZE& aSizeX, const IMAGE_SIZE& aSizeY )
+{
+    m_outputSizeX = aSizeX;
+    m_outputSizeY = aSizeY;
+    updateImageInfo();
+
+    m_UnitSizeX->ChangeValue( FormatOutputSize( m_outputSizeX.GetOutputSize() ) );
+    m_UnitSizeY->ChangeValue( FormatOutputSize( m_outputSizeY.GetOutputSize() ) );
+}
+
+
 void BITMAP2CMP_PANEL::ToggleAspectRatioLock( wxCommandEvent& event )
 {
     if( m_aspectRatioCheckbox->GetValue() )
@@ -447,13 +459,13 @@ void BITMAP2CMP_PANEL::NegateGreyscaleImage( )
 
 void BITMAP2CMP_PANEL::OnNegativeClicked( wxCommandEvent&  )
 {
-    if( m_checkNegative->GetValue() != m_Negative )
+    if( m_checkNegative->GetValue() != m_negative )
     {
         NegateGreyscaleImage();
 
         m_Greyscale_Bitmap = wxBitmap( m_Greyscale_Image );
         Binarize( (double)m_sliderThreshold->GetValue()/m_sliderThreshold->GetMax() );
-        m_Negative = m_checkNegative->GetValue();
+        m_negative = m_checkNegative->GetValue();
 
         Refresh();
     }
diff --git a/bitmap2component/bitmap2cmp_panel.h b/bitmap2component/bitmap2cmp_panel.h
index 1147705bae..1229fccf67 100644
--- a/bitmap2component/bitmap2cmp_panel.h
+++ b/bitmap2component/bitmap2cmp_panel.h
@@ -31,6 +31,59 @@ class BITMAP2CMP_FRAME;
 class BITMAP2CMP_SETTINGS;
 
 
+class IMAGE_SIZE
+{
+public:
+    IMAGE_SIZE();
+
+    // Set the unit used for m_outputSize, and convert the old m_outputSize value
+    // to the value in new unit
+    void SetUnit( EDA_UNITS aUnit );
+
+    // Accessors:
+    void SetOriginalDPI( int aDPI )
+    {
+        m_originalDPI = aDPI;
+    }
+
+    void SetOriginalSizePixels( int aPixels )
+    {
+        m_originalSizePixels = aPixels;
+    }
+
+    double GetOutputSize()
+    {
+        return m_outputSize;
+    }
+
+    void SetOutputSize( double aSize, EDA_UNITS aUnit )
+    {
+        m_unit = aUnit;
+        m_outputSize = aSize;
+    }
+
+    int  GetOriginalSizePixels()
+    {
+        return m_originalSizePixels;
+    }
+
+    // Set the m_outputSize value from the m_originalSizePixels and the selected unit
+    void SetOutputSizeFromInitialImageSize();
+
+    /** @return the pixels per inch value to build the output image.
+     * It is used by potrace to build the polygonal image
+     */
+    int GetOutputDPI();
+
+private:
+    EDA_UNITS m_unit;                 // The units for m_outputSize (mm, inch, dpi)
+    double    m_outputSize;           // The size in m_unit of the output image, depending on
+                                      // the user settings. Set to the initial image size
+    int       m_originalDPI;          // The image DPI if specified in file, or 0 if unknown
+    int       m_originalSizePixels;   // The original image size read from file, in pixels
+};
+
+
 class BITMAP2CMP_PANEL : public BITMAP2CMP_PANEL_BASE
 {
 public:
@@ -46,6 +99,10 @@ public:
 
     wxWindow* GetCurrentPage();
 
+    IMAGE_SIZE GetOutputSizeX() const { return m_outputSizeX; }
+    IMAGE_SIZE GetOutputSizeY() const { return m_outputSizeY; }
+    void SetOutputSize( const IMAGE_SIZE& aSizeX, const IMAGE_SIZE& aSizeY );
+
     /**
      * generate a export data of the current bitmap.
      * @param aOutput is a string buffer to fill with data
@@ -86,15 +143,15 @@ private:
 private:
     BITMAP2CMP_FRAME* m_parentFrame;
 
-    wxImage    m_Pict_Image;
-    wxBitmap   m_Pict_Bitmap;
-    wxImage    m_Greyscale_Image;
-    wxBitmap   m_Greyscale_Bitmap;
-    wxImage    m_NB_Image;
-    wxBitmap   m_BN_Bitmap;
-    IMAGE_SIZE m_outputSizeX;
-    IMAGE_SIZE m_outputSizeY;
-    bool       m_Negative;
-    double     m_AspectRatio;
+    wxImage           m_Pict_Image;
+    wxBitmap          m_Pict_Bitmap;
+    wxImage           m_Greyscale_Image;
+    wxBitmap          m_Greyscale_Bitmap;
+    wxImage           m_NB_Image;
+    wxBitmap          m_BN_Bitmap;
+    IMAGE_SIZE        m_outputSizeX;
+    IMAGE_SIZE        m_outputSizeY;
+    bool              m_negative;
+    double            m_aspectRatio;
 };
 #endif// BITMAP2CMP_PANEL
diff --git a/common/tool/action_menu.cpp b/common/tool/action_menu.cpp
index 6db881cd85..bfdf2344ce 100644
--- a/common/tool/action_menu.cpp
+++ b/common/tool/action_menu.cpp
@@ -235,6 +235,17 @@ void ACTION_MENU::AddQuitOrClose( KIFACE_BASE* aKiface, wxString aAppname )
 }
 
 
+void ACTION_MENU::AddQuit( const wxString& aAppname )
+{
+    // Don't use ACTIONS::quit; wxWidgets moves this on OSX and expects to find it via
+    // wxID_EXIT
+    Add( _( "Quit" ) + wxS( "\tCtrl+Q" ),
+         wxString::Format( _( "Quit %s" ), aAppname ),
+         wxID_EXIT,
+         BITMAPS::exit );
+}
+
+
 void ACTION_MENU::Clear()
 {
     m_titleDisplayed = false;
diff --git a/eeschema/tools/simulator_control.h b/eeschema/tools/simulator_control.h
index 3da13acb1a..9978229640 100644
--- a/eeschema/tools/simulator_control.h
+++ b/eeschema/tools/simulator_control.h
@@ -39,7 +39,8 @@ class SIMULATOR_CONTROL : public wxEvtHandler, public TOOL_INTERACTIVE
 {
 public:
     SIMULATOR_CONTROL() :
-            TOOL_INTERACTIVE( "eeschema.SimulatorControl" ), m_simulatorFrame( nullptr ),
+            TOOL_INTERACTIVE( "eeschema.SimulatorControl" ),
+            m_simulatorFrame( nullptr ),
             m_schematicFrame( nullptr )
     { }
 
diff --git a/include/tool/action_menu.h b/include/tool/action_menu.h
index f3028818b2..95221eb796 100644
--- a/include/tool/action_menu.h
+++ b/include/tool/action_menu.h
@@ -121,13 +121,22 @@ public:
     /**
      * Add either a standard Quit or Close item to the menu.
      *
-     * If \a aKiface is NULL or in single-instance then quit (wxID_QUIT) is used, otherwise
+     * If \a aKiface is NULL or in single-instance then quit (wxID_EXIT) is used, otherwise
      * close (wxID_CLOSE) is used.
      *
      * @param aAppname is the application name to append to the tooltip.
      */
     void AddQuitOrClose( KIFACE_BASE* aKiface, wxString aAppname = "" );
 
+    /**
+     * Add a standard Quit item to the menu.
+     *
+     * Emits the wxID_EXIT event.
+     *
+     * @param aAppname is the application name to append to the tooltip.
+     */
+    void AddQuit( const wxString& aAppname = "" );
+
     /**
      * Remove all the entries from the menu (as well as its title).
      *
diff --git a/pcb_calculator/CMakeLists.txt b/pcb_calculator/CMakeLists.txt
index 689f4333d3..1e91f8c8cc 100644
--- a/pcb_calculator/CMakeLists.txt
+++ b/pcb_calculator/CMakeLists.txt
@@ -10,19 +10,20 @@ include_directories(
 set( PCB_CALCULATOR_SRCS
     common_data.cpp
     params_read_write.cpp
+    pcb_calculator_control.cpp
     pcb_calculator_frame.cpp
     pcb_calculator_settings.cpp
     datafile_read_write.cpp
-        calculator_panels/panel_rf_attenuators.cpp
-        calculator_panels/panel_rf_attenuators_base.cpp
+    calculator_panels/panel_rf_attenuators.cpp
+    calculator_panels/panel_rf_attenuators_base.cpp
     calculator_panels/panel_board_class.cpp
     calculator_panels/panel_board_class_base.cpp
     calculator_panels/panel_cable_size.cpp
     calculator_panels/panel_cable_size_base.cpp
     calculator_panels/panel_color_code.cpp
     calculator_panels/panel_color_code_base.cpp
-        calculator_panels/panel_galvanic_corrosion.cpp
-        calculator_panels/panel_galvanic_corrosion_base.cpp
+    calculator_panels/panel_galvanic_corrosion.cpp
+    calculator_panels/panel_galvanic_corrosion_base.cpp
     calculator_panels/panel_electrical_spacing.cpp
     calculator_panels/panel_electrical_spacing_base.cpp
     calculator_panels/iec60664.cpp
diff --git a/pcb_calculator/pcb_calculator_control.cpp b/pcb_calculator/pcb_calculator_control.cpp
new file mode 100644
index 0000000000..01ab1f7d79
--- /dev/null
+++ b/pcb_calculator/pcb_calculator_control.cpp
@@ -0,0 +1,54 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2023 KiCad Developers, see AUTHORS.txt for contributors.
+ *
+ * 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, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+#include <kiway.h>
+#include <tool/tool_manager.h>
+#include <tool/actions.h>
+#include <pcb_calculator_frame.h>
+#include <pcb_calculator_control.h>
+
+
+bool PCB_CALCULATOR_CONTROL::Init()
+{
+    Reset( MODEL_RELOAD );
+    return true;
+}
+
+
+void PCB_CALCULATOR_CONTROL::Reset( RESET_REASON aReason )
+{
+    m_frame = getEditFrame<PCB_CALCULATOR_FRAME>();
+}
+
+
+int PCB_CALCULATOR_CONTROL::Close( const TOOL_EVENT& aEvent )
+{
+    m_frame->Close();
+    return 0;
+}
+
+
+void PCB_CALCULATOR_CONTROL::setTransitions()
+{
+    Go( &PCB_CALCULATOR_CONTROL::Close,                  ACTIONS::quit.MakeEvent() );
+}
diff --git a/pcb_calculator/pcb_calculator_control.h b/pcb_calculator/pcb_calculator_control.h
new file mode 100644
index 0000000000..3473620a0f
--- /dev/null
+++ b/pcb_calculator/pcb_calculator_control.h
@@ -0,0 +1,63 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2023 KiCad Developers, see AUTHORS.txt for contributors.
+ *
+ * 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, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+
+#ifndef PCB_CALCULATOR_CONTROL_H
+#define PCB_CALCULATOR_CONTROL_H
+
+#include "tool/tool_interactive.h"
+
+class PCB_CALCULATOR_FRAME;
+
+
+/**
+ * Handle actions for the various symbol editor and viewers.
+ */
+class PCB_CALCULATOR_CONTROL : public wxEvtHandler, public TOOL_INTERACTIVE
+{
+public:
+    PCB_CALCULATOR_CONTROL() :
+            TOOL_INTERACTIVE( "pcb_calculator.Control" ),
+            m_frame( nullptr )
+    { }
+
+    virtual ~PCB_CALCULATOR_CONTROL() { }
+
+    /// @copydoc TOOL_INTERACTIVE::Init()
+    bool Init() override;
+
+    /// @copydoc TOOL_INTERACTIVE::Reset()
+    void Reset( RESET_REASON aReason ) override;
+
+    int Close( const TOOL_EVENT& aEvent );
+
+private:
+    ///< Set up handlers for various events.
+    void setTransitions() override;
+
+private:
+    PCB_CALCULATOR_FRAME* m_frame;
+};
+
+
+#endif // PCB_CALCULATOR_CONTROL_H
diff --git a/pcb_calculator/pcb_calculator_frame.cpp b/pcb_calculator/pcb_calculator_frame.cpp
index 48cbc1fa8f..a852a753c8 100644
--- a/pcb_calculator/pcb_calculator_frame.cpp
+++ b/pcb_calculator/pcb_calculator_frame.cpp
@@ -27,9 +27,14 @@
 #include <bitmap_store.h>
 #include <geometry/shape_poly_set.h>
 #include <kiface_base.h>
+#include <menus_helpers.h>
+#include <tool/tool_manager.h>
+#include <tool/tool_dispatcher.h>
+#include <tool/common_control.h>
 #include <attenuators/attenuator_classes.h>
 #include <pcb_calculator_frame.h>
 #include <pcb_calculator_settings.h>
+#include <pcb_calculator_control.h>
 
 #include <calculator_panels/panel_rf_attenuators.h>
 #include <calculator_panels/panel_board_class.h>
@@ -44,6 +49,7 @@
 #include <calculator_panels/panel_transline.h>
 #include <calculator_panels/panel_via_size.h>
 #include <calculator_panels/panel_wavelength.h>
+#include "widgets/wx_menubar.h"
 
 
 PCB_CALCULATOR_FRAME::PCB_CALCULATOR_FRAME( KIWAY* aKiway, wxWindow* aParent ) :
@@ -53,6 +59,8 @@ PCB_CALCULATOR_FRAME::PCB_CALCULATOR_FRAME( KIWAY* aKiway, wxWindow* aParent ) :
                   wxT( "calculator_tools" ), unityScale ),
     m_lastNotebookPage( -1 )
 {
+    m_aboutTitle = _HKI( "KiCad Calculator Tools" );
+
     SHAPE_POLY_SET dummy;   // A ugly trick to force the linker to include
                             // some methods in code and avoid link errors
 
@@ -68,35 +76,7 @@ PCB_CALCULATOR_FRAME::PCB_CALCULATOR_FRAME( KIWAY* aKiway, wxWindow* aParent ) :
     Layout();
     Centre( wxBOTH );
 
-    m_treebook->AddPage( nullptr, _( "General system design" ) );
-
-    AddCalculator( new PANEL_REGULATOR( m_treebook ), _( "Regulators" ) );
-
-    m_treebook->AddPage( nullptr, _( "Power, current and isolation" ) );
-
-    AddCalculator( new PANEL_ELECTRICAL_SPACING( m_treebook ), _( "Electrical Spacing" ) );
-    AddCalculator( new PANEL_VIA_SIZE( m_treebook ), _( "Via Size" ) );
-    AddCalculator( new PANEL_TRACK_WIDTH( m_treebook ), _( "Track Width" ) );
-    AddCalculator( new PANEL_FUSING_CURRENT( m_treebook ), _( "Fusing Current" ) );
-    AddCalculator( new PANEL_CABLE_SIZE( m_treebook ), _( "Cable Size" ) );
-
-    m_treebook->AddPage( nullptr, _( "High speed" ) );
-
-    AddCalculator( new PANEL_WAVELENGTH( m_treebook ), _( "Wavelength" ) );
-    AddCalculator( new PANEL_RF_ATTENUATORS( m_treebook ), _( "RF Attenuators" ) );
-    AddCalculator( new PANEL_TRANSLINE( m_treebook ), _( "Transmission Lines") );
-
-    m_treebook->AddPage( nullptr, _( "Memo" ) );
-
-    AddCalculator( new PANEL_E_SERIES( m_treebook ), _( "E-Series" ) );
-    AddCalculator( new PANEL_COLOR_CODE( m_treebook ), _( "Color Code" ) );
-    AddCalculator( new PANEL_BOARD_CLASS( m_treebook ), _("Board Classes") );
-    AddCalculator( new PANEL_GALVANIC_CORROSION( m_treebook ), _( "Galvanic Corrosion" ) );
-
-    LoadSettings( config() );
-
-    if( PANEL_REGULATOR* regPanel = GetCalculator<PANEL_REGULATOR>() )
-        regPanel->ReadDataFile();
+    loadPages();
 
     // Give an icon
     wxIcon icon;
@@ -111,6 +91,18 @@ PCB_CALCULATOR_FRAME::PCB_CALCULATOR_FRAME( KIWAY* aKiway, wxWindow* aParent ) :
 
     SetIcons( icon_bundle );
 
+    m_toolManager = new TOOL_MANAGER;
+    m_toolManager->SetEnvironment( nullptr, nullptr, nullptr, config(), this );
+
+    m_toolDispatcher = new TOOL_DISPATCHER( m_toolManager );
+
+    // Register tools
+    m_toolManager->RegisterTool( new COMMON_CONTROL );
+    m_toolManager->RegisterTool( new PCB_CALCULATOR_CONTROL );
+    m_toolManager->InitTools();
+
+    ReCreateMenuBar();
+
     GetSizer()->SetSizeHints( this );
 
     // Set previous size and position
@@ -144,6 +136,110 @@ PCB_CALCULATOR_FRAME::~PCB_CALCULATOR_FRAME()
     this->Freeze();
 }
 
+
+void PCB_CALCULATOR_FRAME::loadPages()
+{
+    m_treebook->AddPage( nullptr, _( "General system design" ) );
+
+    AddCalculator( new PANEL_REGULATOR( m_treebook ), _( "Regulators" ) );
+
+    m_treebook->AddPage( nullptr, _( "Power, current and isolation" ) );
+
+    AddCalculator( new PANEL_ELECTRICAL_SPACING( m_treebook ), _( "Electrical Spacing" ) );
+    AddCalculator( new PANEL_VIA_SIZE( m_treebook ), _( "Via Size" ) );
+    AddCalculator( new PANEL_TRACK_WIDTH( m_treebook ), _( "Track Width" ) );
+    AddCalculator( new PANEL_FUSING_CURRENT( m_treebook ), _( "Fusing Current" ) );
+    AddCalculator( new PANEL_CABLE_SIZE( m_treebook ), _( "Cable Size" ) );
+
+    m_treebook->AddPage( nullptr, _( "High Speed" ) );
+
+    AddCalculator( new PANEL_WAVELENGTH( m_treebook ), _( "Wavelength" ) );
+    AddCalculator( new PANEL_RF_ATTENUATORS( m_treebook ), _( "RF Attenuators" ) );
+    AddCalculator( new PANEL_TRANSLINE( m_treebook ), _( "Transmission Lines") );
+
+    m_treebook->AddPage( nullptr, _( "Memo" ) );
+
+    AddCalculator( new PANEL_E_SERIES( m_treebook ), _( "E-Series" ) );
+    AddCalculator( new PANEL_COLOR_CODE( m_treebook ), _( "Color Code" ) );
+    AddCalculator( new PANEL_BOARD_CLASS( m_treebook ), _( "Board Classes" ) );
+    AddCalculator( new PANEL_GALVANIC_CORROSION( m_treebook ), _( "Galvanic Corrosion" ) );
+
+    LoadSettings( config() );
+
+    if( PANEL_REGULATOR* regPanel = GetCalculator<PANEL_REGULATOR>() )
+        regPanel->ReadDataFile();
+}
+
+
+void PCB_CALCULATOR_FRAME::doReCreateMenuBar()
+{
+    COMMON_CONTROL* tool = m_toolManager->GetTool<COMMON_CONTROL>();
+    EDA_BASE_FRAME* base_frame = dynamic_cast<EDA_BASE_FRAME*>( this );
+
+    // base_frame == nullptr should not happen, but it makes Coverity happy
+    wxCHECK( base_frame, /* void */ );
+
+    // wxWidgets handles the OSX Application menu behind the scenes, but that means
+    // we always have to start from scratch with a new wxMenuBar.
+    wxMenuBar*  oldMenuBar = base_frame->GetMenuBar();
+    WX_MENUBAR* menuBar    = new WX_MENUBAR();
+
+    //-- File menu -----------------------------------------------------------
+    //
+    ACTION_MENU* fileMenu = new ACTION_MENU( false, tool );
+
+    fileMenu->AddQuit( _( "Calculator Tools" ) );
+
+    //-- Preferences menu -----------------------------------------------
+    //
+    ACTION_MENU* prefsMenu = new ACTION_MENU( false, tool );
+
+    // We can't use ACTIONS::showPreferences yet because wxWidgets moves this on
+    // Mac, and it needs the wxID_PREFERENCES id to find it.
+    prefsMenu->Add( _( "Preferences..." ) + "\tCtrl+,",
+                    _( "Show preferences for all open tools" ),
+                    wxID_PREFERENCES,
+                    BITMAPS::preference );
+
+    prefsMenu->AppendSeparator();
+    AddMenuLanguageList( prefsMenu, tool );
+
+
+    //-- Menubar -------------------------------------------------------------
+    //
+    menuBar->Append( fileMenu, _( "&File" ) );
+    menuBar->Append( prefsMenu, _( "&Preferences" ) );
+    base_frame->AddStandardHelpMenu( menuBar );
+
+    base_frame->SetMenuBar( menuBar );
+    delete oldMenuBar;
+}
+
+
+void PCB_CALCULATOR_FRAME::ShowChangedLanguage()
+{
+    EDA_BASE_FRAME::ShowChangedLanguage();
+
+    SetTitle( _( "Calculator Tools" ) );
+
+    SaveSettings( config() );
+    Freeze();
+
+    int page = m_treebook->GetSelection();
+    m_treebook->DeleteAllPages();
+    m_panels.clear();
+
+    loadPages();
+    Layout();
+
+    m_treebook->SetSelection( page );
+    LoadSettings( config() );
+
+    Thaw();
+    Refresh();
+}
+
+
 void PCB_CALCULATOR_FRAME::OnPageChanged ( wxTreebookEvent& aEvent )
 {
     int page = aEvent.GetSelection();
@@ -152,11 +248,14 @@ void PCB_CALCULATOR_FRAME::OnPageChanged ( wxTreebookEvent& aEvent )
     if ( m_treebook->GetPageParent( page ) == wxNOT_FOUND )
     {
         m_treebook->ExpandNode( page );
+
         // Select the first child
-        m_treebook->ChangeSelection( page + 1 );
+        if( page + 1 < m_treebook->GetPageCount() )
+            m_treebook->ChangeSelection( page + 1 );
     }
 }
 
+
 void PCB_CALCULATOR_FRAME::AddCalculator( CALCULATOR_PANEL *aPanel, const wxString& panelUIName )
 {
     // Update internal structures
diff --git a/pcb_calculator/pcb_calculator_frame.h b/pcb_calculator/pcb_calculator_frame.h
index f81571fd78..b0c0880c73 100644
--- a/pcb_calculator/pcb_calculator_frame.h
+++ b/pcb_calculator/pcb_calculator_frame.h
@@ -63,6 +63,15 @@ public:
 
     void AddCalculator( CALCULATOR_PANEL *aPanel, const wxString& panelUIName );
 
+    void ShowChangedLanguage() override;
+
+    // Config read-write, virtual from EDA_BASE_FRAME
+    void LoadSettings( APP_SETTINGS_BASE* aCfg ) override;
+    void SaveSettings( APP_SETTINGS_BASE* aCfg ) override;
+
+protected:
+    void doReCreateMenuBar() override;
+
 private:
     // Event handlers
     void OnClosePcbCalc( wxCloseEvent& event );
@@ -71,9 +80,7 @@ private:
 
     void onThemeChanged( wxSysColourChangedEvent& aEvent );
 
-    // Config read-write, virtual from EDA_BASE_FRAME
-    void LoadSettings( APP_SETTINGS_BASE* aCfg ) override;
-    void SaveSettings( APP_SETTINGS_BASE* aCfg ) override;
+    void loadPages();
 
 private:
     wxBoxSizer* m_mainSizer;