KiCad PCB EDA Suite
Loading...
Searching...
No Matches
dialog_export_odbpp.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software: you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
21
22#include <board.h>
23#include <confirm.h>
24#include <footprint.h>
25#include <kidialog.h>
26#include <kiway_holder.h>
27#include <paths.h>
28#include <pcb_edit_frame.h>
29#include <pcbnew_settings.h>
30#include <pgm_base.h>
31#include <progress_reporter.h>
32#include <project.h>
37
38#include <set>
39#include <vector>
40#include <thread_pool.h>
41#include <io/io_mgr.h>
43#include <pcb_io/pcb_io_mgr.h>
44#include <wx/dir.h>
45#include <wx/filedlg.h>
46#include <wx/wfstream.h>
47#include <wx/zipstrm.h>
48#include <wx/tarstrm.h>
49#include <wx/zstream.h>
50
51
53 DIALOG_EXPORT_ODBPP_BASE( aParent ),
54 m_parent( aParent ),
55 m_job( nullptr )
56{
57 m_browseButton->SetBitmap( KiBitmapBundle( BITMAPS::small_folder ) );
58
60
61 // DIALOG_SHIM needs a unique hash_key because classname will be the same for both job and
62 // non-job versions.
63 m_hash_key = TO_UTF8( GetTitle() );
64
65 // Now all widgets have the size fixed, call FinishDialogSettings
67}
68
69
71 wxWindow* aParent ) :
72 DIALOG_EXPORT_ODBPP_BASE( aParent ),
73 m_parent( aEditFrame ),
74 m_job( aJob )
75{
76 m_browseButton->Hide();
77
79
80 // DIALOG_SHIM needs a unique hash_key because classname will be the same for both job and
81 // non-job versions.
82 m_hash_key = TO_UTF8( GetTitle() );
83
84 // Now all widgets have the size fixed, call FinishDialogSettings
86}
87
88
90{
91 if( !m_job )
92 {
93 if( m_outputFileName->GetValue().IsEmpty() )
94 {
95 wxFileName brdFile( m_parent->GetBoard()->GetFileName() );
96 wxFileName odbFile( brdFile.GetPath(), wxString::Format( wxS( "%s-odb" ), brdFile.GetName() ),
98
99 m_outputFileName->SetValue( odbFile.GetFullPath() );
101 }
102 }
103 else
104 {
105 SetTitle( m_job->GetSettingsDialogTitle() );
106
107 m_choiceUnits->SetSelection( static_cast<int>( m_job->m_units ) );
108 m_precision->SetValue( m_job->m_precision );
109 m_choiceCompress->SetSelection( static_cast<int>( m_job->m_compressionMode ) );
111 }
112
113 return true;
114}
115
116
117void DIALOG_EXPORT_ODBPP::onBrowseClicked( wxCommandEvent& event )
118{
119 // clang-format off
120 wxString filter = _( "zip files" )
122 + _( "tgz files" )
123 + AddFileExtListToFilter( { "tgz" } );
124 // clang-format on
125
126 // Build the absolute path of current output directory to preselect it in the file browser.
127 wxString path = ExpandEnvVarSubstitutions( m_outputFileName->GetValue(), &Prj() );
128 wxFileName fn( Prj().AbsolutePath( path ) );
129
130 wxFileName brdFile( m_parent->GetBoard()->GetFileName() );
131
132 wxString fileDialogName( wxString::Format( wxS( "%s-odb" ), brdFile.GetName() ) );
133
134 wxFileDialog dlg( this, _( "Export ODB++ File" ), fn.GetPath(), fileDialogName, filter, wxFD_SAVE );
135
136 if( dlg.ShowModal() == wxID_CANCEL )
137 return;
138
139 path = dlg.GetPath();
140
141 fn = wxFileName( path );
142
143 if( fn.GetExt().Lower() == "zip" )
144 {
145 m_choiceCompress->SetSelection( static_cast<int>( JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::ZIP ) );
146 }
147 else if( fn.GetExt().Lower() == "tgz" )
148 {
149 m_choiceCompress->SetSelection( static_cast<int>( JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::TGZ ) );
150 }
151 else if( path.EndsWith( "/" ) || path.EndsWith( "\\" ) )
152 {
153 m_choiceCompress->SetSelection( static_cast<int>( JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::NONE ) );
154 }
155 else
156 {
157 DisplayErrorMessage( this, _( "The selected output file name is not a supported archive format." ) );
158 return;
159 }
160
161 m_outputFileName->SetValue( path );
162}
163
164
165void DIALOG_EXPORT_ODBPP::onFormatChoice( wxCommandEvent& event )
166{
168}
169
170
172{
173 wxString fn = m_outputFileName->GetValue();
174
175 wxFileName fileName( fn );
176
177 auto compressionMode = static_cast<JOB_EXPORT_PCB_ODB::ODB_COMPRESSION>( m_choiceCompress->GetSelection() );
178
179 int sepIdx = std::max( fn.Find( '/', true ), fn.Find( '\\', true ) );
180 int dotIdx = fn.Find( '.', true );
181
182 if( fileName.IsDir() )
183 fn = fn.Mid( 0, sepIdx );
184 else if( sepIdx < dotIdx )
185 fn = fn.Mid( 0, dotIdx );
186
187 switch( compressionMode )
188 {
190 fn = fn + '.' + FILEEXT::ArchiveFileExtension;
191 break;
193 fn += ".tgz";
194 break;
196 fn = wxFileName( fn, "" ).GetFullPath();
197 break;
198 default:
199 break;
200 };
201
202 m_outputFileName->SetValue( fn );
203}
204
205void DIALOG_EXPORT_ODBPP::onOKClick( wxCommandEvent& event )
206{
207 if( !m_job )
208 {
209 wxString fn = m_outputFileName->GetValue();
210
211 if( fn.IsEmpty() )
212 {
213 DisplayErrorMessage( this, _( "Output file name cannot be empty." ) );
214 return;
215 }
216
217 auto compressionMode = static_cast<JOB_EXPORT_PCB_ODB::ODB_COMPRESSION>( m_choiceCompress->GetSelection() );
218
219 wxFileName fileName( fn );
220 bool isDirectory = fileName.IsDir();
221 wxString extension = fileName.GetExt();
222
223 if( ( compressionMode == JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::NONE && !isDirectory )
224 || ( compressionMode == JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::ZIP && extension != "zip" )
225 || ( compressionMode == JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::TGZ && extension != "tgz" ) )
226 {
227 DisplayErrorMessage( this, _( "The output file name conflicts with the selected compression format." ) );
228 return;
229 }
230 }
231
232 event.Skip();
233}
234
235
237{
238 if( m_job )
239 {
241
242 m_job->m_precision = m_precision->GetValue();
243 m_job->m_units = static_cast<JOB_EXPORT_PCB_ODB::ODB_UNITS>( m_choiceUnits->GetSelection() );
245 }
246
247 return true;
248}
249
250
252 PCB_EDIT_FRAME* aParentFrame, PROGRESS_REPORTER* aProgressReporter,
253 REPORTER* aReporter )
254{
255 wxCHECK( aBoard, /* void */ );
256 wxString outputPath = aJob.GetFullOutputPath( aBoard->GetProject() );
257
258 if( outputPath.IsEmpty() )
259 outputPath = wxFileName( aJob.m_filename ).GetPath();
260
261 wxFileName outputFn( outputPath );
262
263 // Write through symlinks, don't replace them
265
266 if( outputFn.GetPath().IsEmpty() && outputFn.HasName() )
267 outputFn.MakeAbsolute();
268
269 bool outputIsSingleFile = aJob.m_compressionMode != JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::NONE;
270 wxString msg;
271
272 if( !PATHS::EnsurePathExists( outputFn.GetPath(), outputIsSingleFile ) )
273 {
274 msg.Printf( _( "Cannot create output directory '%s'." ), outputFn.GetFullPath() );
275
276 if( aReporter )
277 aReporter->Report( msg, RPT_SEVERITY_ERROR );
278
279 return;
280 }
281
282 if( outputFn.IsDir() && !outputFn.IsDirWritable() )
283 {
284 msg.Printf( _( "Insufficient permissions to folder '%s'." ), outputFn.GetPath() );
285
286 if( aReporter )
287 aReporter->Report( msg, RPT_SEVERITY_ERROR );
288
289 return;
290 }
291
292 if( outputIsSingleFile )
293 {
294 bool writeable = outputFn.FileExists() ? outputFn.IsFileWritable() : outputFn.IsDirWritable();
295
296 if( !writeable )
297 {
298 msg.Printf( _( "Insufficient permissions to save file '%s'." ), outputFn.GetFullPath() );
299
300 if( aReporter )
301 aReporter->Report( msg, RPT_SEVERITY_ERROR );
302
303 return;
304 }
305 }
306
307 wxFileName tempFile( outputFn.GetFullPath() );
308
309 if( outputIsSingleFile )
310 {
311 if( outputFn.Exists() )
312 {
313 if( aParentFrame )
314 {
315 msg = wxString::Format( _( "Output files '%s' already exists. Do you want to overwrite it?" ),
316 outputFn.GetFullPath() );
317
318 KIDIALOG errorDlg( aParentFrame, msg, _( "Confirmation" ), wxOK | wxCANCEL | wxICON_WARNING );
319 errorDlg.SetOKLabel( _( "Overwrite" ) );
320
321 if( errorDlg.ShowModal() != wxID_OK )
322 return;
323
324 if( !wxRemoveFile( outputFn.GetFullPath() ) )
325 {
326 msg.Printf( _( "Cannot remove existing output file '%s'." ), outputFn.GetFullPath() );
327 DisplayErrorMessage( aParentFrame, msg );
328 return;
329 }
330 }
331 else
332 {
333 msg = wxString::Format( _( "Output file '%s' already exists." ), outputFn.GetFullPath() );
334
335 if( aReporter )
336 aReporter->Report( msg, RPT_SEVERITY_ERROR );
337
338 return;
339 }
340 }
341
342 tempFile.AssignDir( wxFileName::GetTempDir() );
343 tempFile.AppendDir( "kicad" );
344 tempFile.AppendDir( "odb" );
345
346 if( !wxFileName::Mkdir( tempFile.GetFullPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
347 {
348 msg.Printf( _( "Cannot create temporary output directory." ) );
349
350 if( aReporter )
351 aReporter->Report( msg, RPT_SEVERITY_ERROR );
352
353 return;
354 }
355 }
356 else
357 {
358 // Test for the output directory of tempFile
359 wxDir testDir( tempFile.GetFullPath() );
360
361 if( testDir.IsOpened() && ( testDir.HasFiles() || testDir.HasSubDirs() ) )
362 {
363 if( aParentFrame )
364 {
365 msg = wxString::Format( _( "Output directory '%s' already exists and is not empty. "
366 "Do you want to overwrite it?" ),
367 tempFile.GetFullPath() );
368
369 KIDIALOG errorDlg( aParentFrame, msg, _( "Confirmation" ), wxOK | wxCANCEL | wxICON_WARNING );
370 errorDlg.SetOKLabel( _( "Overwrite" ) );
371
372 if( errorDlg.ShowModal() != wxID_OK )
373 return;
374
375 if( !tempFile.Rmdir( wxPATH_RMDIR_RECURSIVE ) )
376 {
377 msg.Printf( _( "Cannot remove existing output directory '%s'." ), tempFile.GetFullPath() );
378 DisplayErrorMessage( aParentFrame, msg );
379 return;
380 }
381 }
382 else
383 {
384 msg = wxString::Format( _( "Output directory '%s' already exists." ), tempFile.GetFullPath() );
385
386 if( aReporter )
387 aReporter->Report( msg, RPT_SEVERITY_ERROR );
388
389 return;
390 }
391 }
392 }
393
394 std::map<std::string, UTF8> props;
395
396 props["units"] = aJob.m_units == JOB_EXPORT_PCB_ODB::ODB_UNITS::MM ? "mm" : "inch";
397 props["sigfig"] = wxString::Format( "%d", aJob.m_precision );
398
399 auto saveFile =
400 [&]() -> bool
401 {
402 try
403 {
405 pi->SetReporter( aReporter );
406 pi->SetProgressReporter( aProgressReporter );
407 pi->SaveBoard( tempFile.GetFullPath(), aBoard, &props );
408 return true;
409 }
410 catch( const IO_ERROR& ioe )
411 {
412 if( aReporter )
413 {
414 msg = wxString::Format( _( "Error generating ODBPP files '%s'.\n%s" ),
415 tempFile.GetFullPath(), ioe.What() );
416 aReporter->Report( msg, RPT_SEVERITY_ERROR );
417 }
418
419 // In case we started a file but didn't fully write it, clean up
420 wxFileName::Rmdir( tempFile.GetFullPath() );
421 return false;
422 }
423 };
424
426 auto ret = tp.submit( saveFile );
427
428 std::future_status status = ret.wait_for( std::chrono::milliseconds( 250 ) );
429
430 while( status != std::future_status::ready )
431 {
432 if( aProgressReporter )
433 aProgressReporter->KeepRefreshing();
434
435 status = ret.wait_for( std::chrono::milliseconds( 250 ) );
436 }
437
438 try
439 {
440 if( !ret.get() )
441 return;
442 }
443 catch( const std::exception& e )
444 {
445 if( aReporter )
446 {
447 aReporter->Report( wxString::Format( "Exception in ODB++ generation: %s", e.what() ),
449 }
450
451 return;
452 }
453
455 {
456 if( aProgressReporter )
457 aProgressReporter->AdvancePhase( _( "Compressing output" ) );
458
459 wxFFileOutputStream fnout( outputFn.GetFullPath() );
460 wxZipOutputStream zipStream( fnout );
461
462 std::function<void( const wxString&, const wxString& )> addDirToZip =
463 [&]( const wxString& dirPath, const wxString& parentPath )
464 {
465 wxDir dir( dirPath );
466 wxString fileName;
467
468 bool cont = dir.GetFirst( &fileName, wxEmptyString, wxDIR_DEFAULT );
469
470 while( cont )
471 {
472 wxFileName fileInZip( dirPath, fileName );
473 wxString relativePath = fileName;
474
475 if( !parentPath.IsEmpty() )
476 relativePath = parentPath + wxString( wxFileName::GetPathSeparator() ) + fileName;
477
478 if( wxFileName::DirExists( fileInZip.GetFullPath() ) )
479 {
480 zipStream.PutNextDirEntry( relativePath );
481 addDirToZip( fileInZip.GetFullPath(), relativePath );
482 }
483 else
484 {
485 wxFFileInputStream fileStream( fileInZip.GetFullPath() );
486 zipStream.PutNextEntry( relativePath );
487 fileStream.Read( zipStream );
488 }
489 cont = dir.GetNext( &fileName );
490 }
491 };
492
493 addDirToZip( tempFile.GetFullPath(), wxEmptyString );
494
495 zipStream.Close();
496 fnout.Close();
497
498 tempFile.Rmdir( wxPATH_RMDIR_RECURSIVE );
499 }
501 {
502 wxFFileOutputStream fnout( outputFn.GetFullPath() );
503 wxZlibOutputStream zlibStream( fnout, -1, wxZLIB_GZIP );
504 wxTarOutputStream tarStream( zlibStream );
505
506 std::function<void( const wxString&, const wxString& )> addDirToTar =
507 [&]( const wxString& dirPath, const wxString& parentPath )
508 {
509 wxDir dir( dirPath );
510 wxString fileName;
511
512 bool cont = dir.GetFirst( &fileName, wxEmptyString, wxDIR_DEFAULT );
513 while( cont )
514 {
515 wxFileName fileInTar( dirPath, fileName );
516 wxString relativePath = fileName;
517
518 if( !parentPath.IsEmpty() )
519 relativePath = parentPath + wxString( wxFileName::GetPathSeparator() ) + fileName;
520
521 if( wxFileName::DirExists( fileInTar.GetFullPath() ) )
522 {
523 tarStream.PutNextDirEntry( relativePath );
524 addDirToTar( fileInTar.GetFullPath(), relativePath );
525 }
526 else
527 {
528 wxFFileInputStream fileStream( fileInTar.GetFullPath() );
529 tarStream.PutNextEntry( relativePath, wxDateTime::Now(), fileStream.GetLength() );
530 fileStream.Read( tarStream );
531 }
532 cont = dir.GetNext( &fileName );
533 }
534 };
535
536 addDirToTar( tempFile.GetFullPath(),
537 tempFile.GetPath( wxPATH_NO_SEPARATOR ).AfterLast( tempFile.GetPathSeparator() ) );
538
539 tarStream.Close();
540 zlibStream.Close();
541 fnout.Close();
542
543 tempFile.Rmdir( wxPATH_RMDIR_RECURSIVE );
544 }
545
546 if( aProgressReporter )
547 aProgressReporter->SetCurrentProgress( 1 );
548}
wxBitmapBundle KiBitmapBundle(BITMAPS aBitmap, int aMinHeight)
Definition: bitmap.cpp:110
Information pertinent to a Pcbnew printed circuit board.
Definition: board.h:317
const wxString & GetFileName() const
Definition: board.h:354
PROJECT * GetProject() const
Definition: board.h:538
Class DIALOG_EXPORT_ODBPP_BASE.
STD_BITMAP_BUTTON * m_browseButton
bool TransferDataFromWindow() override
void onFormatChoice(wxCommandEvent &event) override
static void GenerateODBPPFiles(const JOB_EXPORT_PCB_ODB &aJob, BOARD *aBoard, PCB_EDIT_FRAME *aParentFrame=nullptr, PROGRESS_REPORTER *aProgressReporter=nullptr, REPORTER *aErrorReporter=nullptr)
bool TransferDataToWindow() override
void onBrowseClicked(wxCommandEvent &event) override
JOB_EXPORT_PCB_ODB * m_job
DIALOG_EXPORT_ODBPP(PCB_EDIT_FRAME *aParent)
PCB_EDIT_FRAME * m_parent
void onOKClick(wxCommandEvent &event) override
void SetupStandardButtons(std::map< int, wxString > aLabels={})
std::string m_hash_key
Definition: dialog_shim.h:236
void finishDialogSettings()
In all dialogs, we must call the same functions to fix minimal dlg size, the default position and per...
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
Definition: ki_exception.h:77
virtual const wxString What() const
A composite of Problem() and Where()
Definition: exceptions.cpp:30
wxString GetSettingsDialogTitle() const override
ODB_COMPRESSION m_compressionMode
void SetConfiguredOutputPath(const wxString &aPath)
Sets the configured output path for the job, this path is always saved to file.
Definition: job.cpp:153
wxString GetFullOutputPath(PROJECT *aProject) const
Returns the full output path for the job, taking into account the configured output path,...
Definition: job.cpp:100
wxString GetConfiguredOutputPath() const
Returns the configured output path for the job.
Definition: job.h:232
Helper class to create more flexible dialogs, including 'do not show again' checkbox handling.
Definition: kidialog.h:50
int ShowModal() override
Definition: kidialog.cpp:95
PROJECT & Prj() const
Return a reference to the PROJECT associated with this KIWAY.
static bool EnsurePathExists(const wxString &aPath, bool aPathToFile=false)
Attempts to create a given path if it does not exist.
Definition: paths.cpp:477
BOARD * GetBoard() const
The main frame for Pcbnew.
static PCB_IO * PluginFind(PCB_FILE_T aFileType)
Return a #PLUGIN which the caller can use to import, export, save, or load design documents.
Definition: pcb_io_mgr.cpp:68
A progress reporter interface for use in multi-threaded environments.
virtual bool KeepRefreshing(bool aWait=false)=0
Update the UI (if any).
virtual void AdvancePhase()=0
Use the next available virtual zone of the dialog progress bar.
virtual void SetCurrentProgress(double aProgress)=0
Set the progress value to aProgress (0..1).
A pure virtual class used to derive REPORTER objects from.
Definition: reporter.h:73
virtual REPORTER & Report(const wxString &aText, SEVERITY aSeverity=RPT_SEVERITY_UNDEFINED)
Report a string with a given severity.
Definition: reporter.h:102
void SetBitmap(const wxBitmapBundle &aBmp)
static void ResolvePossibleSymlinks(wxFileName &aFilename)
Definition: wx_filename.cpp:91
const wxString ExpandEnvVarSubstitutions(const wxString &aString, const PROJECT *aProject)
Replace any environment variable & text variable references with their values.
Definition: common.cpp:355
void DisplayErrorMessage(wxWindow *aParent, const wxString &aText, const wxString &aExtraInfo)
Display an error message with aMessage.
Definition: confirm.cpp:194
This file is part of the common library.
#define _(s)
static const std::string ArchiveFileExtension
std::unique_ptr< T > IO_RELEASER
Helper to hold and release an IO_BASE object when exceptions are thrown.
Definition: io_mgr.h:33
This file is part of the common library.
see class PGM_BASE
@ RPT_SEVERITY_ERROR
#define TO_UTF8(wxstring)
Convert a wxString to a UTF8 encoded C string for all wxWidgets build modes.
Definition: string_utils.h:429
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
Definition: thread_pool.cpp:30
static thread_pool * tp
Definition: thread_pool.cpp:28
BS::thread_pool thread_pool
Definition: thread_pool.h:31
wxString AddFileExtListToFilter(const std::vector< std::string > &aExts)
Build the wildcard extension file dialog wildcard filter to add to the base message dialog.