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 <set>
23#include <vector>
24#include <thread_pool.h>
25
26#include <wx/dir.h>
27#include <wx/filedlg.h>
28#include <wx/wfstream.h>
29#include <kiplatform/ui.h>
30#include <wx/zipstrm.h>
31#include <wx/tarstrm.h>
32#include <wx/zstream.h>
33
34#include <board.h>
35#include <reporter.h>
36#include <confirm.h>
37#include <footprint.h>
38#include <kidialog.h>
39#include <kiway_holder.h>
40#include <paths.h>
41#include <pcb_edit_frame.h>
42#include <pcbnew_settings.h>
43#include <pgm_base.h>
44#include <progress_reporter.h>
45#include <project.h>
48#include <string_utils.h>
50#include <io/io_mgr.h>
52#include <pcb_io/pcb_io_mgr.h>
53#include <kiplatform/io.h>
54
55
56
58 DIALOG_EXPORT_ODBPP_BASE( aParent ),
59 m_parent( aParent ),
60 m_job( nullptr )
61{
63
65
66 // DIALOG_SHIM needs a unique hash_key because classname will be the same for both job and
67 // non-job versions.
68 m_hash_key = TO_UTF8( GetTitle() );
69
70 // Now all widgets have the size fixed, call FinishDialogSettings
72}
73
74
76 wxWindow* aParent ) :
77 DIALOG_EXPORT_ODBPP_BASE( aParent ),
78 m_parent( aEditFrame ),
79 m_job( aJob )
80{
81 m_browseButton->Hide();
82
84
85 // DIALOG_SHIM needs a unique hash_key because classname will be the same for both job and
86 // non-job versions.
87 m_hash_key = TO_UTF8( GetTitle() );
88
89 // Now all widgets have the size fixed, call FinishDialogSettings
91}
92
93
95{
96 if( !m_job )
97 {
98 if( m_outputFileName->GetValue().IsEmpty() )
99 {
100 wxFileName brdFile( m_parent->GetBoard()->GetFileName() );
101 wxFileName odbFile( brdFile.GetPath(), wxString::Format( wxS( "%s-odb" ), brdFile.GetName() ),
103
104 m_outputFileName->SetValue( odbFile.GetFullPath() );
106 }
107 }
108 else
109 {
110 SetTitle( m_job->GetSettingsDialogTitle() );
111
112 m_choiceUnits->SetSelection( static_cast<int>( m_job->m_units ) );
113 m_precision->SetValue( m_job->m_precision );
114 m_choiceCompress->SetSelection( static_cast<int>( m_job->m_compressionMode ) );
115 m_outputFileName->SetValue( m_job->GetConfiguredOutputPath() );
116 }
117
118 return true;
119}
120
121
122void DIALOG_EXPORT_ODBPP::onBrowseClicked( wxCommandEvent& event )
123{
124 // clang-format off
125 wxString filter = _( "zip files" )
127 + _( "tgz files" )
128 + AddFileExtListToFilter( { "tgz" } );
129 // clang-format on
130
131 // Build the absolute path of current output directory to preselect it in the file browser.
132 wxString path = ExpandEnvVarSubstitutions( m_outputFileName->GetValue(), &Prj() );
133 wxFileName fn( Prj().AbsolutePath( path ) );
134
135 wxFileName brdFile( m_parent->GetBoard()->GetFileName() );
136
137 wxString fileDialogName( wxString::Format( wxS( "%s-odb" ), brdFile.GetName() ) );
138
139 wxFileDialog dlg( this, _( "Export ODB++ File" ), fn.GetPath(), fileDialogName, filter, wxFD_SAVE );
140
142
143 if( dlg.ShowModal() == wxID_CANCEL )
144 return;
145
146 path = dlg.GetPath();
147
148 fn = wxFileName( path );
149
150 if( fn.GetExt().Lower() == "zip" )
151 {
152 m_choiceCompress->SetSelection( static_cast<int>( JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::ZIP ) );
153 }
154 else if( fn.GetExt().Lower() == "tgz" )
155 {
156 m_choiceCompress->SetSelection( static_cast<int>( JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::TGZ ) );
157 }
158 else if( path.EndsWith( "/" ) || path.EndsWith( "\\" ) )
159 {
160 m_choiceCompress->SetSelection( static_cast<int>( JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::NONE ) );
161 }
162 else
163 {
164 DisplayErrorMessage( this, _( "The selected output file name is not a supported archive format." ) );
165 return;
166 }
167
168 m_outputFileName->SetValue( path );
169}
170
171
172void DIALOG_EXPORT_ODBPP::onFormatChoice( wxCommandEvent& event )
173{
175}
176
177
179{
180 wxString fn = m_outputFileName->GetValue();
181
182 wxFileName fileName( fn );
183
184 auto compressionMode = static_cast<JOB_EXPORT_PCB_ODB::ODB_COMPRESSION>( m_choiceCompress->GetSelection() );
185
186 int sepIdx = std::max( fn.Find( '/', true ), fn.Find( '\\', true ) );
187 int dotIdx = fn.Find( '.', true );
188
189 if( fileName.IsDir() )
190 fn = fn.Mid( 0, sepIdx );
191 else if( sepIdx < dotIdx )
192 fn = fn.Mid( 0, dotIdx );
193
194 switch( compressionMode )
195 {
197 fn = fn + '.' + FILEEXT::ArchiveFileExtension;
198 break;
200 fn += ".tgz";
201 break;
203 fn = wxFileName( fn, "" ).GetFullPath();
204 break;
205 default:
206 break;
207 };
208
209 m_outputFileName->SetValue( fn );
210}
211
212
213void DIALOG_EXPORT_ODBPP::onOKClick( wxCommandEvent& event )
214{
215 if( !m_job )
216 {
217 wxString fn = m_outputFileName->GetValue();
218
219 if( fn.IsEmpty() )
220 {
221 DisplayErrorMessage( this, _( "Output file name cannot be empty." ) );
222 return;
223 }
224
225 auto compressionMode = static_cast<JOB_EXPORT_PCB_ODB::ODB_COMPRESSION>( m_choiceCompress->GetSelection() );
226
227 wxFileName fileName( fn );
228 bool isDirectory = fileName.IsDir();
229 wxString extension = fileName.GetExt();
230
231 if( ( compressionMode == JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::NONE && !isDirectory )
232 || ( compressionMode == JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::ZIP && extension != "zip" )
233 || ( compressionMode == JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::TGZ && extension != "tgz" ) )
234 {
235 DisplayErrorMessage( this, _( "The output file name conflicts with the selected compression format." ) );
236 return;
237 }
238 }
239
240 event.Skip();
241}
242
243
245{
246 if( m_job )
247 {
248 m_job->SetConfiguredOutputPath( m_outputFileName->GetValue() );
249
250 m_job->m_precision = m_precision->GetValue();
251 m_job->m_units = static_cast<JOB_EXPORT_PCB_ODB::ODB_UNITS>( m_choiceUnits->GetSelection() );
252 m_job->m_compressionMode = static_cast<JOB_EXPORT_PCB_ODB::ODB_COMPRESSION>( m_choiceCompress->GetSelection() );
253 }
254
255 return true;
256}
257
258
260 PCB_EDIT_FRAME* aParentFrame, PROGRESS_REPORTER* aProgressReporter,
261 REPORTER* aReporter )
262{
263 wxCHECK( aBoard, /* void */ );
264 wxString outputPath = aJob.GetFullOutputPath( aBoard->GetProject() );
265
266 if( outputPath.IsEmpty() )
267 outputPath = wxFileName( aJob.m_filename ).GetPath();
268
269 wxFileName outputFn( outputPath );
270
271 // Write through symlinks, don't replace them
273
274 if( outputFn.GetPath().IsEmpty() && outputFn.HasName() )
275 outputFn.MakeAbsolute();
276
277 bool outputIsSingleFile = aJob.m_compressionMode != JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::NONE;
278 wxString msg;
279
280 if( !PATHS::EnsurePathExists( outputFn.GetFullPath(), outputIsSingleFile ) )
281 {
282 msg.Printf( _( "Cannot create output directory '%s'." ), outputFn.GetFullPath() );
283
284 if( aReporter )
285 aReporter->Report( msg, RPT_SEVERITY_ERROR );
286
287 return;
288 }
289
290 if( outputFn.IsDir() && !outputFn.IsDirWritable() )
291 {
292 msg.Printf( _( "Insufficient permissions to folder '%s'." ), outputFn.GetPath() );
293
294 if( aReporter )
295 aReporter->Report( msg, RPT_SEVERITY_ERROR );
296
297 return;
298 }
299
300 if( outputIsSingleFile )
301 {
302 bool writeable = outputFn.FileExists() ? outputFn.IsFileWritable() : outputFn.IsDirWritable();
303
304 if( !writeable )
305 {
306 msg.Printf( _( "Insufficient permissions to save file '%s'." ), outputFn.GetFullPath() );
307
308 if( aReporter )
309 aReporter->Report( msg, RPT_SEVERITY_ERROR );
310
311 return;
312 }
313 }
314
315 wxFileName tempFile( outputFn.GetFullPath() );
316
317 if( outputIsSingleFile )
318 {
319 if( outputFn.Exists() )
320 {
321 if( aParentFrame )
322 {
323 msg = wxString::Format( _( "Output files '%s' already exists. Do you want to overwrite it?" ),
324 outputFn.GetFullPath() );
325
326 KIDIALOG errorDlg( aParentFrame, msg, _( "Confirmation" ), wxOK | wxCANCEL | wxICON_WARNING );
327 errorDlg.SetOKLabel( _( "Overwrite" ) );
328
329 if( errorDlg.ShowModal() != wxID_OK )
330 return;
331
332 if( !wxRemoveFile( outputFn.GetFullPath() ) )
333 {
334 msg.Printf( _( "Cannot remove existing output file '%s'." ), outputFn.GetFullPath() );
335 DisplayErrorMessage( aParentFrame, msg );
336 return;
337 }
338 }
339 }
340
341 tempFile.AssignDir( wxFileName::GetTempDir() );
342 tempFile.AppendDir( "kicad" );
343 tempFile.AppendDir( "odb" );
344
345 if( !wxFileName::Mkdir( tempFile.GetFullPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
346 {
347 msg.Printf( _( "Cannot create temporary output directory." ) );
348
349 if( aReporter )
350 aReporter->Report( msg, RPT_SEVERITY_ERROR );
351
352 return;
353 }
354 }
355 else
356 {
357 // Test for the output directory of tempFile
358 wxDir testDir( tempFile.GetFullPath() );
359
360 if( testDir.IsOpened() && ( testDir.HasFiles() || testDir.HasSubDirs() ) )
361 {
362 if( aParentFrame )
363 {
364 msg = wxString::Format( _( "Output directory '%s' already exists and is not empty. "
365 "Do you want to overwrite it?" ),
366 tempFile.GetFullPath() );
367
368 KIDIALOG errorDlg( aParentFrame, msg, _( "Confirmation" ), wxOK | wxCANCEL | wxICON_WARNING );
369 errorDlg.SetOKLabel( _( "Overwrite" ) );
370
371 if( errorDlg.ShowModal() != wxID_OK )
372 return;
373
374 if( !tempFile.Rmdir( wxPATH_RMDIR_RECURSIVE ) )
375 {
376 msg.Printf( _( "Cannot remove existing output directory '%s'." ), tempFile.GetFullPath() );
377 DisplayErrorMessage( aParentFrame, msg );
378 return;
379 }
380 }
381 }
382 }
383
384 std::map<std::string, UTF8> props;
385
386 props["units"] = aJob.m_units == JOB_EXPORT_PCB_ODB::ODB_UNITS::MM ? "mm" : "inch";
387 props["sigfig"] = wxString::Format( "%d", aJob.m_precision );
388
389 auto saveFile =
390 [&]() -> bool
391 {
392 try
393 {
395 pi->SetReporter( aReporter );
396 pi->SetProgressReporter( aProgressReporter );
397 pi->SaveBoard( tempFile.GetFullPath(), aBoard, &props );
398 return true;
399 }
400 catch( const IO_ERROR& ioe )
401 {
402 if( aReporter )
403 {
404 msg = wxString::Format( _( "Error generating ODBPP files '%s'.\n%s" ),
405 tempFile.GetFullPath(), ioe.What() );
406 aReporter->Report( msg, RPT_SEVERITY_ERROR );
407 }
408
409 // In case we started a file but didn't fully write it, clean up
410 wxFileName::Rmdir( tempFile.GetFullPath() );
411 return false;
412 }
413 };
414
416 auto ret = tp.submit_task( saveFile );
417
418 std::future_status status = ret.wait_for( std::chrono::milliseconds( 250 ) );
419
420 while( status != std::future_status::ready )
421 {
422 if( aProgressReporter )
423 aProgressReporter->KeepRefreshing();
424
425 status = ret.wait_for( std::chrono::milliseconds( 250 ) );
426 }
427
428 try
429 {
430 if( !ret.get() )
431 return;
432 }
433 catch( const std::exception& e )
434 {
435 if( aReporter )
436 {
437 aReporter->Report( wxString::Format( "Exception in ODB++ generation: %s", e.what() ),
439 }
440
441 return;
442 }
443
445 {
446 if( aProgressReporter )
447 aProgressReporter->AdvancePhase( _( "Compressing output" ) );
448
449 wxFFileOutputStream fnout( outputFn.GetFullPath() );
450
451 // Use a large I/O buffer to improve compatibility with cloud-synced folders.
452 // See KIPLATFORM::IO::CLOUD_SYNC_BUFFER_SIZE comment for details.
453 if( FILE* fp = fnout.GetFile()->fp() )
454 setvbuf( fp, nullptr, _IOFBF, KIPLATFORM::IO::CLOUD_SYNC_BUFFER_SIZE );
455
456 wxZipOutputStream zipStream( fnout );
457
458 std::function<void( const wxString&, const wxString& )> addDirToZip =
459 [&]( const wxString& dirPath, const wxString& parentPath )
460 {
461 wxDir dir( dirPath );
462 wxString fileName;
463
464 bool cont = dir.GetFirst( &fileName, wxEmptyString, wxDIR_DEFAULT );
465
466 while( cont )
467 {
468 wxFileName fileInZip( dirPath, fileName );
469 wxString relativePath = fileName;
470
471 if( !parentPath.IsEmpty() )
472 relativePath = parentPath + wxString( wxFileName::GetPathSeparator() ) + fileName;
473
474 if( wxFileName::DirExists( fileInZip.GetFullPath() ) )
475 {
476 zipStream.PutNextDirEntry( relativePath );
477 addDirToZip( fileInZip.GetFullPath(), relativePath );
478 }
479 else
480 {
481 wxFFileInputStream fileStream( fileInZip.GetFullPath() );
482 zipStream.PutNextEntry( relativePath );
483 fileStream.Read( zipStream );
484 }
485 cont = dir.GetNext( &fileName );
486 }
487 };
488
489 addDirToZip( tempFile.GetFullPath(), wxEmptyString );
490
491 zipStream.Close();
492 fnout.Close();
493
494 tempFile.Rmdir( wxPATH_RMDIR_RECURSIVE );
495 }
497 {
498 wxFFileOutputStream fnout( outputFn.GetFullPath() );
499
500 // Use a large I/O buffer to improve compatibility with cloud-synced folders.
501 // See KIPLATFORM::IO::CLOUD_SYNC_BUFFER_SIZE comment for details.
502 if( FILE* fp = fnout.GetFile()->fp() )
503 setvbuf( fp, nullptr, _IOFBF, KIPLATFORM::IO::CLOUD_SYNC_BUFFER_SIZE );
504
505 wxZlibOutputStream zlibStream( fnout, -1, wxZLIB_GZIP );
506 wxTarOutputStream tarStream( zlibStream );
507
508 std::function<void( const wxString&, const wxString& )> addDirToTar =
509 [&]( const wxString& dirPath, const wxString& parentPath )
510 {
511 wxDir dir( dirPath );
512 wxString fileName;
513
514 bool cont = dir.GetFirst( &fileName, wxEmptyString, wxDIR_DEFAULT );
515 while( cont )
516 {
517 wxFileName fileInTar( dirPath, fileName );
518 wxString relativePath = fileName;
519
520 if( !parentPath.IsEmpty() )
521 relativePath = parentPath + wxString( wxFileName::GetPathSeparator() ) + fileName;
522
523 if( wxFileName::DirExists( fileInTar.GetFullPath() ) )
524 {
525 tarStream.PutNextDirEntry( relativePath );
526 addDirToTar( fileInTar.GetFullPath(), relativePath );
527 }
528 else
529 {
530 wxFFileInputStream fileStream( fileInTar.GetFullPath() );
531 tarStream.PutNextEntry( relativePath, wxDateTime::Now(), fileStream.GetLength() );
532 fileStream.Read( tarStream );
533 }
534 cont = dir.GetNext( &fileName );
535 }
536 };
537
538 addDirToTar( tempFile.GetFullPath(),
539 tempFile.GetPath( wxPATH_NO_SEPARATOR ).AfterLast( tempFile.GetPathSeparator() ) );
540
541 tarStream.Close();
542 zlibStream.Close();
543 fnout.Close();
544
545 tempFile.Rmdir( wxPATH_RMDIR_RECURSIVE );
546 }
547
548 if( aProgressReporter )
549 aProgressReporter->SetCurrentProgress( 1 );
550}
wxBitmapBundle KiBitmapBundle(BITMAPS aBitmap, int aMinHeight)
Definition bitmap.cpp:110
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:322
PROJECT * GetProject() const
Definition board.h:579
DIALOG_EXPORT_ODBPP_BASE(wxWindow *parent, wxWindowID id=wxID_ANY, const wxString &title=_("Export ODB++"), const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxSize(-1,-1), long style=wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER|wxBORDER_DEFAULT)
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
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.
virtual const wxString What() const
A composite of Problem() and Where()
ODB_COMPRESSION m_compressionMode
wxString GetFullOutputPath(PROJECT *aProject) const
Returns the full output path for the job, taking into account the configured output path,...
Definition job.cpp:150
Helper class to create more flexible dialogs, including 'do not show again' checkbox handling.
Definition kidialog.h:42
int ShowModal() override
Definition kidialog.cpp:93
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:508
The main frame for Pcbnew.
static PCB_IO * FindPlugin(PCB_FILE_T aFileType)
Return a #PLUGIN which the caller can use to import, export, save, or load design documents.
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
static void ResolvePossibleSymlinks(wxFileName &aFilename)
const wxString ExpandEnvVarSubstitutions(const wxString &aString, const PROJECT *aProject)
Replace any environment variable & text variable references with their values.
Definition common.cpp:558
void DisplayErrorMessage(wxWindow *aParent, const wxString &aText, const wxString &aExtraInfo)
Display an error message with aMessage.
Definition confirm.cpp:202
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
static constexpr size_t CLOUD_SYNC_BUFFER_SIZE
Buffer size for file I/O operations on cloud-synced folders.
Definition io.h:77
void AllowNetworkFileSystems(wxDialog *aDialog)
Configure a file dialog to show network and virtual file systems.
Definition wxgtk/ui.cpp:435
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.
std::string path
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
static thread_pool * tp
BS::priority_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.