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 <wx/zipstrm.h>
30#include <wx/tarstrm.h>
31#include <wx/zstream.h>
32
33#include <board.h>
34#include <confirm.h>
35#include <footprint.h>
36#include <kidialog.h>
37#include <kiway_holder.h>
38#include <paths.h>
39#include <pcb_edit_frame.h>
40#include <pcbnew_settings.h>
41#include <pgm_base.h>
42#include <progress_reporter.h>
43#include <project.h>
46#include <string_utils.h>
48#include <io/io_mgr.h>
50#include <pcb_io/pcb_io_mgr.h>
51
52
53
55 DIALOG_EXPORT_ODBPP_BASE( aParent ),
56 m_parent( aParent ),
57 m_job( nullptr )
58{
60
62
63 // DIALOG_SHIM needs a unique hash_key because classname will be the same for both job and
64 // non-job versions.
65 m_hash_key = TO_UTF8( GetTitle() );
66
67 // Now all widgets have the size fixed, call FinishDialogSettings
69}
70
71
73 wxWindow* aParent ) :
74 DIALOG_EXPORT_ODBPP_BASE( aParent ),
75 m_parent( aEditFrame ),
76 m_job( aJob )
77{
78 m_browseButton->Hide();
79
81
82 // DIALOG_SHIM needs a unique hash_key because classname will be the same for both job and
83 // non-job versions.
84 m_hash_key = TO_UTF8( GetTitle() );
85
86 // Now all widgets have the size fixed, call FinishDialogSettings
88}
89
90
92{
93 if( !m_job )
94 {
95 if( m_outputFileName->GetValue().IsEmpty() )
96 {
97 wxFileName brdFile( m_parent->GetBoard()->GetFileName() );
98 wxFileName odbFile( brdFile.GetPath(), wxString::Format( wxS( "%s-odb" ), brdFile.GetName() ),
100
101 m_outputFileName->SetValue( odbFile.GetFullPath() );
103 }
104 }
105 else
106 {
107 SetTitle( m_job->GetSettingsDialogTitle() );
108
109 m_choiceUnits->SetSelection( static_cast<int>( m_job->m_units ) );
110 m_precision->SetValue( m_job->m_precision );
111 m_choiceCompress->SetSelection( static_cast<int>( m_job->m_compressionMode ) );
112 m_outputFileName->SetValue( m_job->GetConfiguredOutputPath() );
113 }
114
115 return true;
116}
117
118
119void DIALOG_EXPORT_ODBPP::onBrowseClicked( wxCommandEvent& event )
120{
121 // clang-format off
122 wxString filter = _( "zip files" )
124 + _( "tgz files" )
125 + AddFileExtListToFilter( { "tgz" } );
126 // clang-format on
127
128 // Build the absolute path of current output directory to preselect it in the file browser.
129 wxString path = ExpandEnvVarSubstitutions( m_outputFileName->GetValue(), &Prj() );
130 wxFileName fn( Prj().AbsolutePath( path ) );
131
132 wxFileName brdFile( m_parent->GetBoard()->GetFileName() );
133
134 wxString fileDialogName( wxString::Format( wxS( "%s-odb" ), brdFile.GetName() ) );
135
136 wxFileDialog dlg( this, _( "Export ODB++ File" ), fn.GetPath(), fileDialogName, filter, wxFD_SAVE );
137
138 if( dlg.ShowModal() == wxID_CANCEL )
139 return;
140
141 path = dlg.GetPath();
142
143 fn = wxFileName( path );
144
145 if( fn.GetExt().Lower() == "zip" )
146 {
147 m_choiceCompress->SetSelection( static_cast<int>( JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::ZIP ) );
148 }
149 else if( fn.GetExt().Lower() == "tgz" )
150 {
151 m_choiceCompress->SetSelection( static_cast<int>( JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::TGZ ) );
152 }
153 else if( path.EndsWith( "/" ) || path.EndsWith( "\\" ) )
154 {
155 m_choiceCompress->SetSelection( static_cast<int>( JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::NONE ) );
156 }
157 else
158 {
159 DisplayErrorMessage( this, _( "The selected output file name is not a supported archive format." ) );
160 return;
161 }
162
163 m_outputFileName->SetValue( path );
164}
165
166
167void DIALOG_EXPORT_ODBPP::onFormatChoice( wxCommandEvent& event )
168{
170}
171
172
174{
175 wxString fn = m_outputFileName->GetValue();
176
177 wxFileName fileName( fn );
178
179 auto compressionMode = static_cast<JOB_EXPORT_PCB_ODB::ODB_COMPRESSION>( m_choiceCompress->GetSelection() );
180
181 int sepIdx = std::max( fn.Find( '/', true ), fn.Find( '\\', true ) );
182 int dotIdx = fn.Find( '.', true );
183
184 if( fileName.IsDir() )
185 fn = fn.Mid( 0, sepIdx );
186 else if( sepIdx < dotIdx )
187 fn = fn.Mid( 0, dotIdx );
188
189 switch( compressionMode )
190 {
192 fn = fn + '.' + FILEEXT::ArchiveFileExtension;
193 break;
195 fn += ".tgz";
196 break;
198 fn = wxFileName( fn, "" ).GetFullPath();
199 break;
200 default:
201 break;
202 };
203
204 m_outputFileName->SetValue( fn );
205}
206
207void DIALOG_EXPORT_ODBPP::onOKClick( wxCommandEvent& event )
208{
209 if( !m_job )
210 {
211 wxString fn = m_outputFileName->GetValue();
212
213 if( fn.IsEmpty() )
214 {
215 DisplayErrorMessage( this, _( "Output file name cannot be empty." ) );
216 return;
217 }
218
219 auto compressionMode = static_cast<JOB_EXPORT_PCB_ODB::ODB_COMPRESSION>( m_choiceCompress->GetSelection() );
220
221 wxFileName fileName( fn );
222 bool isDirectory = fileName.IsDir();
223 wxString extension = fileName.GetExt();
224
225 if( ( compressionMode == JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::NONE && !isDirectory )
226 || ( compressionMode == JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::ZIP && extension != "zip" )
227 || ( compressionMode == JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::TGZ && extension != "tgz" ) )
228 {
229 DisplayErrorMessage( this, _( "The output file name conflicts with the selected compression format." ) );
230 return;
231 }
232 }
233
234 event.Skip();
235}
236
237
239{
240 if( m_job )
241 {
242 m_job->SetConfiguredOutputPath( m_outputFileName->GetValue() );
243
244 m_job->m_precision = m_precision->GetValue();
245 m_job->m_units = static_cast<JOB_EXPORT_PCB_ODB::ODB_UNITS>( m_choiceUnits->GetSelection() );
246 m_job->m_compressionMode = static_cast<JOB_EXPORT_PCB_ODB::ODB_COMPRESSION>( m_choiceCompress->GetSelection() );
247 }
248
249 return true;
250}
251
252
254 PCB_EDIT_FRAME* aParentFrame, PROGRESS_REPORTER* aProgressReporter,
255 REPORTER* aReporter )
256{
257 wxCHECK( aBoard, /* void */ );
258 wxString outputPath = aJob.GetFullOutputPath( aBoard->GetProject() );
259
260 if( outputPath.IsEmpty() )
261 outputPath = wxFileName( aJob.m_filename ).GetPath();
262
263 wxFileName outputFn( outputPath );
264
265 // Write through symlinks, don't replace them
267
268 if( outputFn.GetPath().IsEmpty() && outputFn.HasName() )
269 outputFn.MakeAbsolute();
270
271 bool outputIsSingleFile = aJob.m_compressionMode != JOB_EXPORT_PCB_ODB::ODB_COMPRESSION::NONE;
272 wxString msg;
273
274 if( !PATHS::EnsurePathExists( outputFn.GetFullPath(), outputIsSingleFile ) )
275 {
276 msg.Printf( _( "Cannot create output directory '%s'." ), outputFn.GetFullPath() );
277
278 if( aReporter )
279 aReporter->Report( msg, RPT_SEVERITY_ERROR );
280
281 return;
282 }
283
284 if( outputFn.IsDir() && !outputFn.IsDirWritable() )
285 {
286 msg.Printf( _( "Insufficient permissions to folder '%s'." ), outputFn.GetPath() );
287
288 if( aReporter )
289 aReporter->Report( msg, RPT_SEVERITY_ERROR );
290
291 return;
292 }
293
294 if( outputIsSingleFile )
295 {
296 bool writeable = outputFn.FileExists() ? outputFn.IsFileWritable() : outputFn.IsDirWritable();
297
298 if( !writeable )
299 {
300 msg.Printf( _( "Insufficient permissions to save file '%s'." ), outputFn.GetFullPath() );
301
302 if( aReporter )
303 aReporter->Report( msg, RPT_SEVERITY_ERROR );
304
305 return;
306 }
307 }
308
309 wxFileName tempFile( outputFn.GetFullPath() );
310
311 if( outputIsSingleFile )
312 {
313 if( outputFn.Exists() )
314 {
315 if( aParentFrame )
316 {
317 msg = wxString::Format( _( "Output files '%s' already exists. Do you want to overwrite it?" ),
318 outputFn.GetFullPath() );
319
320 KIDIALOG errorDlg( aParentFrame, msg, _( "Confirmation" ), wxOK | wxCANCEL | wxICON_WARNING );
321 errorDlg.SetOKLabel( _( "Overwrite" ) );
322
323 if( errorDlg.ShowModal() != wxID_OK )
324 return;
325
326 if( !wxRemoveFile( outputFn.GetFullPath() ) )
327 {
328 msg.Printf( _( "Cannot remove existing output file '%s'." ), outputFn.GetFullPath() );
329 DisplayErrorMessage( aParentFrame, msg );
330 return;
331 }
332 }
333 else
334 {
335 msg = wxString::Format( _( "Output file '%s' already exists." ), outputFn.GetFullPath() );
336
337 if( aReporter )
338 aReporter->Report( msg, RPT_SEVERITY_ERROR );
339
340 return;
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 else
385 {
386 msg = wxString::Format( _( "Output directory '%s' already exists." ), tempFile.GetFullPath() );
387
388 if( aReporter )
389 aReporter->Report( msg, RPT_SEVERITY_ERROR );
390
391 return;
392 }
393 }
394 }
395
396 std::map<std::string, UTF8> props;
397
398 props["units"] = aJob.m_units == JOB_EXPORT_PCB_ODB::ODB_UNITS::MM ? "mm" : "inch";
399 props["sigfig"] = wxString::Format( "%d", aJob.m_precision );
400
401 auto saveFile =
402 [&]() -> bool
403 {
404 try
405 {
407 pi->SetReporter( aReporter );
408 pi->SetProgressReporter( aProgressReporter );
409 pi->SaveBoard( tempFile.GetFullPath(), aBoard, &props );
410 return true;
411 }
412 catch( const IO_ERROR& ioe )
413 {
414 if( aReporter )
415 {
416 msg = wxString::Format( _( "Error generating ODBPP files '%s'.\n%s" ),
417 tempFile.GetFullPath(), ioe.What() );
418 aReporter->Report( msg, RPT_SEVERITY_ERROR );
419 }
420
421 // In case we started a file but didn't fully write it, clean up
422 wxFileName::Rmdir( tempFile.GetFullPath() );
423 return false;
424 }
425 };
426
428 auto ret = tp.submit_task( saveFile );
429
430 std::future_status status = ret.wait_for( std::chrono::milliseconds( 250 ) );
431
432 while( status != std::future_status::ready )
433 {
434 if( aProgressReporter )
435 aProgressReporter->KeepRefreshing();
436
437 status = ret.wait_for( std::chrono::milliseconds( 250 ) );
438 }
439
440 try
441 {
442 if( !ret.get() )
443 return;
444 }
445 catch( const std::exception& e )
446 {
447 if( aReporter )
448 {
449 aReporter->Report( wxString::Format( "Exception in ODB++ generation: %s", e.what() ),
451 }
452
453 return;
454 }
455
457 {
458 if( aProgressReporter )
459 aProgressReporter->AdvancePhase( _( "Compressing output" ) );
460
461 wxFFileOutputStream fnout( outputFn.GetFullPath() );
462 wxZipOutputStream zipStream( fnout );
463
464 std::function<void( const wxString&, const wxString& )> addDirToZip =
465 [&]( const wxString& dirPath, const wxString& parentPath )
466 {
467 wxDir dir( dirPath );
468 wxString fileName;
469
470 bool cont = dir.GetFirst( &fileName, wxEmptyString, wxDIR_DEFAULT );
471
472 while( cont )
473 {
474 wxFileName fileInZip( dirPath, fileName );
475 wxString relativePath = fileName;
476
477 if( !parentPath.IsEmpty() )
478 relativePath = parentPath + wxString( wxFileName::GetPathSeparator() ) + fileName;
479
480 if( wxFileName::DirExists( fileInZip.GetFullPath() ) )
481 {
482 zipStream.PutNextDirEntry( relativePath );
483 addDirToZip( fileInZip.GetFullPath(), relativePath );
484 }
485 else
486 {
487 wxFFileInputStream fileStream( fileInZip.GetFullPath() );
488 zipStream.PutNextEntry( relativePath );
489 fileStream.Read( zipStream );
490 }
491 cont = dir.GetNext( &fileName );
492 }
493 };
494
495 addDirToZip( tempFile.GetFullPath(), wxEmptyString );
496
497 zipStream.Close();
498 fnout.Close();
499
500 tempFile.Rmdir( wxPATH_RMDIR_RECURSIVE );
501 }
503 {
504 wxFFileOutputStream fnout( outputFn.GetFullPath() );
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:317
PROJECT * GetProject() const
Definition board.h:537
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:100
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:477
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.
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: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
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.
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
static thread_pool * tp
BS::thread_pool< 0 > 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.