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