Move IPC2581 output to REPORTER

Adds a wx_reporter_panel to the 2581 output window, shunt messages
through the reporter instance

Fixes https://gitlab.com/kicad/code/kicad/-/issues/16314
This commit is contained in:
Seth Hillbrand 2025-08-20 13:34:36 -07:00
parent c2f503774b
commit 44228bd580
7 changed files with 151 additions and 150 deletions

View File

@ -20,9 +20,11 @@
#include "dialogs/dialog_export_2581.h"
#include <set>
#include <map>
#include <vector>
#include <wx/filedlg.h>
#include <wx/filefn.h>
#include <board.h>
#include <footprint.h>
@ -32,10 +34,14 @@
#include <pgm_base.h>
#include <project.h>
#include <project/project_file.h>
#include <pcb_io/pcb_io_mgr.h>
#include <widgets/wx_html_report_panel.h>
#include <widgets/wx_progress_reporters.h>
#include <settings/settings_manager.h>
#include <string_utils.h>
#include <widgets/std_bitmap_button.h>
#include <jobs/job_export_pcb_ipc2581.h>
#include <wx_filename.h>
@ -213,6 +219,88 @@ void DIALOG_EXPORT_2581::onDistPNChange( wxCommandEvent& event )
}
void DIALOG_EXPORT_2581::onOKClick( wxCommandEvent& event )
{
if( m_job )
{
if( TransferDataFromWindow() )
EndModal( wxID_OK );
return;
}
if( !TransferDataFromWindow() )
return;
m_messagesPanel->Clear();
REPORTER& reporter = m_messagesPanel->Reporter();
wxFileName pcbFileName = GetOutputPath();
WX_FILENAME::ResolvePossibleSymlinks( pcbFileName );
if( pcbFileName.GetName().empty() )
{
reporter.Report( _( "The board must be saved before generating IPC-2581 file." ),
RPT_SEVERITY_ERROR );
return;
}
if( !m_parent->IsWritable( pcbFileName ) )
{
reporter.Report( wxString::Format( _( "Insufficient permissions to write file '%s'." ),
pcbFileName.GetFullPath() ),
RPT_SEVERITY_ERROR );
return;
}
wxString tempFile = wxFileName::CreateTempFileName( wxS( "pcbnew_ipc" ) );
WX_PROGRESS_REPORTER progress( this, _( "Generate IPC-2581 File" ), 5, PR_CAN_ABORT );
std::map<std::string, UTF8> props;
props[ "units" ] = TO_UTF8( GetUnitsString() );
props[ "sigfig" ] = TO_UTF8( GetPrecision() );
props[ "version" ] = TO_UTF8( wxString( GetVersion() ) );
props[ "OEMRef" ] = TO_UTF8( GetOEM() );
props[ "mpn" ] = TO_UTF8( GetMPN() );
props[ "mfg" ] = TO_UTF8( GetMfg() );
props[ "dist" ] = TO_UTF8( GetDist() );
props[ "distpn" ] = TO_UTF8( GetDistPN() );
try
{
IO_RELEASER<PCB_IO> pi( PCB_IO_MGR::PluginFind( PCB_IO_MGR::IPC2581 ) );
pi->SetProgressReporter( &progress );
pi->SetReporter( &reporter );
pi->SaveBoard( tempFile, m_parent->GetBoard(), &props );
}
catch( const IO_ERROR& ioe )
{
reporter.Report( wxString::Format( _( "Error generating IPC-2581 file '%s'.\n%s" ),
pcbFileName.GetFullPath(), ioe.What() ),
RPT_SEVERITY_ERROR );
wxRemoveFile( tempFile );
return;
}
if( wxFileExists( pcbFileName.GetFullPath() ) )
wxRemoveFile( pcbFileName.GetFullPath() );
if( !wxRenameFile( tempFile, pcbFileName.GetFullPath() ) )
{
reporter.Report( wxString::Format( _( "Failed to create file '%s'." ), pcbFileName.GetFullPath() ),
RPT_SEVERITY_ERROR );
wxRemoveFile( tempFile );
return;
}
reporter.Report( _( "IPC-2581 file generated successfully." ), RPT_SEVERITY_ACTION );
}
void DIALOG_EXPORT_2581::init()
{
m_textDistributor->SetSize( m_choiceDistPN->GetSize() );

View File

@ -1700,6 +1700,17 @@
</object>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxEXPAND|wxLEFT|wxRIGHT</property>
<property name="proportion">1</property>
<object class="wxPanel" expanded="false">
<property name="minimum_size">-300,150</property>
<property name="name">m_messagesPanel</property>
<property name="permission">protected</property>
<property name="subclass">WX_HTML_REPORT_PANEL; widgets/wx_html_report_panel.h; forward_declare</property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxALL|wxEXPAND</property>

View File

@ -103,6 +103,7 @@ private:
void onCompressCheck( wxCommandEvent& event ) override;
void onMfgPNChange( wxCommandEvent& event ) override;
void onDistPNChange( wxCommandEvent& event ) override;
void onOKClick( wxCommandEvent& event ) override;
void init();

View File

@ -1,11 +1,12 @@
///////////////////////////////////////////////////////////////////////////
// C++ code generated with wxFormBuilder (version 4.2.1-0-g80c4cb6)
// C++ code generated with wxFormBuilder (version 4.2.1-0-g80c4cb6a-dirty)
// http://www.wxformbuilder.org/
//
// PLEASE DO *NOT* EDIT THIS FILE!
///////////////////////////////////////////////////////////////////////////
#include "widgets/std_bitmap_button.h"
#include "widgets/wx_html_report_panel.h"
#include "dialog_export_2581_base.h"
@ -185,6 +186,11 @@ DIALOG_EXPORT_2581_BASE::DIALOG_EXPORT_2581_BASE( wxWindow* parent, wxWindowID i
bMainSizer->Add( bSizerMiddle, 0, wxEXPAND|wxBOTTOM, 5 );
m_messagesPanel = new WX_HTML_REPORT_PANEL( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
m_messagesPanel->SetMinSize( wxSize( -300,150 ) );
bMainSizer->Add( m_messagesPanel, 1, wxEXPAND|wxLEFT|wxRIGHT, 5 );
m_stdButtons = new wxStdDialogButtonSizer();
m_stdButtonsOK = new wxButton( this, wxID_OK );
m_stdButtons->AddButton( m_stdButtonsOK );

View File

@ -1,5 +1,5 @@
///////////////////////////////////////////////////////////////////////////
// C++ code generated with wxFormBuilder (version 4.2.1-0-g80c4cb6)
// C++ code generated with wxFormBuilder (version 4.2.1-0-g80c4cb6a-dirty)
// http://www.wxformbuilder.org/
//
// PLEASE DO *NOT* EDIT THIS FILE!
@ -11,6 +11,7 @@
#include <wx/xrc/xmlres.h>
#include <wx/intl.h>
class STD_BITMAP_BUTTON;
class WX_HTML_REPORT_PANEL;
#include "dialog_shim.h"
#include <wx/string.h>
@ -31,6 +32,7 @@ class STD_BITMAP_BUTTON;
#include <wx/spinctrl.h>
#include <wx/checkbox.h>
#include <wx/gbsizer.h>
#include <wx/panel.h>
#include <wx/dialog.h>
///////////////////////////////////////////////////////////////////////////
@ -68,6 +70,7 @@ class DIALOG_EXPORT_2581_BASE : public DIALOG_SHIM
wxChoice* m_choiceDistPN;
wxStaticText* m_staticText9;
wxTextCtrl* m_textDistributor;
WX_HTML_REPORT_PANEL* m_messagesPanel;
wxStdDialogButtonSizer* m_stdButtons;
wxButton* m_stdButtonsOK;
wxButton* m_stdButtonsCancel;

View File

@ -1284,131 +1284,7 @@ int BOARD_EDITOR_CONTROL::GenIPC2581File( const TOOL_EVENT& aEvent )
{
DIALOG_EXPORT_2581 dlg( m_frame );
if( dlg.ShowModal() != wxID_OK )
return 0;
wxFileName pcbFileName = dlg.GetOutputPath();
// Write through symlinks, don't replace them
WX_FILENAME::ResolvePossibleSymlinks( pcbFileName );
if( pcbFileName.GetName().empty() )
{
DisplayError( m_frame, _( "The board must be saved before generating IPC-2581 file." ) );
return 0;
}
if( !m_frame->IsWritable( pcbFileName ) )
{
DisplayError( m_frame, wxString::Format( _( "Insufficient permissions to write file '%s'." ),
pcbFileName.GetFullPath() ) );
return 0;
}
wxString tempFile = wxFileName::CreateTempFileName( wxS( "pcbnew_ipc" ) );
wxString upperTxt;
wxString lowerTxt;
WX_PROGRESS_REPORTER reporter( m_frame, _( "Generate IPC-2581 File" ), 5, PR_CAN_ABORT );
std::map<std::string, UTF8> props;
props["units"] = dlg.GetUnitsString();
props["sigfig"] = dlg.GetPrecision();
props["version"] = dlg.GetVersion();
props["OEMRef"] = dlg.GetOEM();
props["mpn"] = dlg.GetMPN();
props["mfg"] = dlg.GetMfg();
props["dist"] = dlg.GetDist();
props["distpn"] = dlg.GetDistPN();
auto saveFile =
[&]() -> bool
{
try
{
IO_RELEASER<PCB_IO> pi( PCB_IO_MGR::PluginFind( PCB_IO_MGR::IPC2581 ) );
pi->SetProgressReporter( &reporter );
pi->SaveBoard( tempFile, m_frame->GetBoard(), &props );
return true;
}
catch( const IO_ERROR& ioe )
{
DisplayError( m_frame, wxString::Format( _( "Error generating IPC-2581 file '%s'.\n%s" ),
pcbFileName.GetFullPath(),
ioe.What() ) );
lowerTxt.Printf( _( "Failed to create temporary file '%s'." ), tempFile );
m_frame->SetMsgPanel( upperTxt, lowerTxt );
// In case we started a file but didn't fully write it, clean up
wxRemoveFile( tempFile );
return false;
}
};
thread_pool& tp = GetKiCadThreadPool();
auto ret = tp.submit( saveFile );
std::future_status status = ret.wait_for( std::chrono::milliseconds( 250 ) );
while( status != std::future_status::ready )
{
reporter.KeepRefreshing();
status = ret.wait_for( std::chrono::milliseconds( 250 ) );
}
try
{
if( !ret.get() )
return 0;
}
catch( const std::exception& e )
{
wxLogError( "Exception in IPC-2581 generation: %s", e.what() );
m_frame->GetScreen()->SetContentModified( false );
return 0;
}
// Preserve the permissions of the current file
KIPLATFORM::IO::DuplicatePermissions( pcbFileName.GetFullPath(), tempFile );
if( dlg.GetCompress() )
{
wxFileName tempfn = pcbFileName;
tempfn.SetExt( FILEEXT::Ipc2581FileExtension );
wxFileName zipfn = tempFile;
zipfn.SetExt( "zip" );
{
wxFFileOutputStream fnout( zipfn.GetFullPath() );
wxZipOutputStream zip( fnout );
wxFFileInputStream fnin( tempFile );
zip.PutNextEntry( tempfn.GetFullName() );
fnin.Read( zip );
}
wxRemoveFile( tempFile );
tempFile = zipfn.GetFullPath();
}
// If save succeeded, replace the original with what we just wrote
if( !wxRenameFile( tempFile, pcbFileName.GetFullPath() ) )
{
DisplayError( m_frame, wxString::Format( _( "Error generating IPC-2581 file '%s'.\n"
"Failed to rename temporary file '%s." ),
pcbFileName.GetFullPath(),
tempFile ) );
lowerTxt.Printf( _( "Failed to rename temporary file '%s'." ),
tempFile );
m_frame->SetMsgPanel( upperTxt, lowerTxt );
}
m_frame->GetScreen()->SetContentModified( false );
dlg.ShowModal();
return 0;
}

View File

@ -923,7 +923,7 @@ void PCB_IO_IPC2581::addShape( wxXmlNode* aContentNode, const PAD& aPad, PCB_LAY
break;
}
default:
wxLogError( "Unknown pad type" );
Report( _( "Pad has unsupported type; it was skipped." ), RPT_SEVERITY_WARNING );
break;
}
@ -1256,8 +1256,9 @@ wxXmlNode* PCB_IO_IPC2581::generateBOMSection( wxXmlNode* aEcadNode )
if( iter == m_footprint_dict.end() )
{
wxLogError( "Footprint %s not found in dictionary",
fp->GetFPID().GetLibItemName().wx_str() );
Report( wxString::Format( _( "Footprint %s not found in dictionary; BOM data may be incomplete." ),
fp->GetFPID().GetLibItemName().wx_str() ),
RPT_SEVERITY_WARNING );
continue;
}
@ -1271,8 +1272,9 @@ wxXmlNode* PCB_IO_IPC2581::generateBOMSection( wxXmlNode* aEcadNode )
}
else
{
wxLogError( "Footprint %s not found in OEMRef dictionary",
fp->GetFPID().GetLibItemName().wx_str() );
Report( wxString::Format( _( "Component \"%s\" missing OEM reference; BOM entry will be skipped." ),
fp->GetFPID().GetLibItemName().wx_str() ),
RPT_SEVERITY_WARNING );
}
entry->m_OEMDesignRef = genString( entry->m_OEMDesignRef, "REF" );
@ -2253,7 +2255,7 @@ void PCB_IO_IPC2581::generateProfile( wxXmlNode* aStepNode )
if( ! m_board->GetBoardPolygonOutlines( board_outline ) )
{
wxLogError( "Failed to get board outline" );
Report( _( "Board outline is invalid or missing. Please run DRC." ), RPT_SEVERITY_ERROR );
return;
}
@ -2614,7 +2616,8 @@ void PCB_IO_IPC2581::generateComponents( wxXmlNode* aStepNode )
}
if( !m_OEMRef_dict.emplace( fp, name ).second )
wxLogError( "Duplicate footprint pointers. Please report this bug." );
Report( _( "Duplicate footprint pointers encountered; IPC-2581 output may be incorrect." ),
RPT_SEVERITY_ERROR );
addAttribute( componentNode, "part", genString( name, "REF" ) );
addAttribute( componentNode, "layerRef", m_layer_name_map[fp->GetLayer()] );
@ -2814,7 +2817,8 @@ void PCB_IO_IPC2581::generateLayerSetDrill( wxXmlNode* aLayerNode )
if( it == m_padstack_dict.end() )
{
wxLogError( "Failed to find padstack for via" );
Report( _( "Via uses unsupported padstack; omitted from drill data." ),
RPT_SEVERITY_WARNING );
continue;
}
@ -2839,7 +2843,8 @@ void PCB_IO_IPC2581::generateLayerSetDrill( wxXmlNode* aLayerNode )
if( it == m_padstack_dict.end() )
{
wxLogError( "Failed to find padstack for pad" );
Report( _( "Pad uses unsupported padstack; hole was omitted from drill data." ),
RPT_SEVERITY_WARNING );
continue;
}
@ -2899,6 +2904,8 @@ void PCB_IO_IPC2581::generateLayerSetNet( wxXmlNode* aLayerNode, PCB_LAYER_ID aL
wxXmlNode* teardropLayerSetNode = nullptr;
wxXmlNode* teardropFeatureSetNode = nullptr;
bool teardrop_warning = false;
if( BOARD_CONNECTED_ITEM* item = dynamic_cast<BOARD_CONNECTED_ITEM*>( *it );
IsCopperLayer( aLayer ) && item )
{
@ -2954,7 +2961,9 @@ void PCB_IO_IPC2581::generateLayerSetNet( wxXmlNode* aLayerNode, PCB_LAYER_ID aL
{
wxXmlNode* zoneFeatureNode = specialNode;
if( zone->IsTeardropArea() && m_version > 'B' )
if( zone->IsTeardropArea() )
{
if( m_version > 'B' )
{
if( !teardropFeatureSetNode )
{
@ -2974,6 +2983,13 @@ void PCB_IO_IPC2581::generateLayerSetNet( wxXmlNode* aLayerNode, PCB_LAYER_ID aL
zoneFeatureNode = teardropFeatureSetNode;
}
else if( !teardrop_warning )
{
Report( _( "Teardrops are not supported in IPC-2581 revision B; they were exported as zones." ),
RPT_SEVERITY_WARNING );
teardrop_warning = true;
}
}
else
{
if( FOOTPRINT* fp = zone->GetParentFootprint() )
@ -3448,7 +3464,7 @@ void PCB_IO_IPC2581::SaveBoard( const wxString& aFileName, BOARD* aBoard,
if( !m_xml_doc->Save( out_stream ) )
{
wxLogError( _( "Failed to save file to buffer" ) );
Report( _( "Failed to save IPC-2581 data to buffer." ), RPT_SEVERITY_ERROR );
return;
}