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#include <locale_io.h>
55
56
57
59 DIALOG_EXPORT_ODBPP_BASE( aParent ),
60 m_parent( aParent ),
61 m_job( nullptr )
62{
64
66
67 // DIALOG_SHIM needs a unique hash_key because classname will be the same for both job and
68 // non-job versions.
69 m_hash_key = TO_UTF8( GetTitle() );
70
71 // Now all widgets have the size fixed, call FinishDialogSettings
73}
74
75
77 wxWindow* aParent ) :
78 DIALOG_EXPORT_ODBPP_BASE( aParent ),
79 m_parent( aEditFrame ),
80 m_job( aJob )
81{
82 m_browseButton->Hide();
83
85
86 // DIALOG_SHIM needs a unique hash_key because classname will be the same for both job and
87 // non-job versions.
88 m_hash_key = TO_UTF8( GetTitle() );
89
90 // Now all widgets have the size fixed, call FinishDialogSettings
92}
93
94
96{
97 if( !m_job )
98 {
99 if( m_outputFileName->GetValue().IsEmpty() )
100 {
101 wxFileName brdFile( m_parent->GetBoard()->GetFileName() );
102 wxFileName odbFile( brdFile.GetPath(), wxString::Format( wxS( "%s-odb" ), brdFile.GetName() ),
104
105 m_outputFileName->SetValue( odbFile.GetFullPath() );
107 }
108 }
109 else
110 {
111 SetTitle( m_job->GetSettingsDialogTitle() );
112
113 m_choiceUnits->SetSelection( static_cast<int>( m_job->m_units ) );
114 m_precision->SetValue( m_job->m_precision );
115 m_choiceCompress->SetSelection( static_cast<int>( m_job->m_compressionMode ) );
116 m_outputFileName->SetValue( m_job->GetConfiguredOutputPath() );
117 }
118
119 return true;
120}
121
122
123void DIALOG_EXPORT_ODBPP::onBrowseClicked( wxCommandEvent& event )
124{
125 // clang-format off
126 wxString filter = _( "zip files" )
128 + _( "tgz files" )
129 + AddFileExtListToFilter( { "tgz" } );
130 // clang-format on
131
132 // Build the absolute path of current output directory to preselect it in the file browser.
133 wxString path = ExpandEnvVarSubstitutions( m_outputFileName->GetValue(), &Prj() );
134 wxFileName fn( Prj().AbsolutePath( path ) );
135
136 wxFileName brdFile( m_parent->GetBoard()->GetFileName() );
137
138 wxString fileDialogName( wxString::Format( wxS( "%s-odb" ), brdFile.GetName() ) );
139
140 wxFileDialog dlg( this, _( "Export ODB++ File" ), fn.GetPath(), fileDialogName, filter, wxFD_SAVE );
141
143
144 if( dlg.ShowModal() == wxID_CANCEL )
145 return;
146
147 path = dlg.GetPath();
148
149 fn = wxFileName( path );
150
151 if( fn.GetExt().Lower() == "zip" )
152 {
153 m_choiceCompress->SetSelection( static_cast<int>( JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::ZIP ) );
154 }
155 else if( fn.GetExt().Lower() == "tgz" )
156 {
157 m_choiceCompress->SetSelection( static_cast<int>( JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::TGZ ) );
158 }
159 else if( path.EndsWith( "/" ) || path.EndsWith( "\\" ) )
160 {
161 m_choiceCompress->SetSelection( static_cast<int>( JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::NONE ) );
162 }
163 else
164 {
165 DisplayErrorMessage( this, _( "The selected output file name is not a supported archive format." ) );
166 return;
167 }
168
169 m_outputFileName->SetValue( path );
170}
171
172
173void DIALOG_EXPORT_ODBPP::onFormatChoice( wxCommandEvent& event )
174{
176}
177
178
180{
181 wxString fn = m_outputFileName->GetValue();
182
183 wxFileName fileName( fn );
184
185 auto compressionMode = static_cast<JOB_EXPORT_PCB_ODB::ODB_COMPRESSION>( m_choiceCompress->GetSelection() );
186
187 int sepIdx = std::max( fn.Find( '/', true ), fn.Find( '\\', true ) );
188 int dotIdx = fn.Find( '.', true );
189
190 if( fileName.IsDir() )
191 fn = fn.Mid( 0, sepIdx );
192 else if( sepIdx < dotIdx )
193 fn = fn.Mid( 0, dotIdx );
194
195 switch( compressionMode )
196 {
198 fn = fn + '.' + FILEEXT::ArchiveFileExtension;
199 break;
201 fn += ".tgz";
202 break;
204 fn = wxFileName( fn, "" ).GetFullPath();
205 break;
206 default:
207 break;
208 };
209
210 m_outputFileName->SetValue( fn );
211}
212
213
214void DIALOG_EXPORT_ODBPP::onOKClick( wxCommandEvent& event )
215{
216 if( !m_job )
217 {
218 wxString fn = m_outputFileName->GetValue();
219
220 if( fn.IsEmpty() )
221 {
222 DisplayErrorMessage( this, _( "Output file name cannot be empty." ) );
223 return;
224 }
225
226 auto compressionMode = static_cast<JOB_EXPORT_PCB_ODB::ODB_COMPRESSION>( m_choiceCompress->GetSelection() );
227
228 wxFileName fileName( fn );
229 bool isDirectory = fileName.IsDir();
230 wxString extension = fileName.GetExt();
231
232 if( ( compressionMode == JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::NONE && !isDirectory )
233 || ( compressionMode == JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::ZIP && extension != "zip" )
234 || ( compressionMode == JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::TGZ && extension != "tgz" ) )
235 {
236 DisplayErrorMessage( this, _( "The output file name conflicts with the selected compression format." ) );
237 return;
238 }
239 }
240
241 event.Skip();
242}
243
244
246{
247 if( m_job )
248 {
249 m_job->SetConfiguredOutputPath( m_outputFileName->GetValue() );
250
251 m_job->m_precision = m_precision->GetValue();
252 m_job->m_units = static_cast<JOB_EXPORT_PCB_ODB::ODB_UNITS>( m_choiceUnits->GetSelection() );
253 m_job->m_compressionMode = static_cast<JOB_EXPORT_PCB_ODB::ODB_COMPRESSION>( m_choiceCompress->GetSelection() );
254 }
255
256 return true;
257}
258
259
261 PCB_EDIT_FRAME* aParentFrame, PROGRESS_REPORTER* aProgressReporter,
262 REPORTER* aReporter )
263{
264 LOCALE_IO toggle;
265
266 wxCHECK( aBoard, /* void */ );
267 wxString outputPath = aJob.GetFullOutputPath( aBoard->GetProject() );
268
269 if( outputPath.IsEmpty() )
270 outputPath = wxFileName( aJob.m_filename ).GetPath();
271
272 wxFileName outputFn( outputPath );
273
274 // Write through symlinks, don't replace them
276
277 if( outputFn.GetPath().IsEmpty() && outputFn.HasName() )
278 outputFn.MakeAbsolute();
279
280 bool outputIsSingleFile = aJob.m_compressionMode != JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::NONE;
281 wxString msg;
282
283 if( !PATHS::EnsurePathExists( outputFn.GetFullPath(), outputIsSingleFile ) )
284 {
285 msg.Printf( _( "Cannot create output directory '%s'." ), outputFn.GetFullPath() );
286
287 if( aReporter )
288 aReporter->Report( msg, RPT_SEVERITY_ERROR );
289
290 return;
291 }
292
293 if( outputFn.IsDir() && !outputFn.IsDirWritable() )
294 {
295 msg.Printf( _( "Insufficient permissions to folder '%s'." ), outputFn.GetPath() );
296
297 if( aReporter )
298 aReporter->Report( msg, RPT_SEVERITY_ERROR );
299
300 return;
301 }
302
303 if( outputIsSingleFile )
304 {
305 bool writeable = outputFn.FileExists() ? outputFn.IsFileWritable() : outputFn.IsDirWritable();
306
307 if( !writeable )
308 {
309 msg.Printf( _( "Insufficient permissions to save file '%s'." ), outputFn.GetFullPath() );
310
311 if( aReporter )
312 aReporter->Report( msg, RPT_SEVERITY_ERROR );
313
314 return;
315 }
316 }
317
318 wxFileName tempFile( outputFn.GetFullPath() );
319
320 if( outputIsSingleFile )
321 {
322 if( outputFn.Exists() )
323 {
324 if( aParentFrame )
325 {
326 msg = wxString::Format( _( "Output files '%s' already exists. Do you want to overwrite it?" ),
327 outputFn.GetFullPath() );
328
329 KIDIALOG errorDlg( aParentFrame, msg, _( "Confirmation" ), wxOK | wxCANCEL | wxICON_WARNING );
330 errorDlg.SetOKLabel( _( "Overwrite" ) );
331
332 if( errorDlg.ShowModal() != wxID_OK )
333 return;
334
335 if( !wxRemoveFile( outputFn.GetFullPath() ) )
336 {
337 msg.Printf( _( "Cannot remove existing output file '%s'." ), outputFn.GetFullPath() );
338 DisplayErrorMessage( aParentFrame, msg );
339 return;
340 }
341 }
342 }
343
344 tempFile.AssignDir( wxFileName::GetTempDir() );
345 tempFile.AppendDir( "kicad" );
346 tempFile.AppendDir( "odb" );
347
348 if( !wxFileName::Mkdir( tempFile.GetFullPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
349 {
350 msg.Printf( _( "Cannot create temporary output directory." ) );
351
352 if( aReporter )
353 aReporter->Report( msg, RPT_SEVERITY_ERROR );
354
355 return;
356 }
357 }
358 else
359 {
360 // Test for the output directory of tempFile
361 wxDir testDir( tempFile.GetFullPath() );
362
363 if( testDir.IsOpened() && ( testDir.HasFiles() || testDir.HasSubDirs() ) )
364 {
365 if( aParentFrame )
366 {
367 msg = wxString::Format( _( "Output directory '%s' already exists and is not empty. "
368 "Do you want to overwrite it?" ),
369 tempFile.GetFullPath() );
370
371 KIDIALOG errorDlg( aParentFrame, msg, _( "Confirmation" ), wxOK | wxCANCEL | wxICON_WARNING );
372 errorDlg.SetOKLabel( _( "Overwrite" ) );
373
374 if( errorDlg.ShowModal() != wxID_OK )
375 return;
376
377 if( !tempFile.Rmdir( wxPATH_RMDIR_RECURSIVE ) )
378 {
379 msg.Printf( _( "Cannot remove existing output directory '%s'." ), tempFile.GetFullPath() );
380 DisplayErrorMessage( aParentFrame, msg );
381 return;
382 }
383 }
384 }
385 }
386
387 std::map<std::string, UTF8> props;
388
389 props["units"] = aJob.m_units == JOB_EXPORT_PCB_ODB::ODB_UNITS::MM ? "mm" : "inch";
390 props["sigfig"] = wxString::Format( "%d", aJob.m_precision );
391
392 auto saveFile =
393 [&]() -> bool
394 {
395 try
396 {
398 pi->SetReporter( aReporter );
399 pi->SetProgressReporter( aProgressReporter );
400 pi->SaveBoard( tempFile.GetFullPath(), aBoard, &props );
401 return true;
402 }
403 catch( const IO_ERROR& ioe )
404 {
405 if( aReporter )
406 {
407 msg = wxString::Format( _( "Error generating ODBPP files '%s'.\n%s" ),
408 tempFile.GetFullPath(), ioe.What() );
409 aReporter->Report( msg, RPT_SEVERITY_ERROR );
410 }
411
412 // In case we started a file but didn't fully write it, clean up
413 wxFileName::Rmdir( tempFile.GetFullPath() );
414 return false;
415 }
416 };
417
419 auto ret = tp.submit_task( saveFile );
420
421 std::future_status status = ret.wait_for( std::chrono::milliseconds( 250 ) );
422
423 while( status != std::future_status::ready )
424 {
425 if( aProgressReporter )
426 aProgressReporter->KeepRefreshing();
427
428 status = ret.wait_for( std::chrono::milliseconds( 250 ) );
429 }
430
431 try
432 {
433 if( !ret.get() )
434 return;
435 }
436 catch( const std::exception& e )
437 {
438 if( aReporter )
439 {
440 aReporter->Report( wxString::Format( "Exception in ODB++ generation: %s", e.what() ),
442 }
443
444 return;
445 }
446
448 {
449 if( aProgressReporter )
450 aProgressReporter->AdvancePhase( _( "Compressing output" ) );
451
452 wxFFileOutputStream fnout( outputFn.GetFullPath() );
453
454 // Use a large I/O buffer to improve compatibility with cloud-synced folders.
455 // See KIPLATFORM::IO::CLOUD_SYNC_BUFFER_SIZE comment for details.
456 if( FILE* fp = fnout.GetFile()->fp() )
457 setvbuf( fp, nullptr, _IOFBF, KIPLATFORM::IO::CLOUD_SYNC_BUFFER_SIZE );
458
459 wxZipOutputStream zipStream( fnout );
460
461 std::function<void( const wxString&, const wxString& )> addDirToZip =
462 [&]( const wxString& dirPath, const wxString& parentPath )
463 {
464 wxDir dir( dirPath );
465 wxString fileName;
466
467 bool cont = dir.GetFirst( &fileName, wxEmptyString, wxDIR_DEFAULT );
468
469 while( cont )
470 {
471 wxFileName fileInZip( dirPath, fileName );
472 wxString relativePath = fileName;
473
474 if( !parentPath.IsEmpty() )
475 relativePath = parentPath + wxString( wxFileName::GetPathSeparator() ) + fileName;
476
477 if( wxFileName::DirExists( fileInZip.GetFullPath() ) )
478 {
479 zipStream.PutNextDirEntry( relativePath );
480 addDirToZip( fileInZip.GetFullPath(), relativePath );
481 }
482 else
483 {
484 wxFFileInputStream fileStream( fileInZip.GetFullPath() );
485 zipStream.PutNextEntry( relativePath );
486 fileStream.Read( zipStream );
487 }
488 cont = dir.GetNext( &fileName );
489 }
490 };
491
492 addDirToZip( tempFile.GetFullPath(), wxEmptyString );
493
494 zipStream.Close();
495 fnout.Close();
496
497 tempFile.Rmdir( wxPATH_RMDIR_RECURSIVE );
498 }
500 {
501 wxFFileOutputStream fnout( outputFn.GetFullPath() );
502
503 // Use a large I/O buffer to improve compatibility with cloud-synced folders.
504 // See KIPLATFORM::IO::CLOUD_SYNC_BUFFER_SIZE comment for details.
505 if( FILE* fp = fnout.GetFile()->fp() )
506 setvbuf( fp, nullptr, _IOFBF, KIPLATFORM::IO::CLOUD_SYNC_BUFFER_SIZE );
507
508 wxZlibOutputStream zlibStream( fnout, -1, wxZLIB_GZIP );
509 wxTarOutputStream tarStream( zlibStream );
510
511 std::function<void( const wxString&, const wxString& )> addDirToTar =
512 [&]( const wxString& dirPath, const wxString& parentPath )
513 {
514 wxDir dir( dirPath );
515 wxString fileName;
516
517 bool cont = dir.GetFirst( &fileName, wxEmptyString, wxDIR_DEFAULT );
518 while( cont )
519 {
520 wxFileName fileInTar( dirPath, fileName );
521 wxString relativePath = fileName;
522
523 if( !parentPath.IsEmpty() )
524 relativePath = parentPath + wxString( wxFileName::GetPathSeparator() ) + fileName;
525
526 if( wxFileName::DirExists( fileInTar.GetFullPath() ) )
527 {
528 tarStream.PutNextDirEntry( relativePath );
529 addDirToTar( fileInTar.GetFullPath(), relativePath );
530 }
531 else
532 {
533 wxFFileInputStream fileStream( fileInTar.GetFullPath() );
534 tarStream.PutNextEntry( relativePath, wxDateTime::Now(), fileStream.GetLength() );
535 fileStream.Read( tarStream );
536 }
537 cont = dir.GetNext( &fileName );
538 }
539 };
540
541 addDirToTar( tempFile.GetFullPath(),
542 tempFile.GetPath( wxPATH_NO_SEPARATOR ).AfterLast( tempFile.GetPathSeparator() ) );
543
544 tarStream.Close();
545 zlibStream.Close();
546 fnout.Close();
547
548 tempFile.Rmdir( wxPATH_RMDIR_RECURSIVE );
549 }
550
551 if( aProgressReporter )
552 aProgressReporter->SetCurrentProgress( 1 );
553}
wxBitmapBundle KiBitmapBundle(BITMAPS aBitmap, int aMinHeight)
Definition bitmap.cpp:110
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:323
PROJECT * GetProject() const
Definition board.h:587
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.
Instantiate the current locale within a scope in which you are expecting exceptions to be thrown.
Definition locale_io.h:41
static bool EnsurePathExists(const wxString &aPath, bool aPathToFile=false)
Attempts to create a given path if it does not exist.
Definition paths.cpp:518
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.