1 /*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2019 Jean-Pierre Charras, jp.charras at wanadoo.fr
5 * Copyright (C) 1992-2021 KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, you may find one here:
19 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
20 * or you may search the http://www.gnu.org website for the version 2 license,
21 * or you may write to the Free Software Foundation, Inc.,
22 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23 */
24
25 /**
26 * @file eeschema/dialogs/dialog_bom.cpp
27 * @brief Dialog box for creating bom and other documents from generic netlist.
28 */
29
30
31 #include <bitmaps.h>
32 #include <bom_plugins.h>
33 #include <confirm.h>
34 #include <dialog_bom_base.h>
35 #include <string_utils.h>
36 #include <eeschema_settings.h>
37 #include <gestfich.h>
38 #include <dialogs/html_message_box.h>
39 #include <i18n_utility.h> // for _HKI definition used in dialog_bom_help_md.h
40 #include <invoke_sch_dialog.h>
41 #include <kiface_base.h>
42 #include <netlist_exporter_xml.h>
43 #include <pgm_base.h>
44 #include <reporter.h>
45 #include <sch_edit_frame.h>
46 #include <paths.h>
47
48 #include <wx/filedlg.h>
49 #include <wx/log.h>
50 #include <wx/textdlg.h>
51
52 wxString s_bomHelpInfo =
53 #include <dialog_bom_help_md.h>
54 ;
55
56 // BOM "plugins" are not actually plugins. They are external tools
57 // (scripts or executables) called by this dialog.
58 typedef std::vector< std::unique_ptr<BOM_GENERATOR_HANDLER> > BOM_GENERATOR_ARRAY;
59
60
61 // The main dialog frame to run scripts to build bom
62 class DIALOG_BOM : public DIALOG_BOM_BASE
63 {
64 private:
65 SCH_EDIT_FRAME* m_parent;
66 BOM_GENERATOR_ARRAY m_generators;
67 bool m_initialized;
68
69 HTML_MESSAGE_BOX* m_helpWindow;
70
71 public:
72 DIALOG_BOM( SCH_EDIT_FRAME* parent );
73 ~DIALOG_BOM();
74
75 private:
76 void OnGeneratorSelected( wxCommandEvent& event ) override;
77 void OnRunGenerator( wxCommandEvent& event ) override;
78 void OnHelp( wxCommandEvent& event ) override;
79 void OnAddGenerator( wxCommandEvent& event ) override;
80 void OnRemoveGenerator( wxCommandEvent& event ) override;
81 void OnEditGenerator( wxCommandEvent& event ) override;
82 void OnCommandLineEdited( wxCommandEvent& event ) override;
83 void OnNameEdited( wxCommandEvent& event ) override;
84 void OnShowConsoleChanged( wxCommandEvent& event ) override;
85 void OnIdle( wxIdleEvent& event ) override;
86
87 void pluginInit();
88 void installGeneratorsList();
89 BOM_GENERATOR_HANDLER* addGenerator( const wxString& aPath,
90 const wxString& aName = wxEmptyString );
91 bool pluginExists( const wxString& aName );
92
selectedGenerator()93 BOM_GENERATOR_HANDLER* selectedGenerator()
94 {
95 int idx = m_lbGenerators->GetSelection();
96
97 if( idx < 0 || idx >= (int)m_generators.size() )
98 return nullptr;
99
100 return m_generators[idx].get();
101 }
102
103 wxString chooseGenerator();
104 };
105
106
107 // Create and show DIALOG_BOM.
InvokeDialogCreateBOM(SCH_EDIT_FRAME * aCaller)108 int InvokeDialogCreateBOM( SCH_EDIT_FRAME* aCaller )
109 {
110 DIALOG_BOM dlg( aCaller );
111
112 // QuasiModal so syntax help works
113 return dlg.ShowQuasiModal();
114 }
115
116
DIALOG_BOM(SCH_EDIT_FRAME * parent)117 DIALOG_BOM::DIALOG_BOM( SCH_EDIT_FRAME* parent ) :
118 DIALOG_BOM_BASE( parent ),
119 m_parent( parent ),
120 m_initialized( false ),
121 m_helpWindow( nullptr )
122 {
123 m_buttonAddGenerator->SetBitmap( KiBitmap( BITMAPS::small_plus ) );
124 m_buttonDelGenerator->SetBitmap( KiBitmap( BITMAPS::small_trash ) );
125 m_buttonEdit->SetBitmap( KiBitmap( BITMAPS::small_edit ) );
126
127 installGeneratorsList();
128
129 #ifndef __WINDOWS__
130 m_checkBoxShowConsole->Show( false );
131 #endif
132
133 m_sdbSizerOK->SetLabel( _( "Generate" ) );
134 m_sdbSizerCancel->SetLabel( _( "Close" ) );
135 m_sdbSizer->Layout();
136
137 SetInitialFocus( m_lbGenerators );
138 m_sdbSizerOK->SetDefault();
139
140 // Now all widgets have the size fixed, call FinishDialogSettings
141 finishDialogSettings();
142
143 m_buttonReset->Bind( wxEVT_BUTTON,
144 [&]( wxCommandEvent& )
145 {
146 EESCHEMA_SETTINGS* cfg = m_parent->eeconfig();
147
148 cfg->m_BomPanel.selected_plugin = wxEmptyString;
149 cfg->m_BomPanel.plugins = cfg->DefaultBomPlugins();
150
151 installGeneratorsList();
152 } );
153 }
154
155
~DIALOG_BOM()156 DIALOG_BOM::~DIALOG_BOM()
157 {
158 if( m_helpWindow )
159 m_helpWindow->Destroy();
160
161 EESCHEMA_SETTINGS* cfg = m_parent->eeconfig();
162
163 cfg->m_BomPanel.plugins.clear();
164
165 for( const std::unique_ptr<BOM_GENERATOR_HANDLER>& plugin : m_generators )
166 {
167 wxString name = plugin->GetName();
168 wxFileName path( plugin->GetStoredPath() );
169
170 // handle empty nickname by stripping path
171 if( name.IsEmpty() )
172 name = path.GetName();
173
174 EESCHEMA_SETTINGS::BOM_PLUGIN_SETTINGS setting( name, path.GetFullPath() );
175 setting.command = plugin->GetCommand();
176
177 cfg->m_BomPanel.plugins.emplace_back( setting );
178 }
179
180 cfg->m_BomPanel.selected_plugin = m_lbGenerators->GetStringSelection().ToStdString();
181 }
182
183
184 // Read the initialized plugins in config and fill the list of names
installGeneratorsList()185 void DIALOG_BOM::installGeneratorsList()
186 {
187 EESCHEMA_SETTINGS* cfg = m_parent->eeconfig();
188
189 wxString active_plugin_name = cfg->m_BomPanel.selected_plugin;
190
191 m_generators.clear();
192
193 for( EESCHEMA_SETTINGS::BOM_PLUGIN_SETTINGS& setting : cfg->m_BomPanel.plugins )
194 {
195 auto plugin = std::make_unique<BOM_GENERATOR_HANDLER>( setting.path );
196
197 plugin->SetName( setting.name );
198
199 if( !setting.command.IsEmpty() )
200 plugin->SetCommand( setting.command );
201
202 m_generators.emplace_back( std::move( plugin ) );
203 }
204
205 m_lbGenerators->Clear();
206
207 if( !m_generators.empty() )
208 {
209 for( unsigned ii = 0; ii < m_generators.size(); ii++ )
210 {
211 wxString name = m_generators[ii]->GetName();
212
213 if( !m_generators[ii]->FindFilePath().Exists( wxFILE_EXISTS_REGULAR ) )
214 {
215 wxLogTrace( BOM_TRACE, "BOM plugin %s not found",
216 m_generators[ii]->FindFilePath().GetFullName() );
217 name.Append( wxT( " " ) + _( "(file missing)" ) );
218
219 if( active_plugin_name == name )
220 active_plugin_name.Clear();
221 }
222
223 m_lbGenerators->Append( name );
224
225 if( active_plugin_name == name )
226 m_lbGenerators->SetSelection( ii );
227 }
228 }
229
230 pluginInit();
231 }
232
233
addGenerator(const wxString & aPath,const wxString & aName)234 BOM_GENERATOR_HANDLER* DIALOG_BOM::addGenerator( const wxString& aPath, const wxString& aName )
235 {
236 BOM_GENERATOR_HANDLER* ret = nullptr;
237 auto plugin = std::make_unique<BOM_GENERATOR_HANDLER>( aPath );
238
239 if( !plugin )
240 return nullptr;
241
242 if( !aName.IsEmpty() )
243 {
244 plugin->SetName( aName );
245 m_lbGenerators->Append( aName );
246 }
247 else
248 {
249 m_lbGenerators->Append( plugin->GetName() );
250 }
251
252 ret = plugin.get();
253 m_generators.push_back( std::move( plugin ) );
254 return ret;
255 }
256
257
pluginExists(const wxString & aName)258 bool DIALOG_BOM::pluginExists( const wxString& aName )
259 {
260 for( unsigned ii = 0; ii < m_generators.size(); ii++ )
261 {
262 if( aName == m_generators[ii]->GetName() )
263 return true;
264 }
265
266 return false;
267 }
268
269
OnGeneratorSelected(wxCommandEvent & event)270 void DIALOG_BOM::OnGeneratorSelected( wxCommandEvent& event )
271 {
272 pluginInit();
273 }
274
275
pluginInit()276 void DIALOG_BOM::pluginInit()
277 {
278 BOM_GENERATOR_HANDLER* plugin = selectedGenerator();
279
280 if( !plugin )
281 {
282 m_textCtrlName->SetValue( wxEmptyString );
283 m_textCtrlCommand->SetValue( wxEmptyString );
284 m_Messages->SetValue( wxEmptyString );
285 return;
286 }
287
288 if( !plugin->FindFilePath().Exists( wxFILE_EXISTS_REGULAR ) )
289 {
290 m_textCtrlName->SetValue( wxEmptyString );
291 m_textCtrlCommand->SetValue( wxEmptyString );
292
293 wxString msg =
294 wxString::Format( _( "The selected BOM generator script %s could not be found." ),
295 plugin->GetFile().GetFullPath() );
296
297 if( !plugin->GetFile().IsAbsolute() )
298 {
299 msg.Append( wxString::Format( _( "\n\nSearched:\n\t%s\n\t%s" ),
300 PATHS::GetUserPluginsPath(),
301 PATHS::GetStockPluginsPath() ) );
302 }
303
304 m_Messages->SetValue( msg );
305 return;
306 }
307
308 m_textCtrlName->SetValue( plugin->GetName() );
309 m_textCtrlCommand->SetValue( plugin->GetCommand() );
310 m_Messages->SetValue( plugin->GetInfo() );
311 m_Messages->SetSelection( 0, 0 );
312
313 #ifdef __WINDOWS__
314 if( plugin->Options().Index( wxT( "show_console" ) ) == wxNOT_FOUND )
315 m_checkBoxShowConsole->SetValue( false );
316 else
317 m_checkBoxShowConsole->SetValue( true );
318 #endif
319
320 // A plugin can be not working, so do not left the OK button enabled if
321 // the plugin is not ready to use
322 m_sdbSizerOK->Enable( plugin->IsOk() );
323 }
324
325
OnRunGenerator(wxCommandEvent & event)326 void DIALOG_BOM::OnRunGenerator( wxCommandEvent& event )
327 {
328 // Calculate the xml netlist filename
329 wxFileName fn = m_parent->Schematic().GetFileName();
330
331 fn.ClearExt();
332
333 wxString fullfilename = fn.GetFullPath();
334 m_parent->ClearMsgPanel();
335
336 wxString reportmsg;
337 WX_STRING_REPORTER reporter( &reportmsg );
338 m_parent->SetNetListerCommand( m_textCtrlCommand->GetValue() );
339
340 #ifdef __WINDOWS__
341 if( m_checkBoxShowConsole->IsChecked() )
342 m_parent->SetExecFlags( wxEXEC_SHOW_CONSOLE );
343 #endif
344
345 if( m_parent->ReadyToNetlist( _( "Generating BOM requires a fully annotated schematic." ) ) )
346 m_parent->WriteNetListFile( -1, fullfilename, GNL_OPT_BOM, &reporter );
347
348 m_Messages->SetValue( reportmsg );
349
350 // Force focus back on the dialog
351 SetFocus();
352 }
353
354
OnRemoveGenerator(wxCommandEvent & event)355 void DIALOG_BOM::OnRemoveGenerator( wxCommandEvent& event )
356 {
357 int ii = m_lbGenerators->GetSelection();
358
359 if( ii < 0 )
360 return;
361
362 m_lbGenerators->Delete( ii );
363 m_generators.erase( m_generators.begin() + ii );
364
365 // Select the next item, if exists
366 if( m_lbGenerators->GetCount() )
367 m_lbGenerators->SetSelection( std::min( ii, (int) m_lbGenerators->GetCount() - 1 ) );
368
369 pluginInit();
370 }
371
372
OnAddGenerator(wxCommandEvent & event)373 void DIALOG_BOM::OnAddGenerator( wxCommandEvent& event )
374 {
375 wxString filename = chooseGenerator();
376
377 if( filename.IsEmpty() )
378 return;
379
380 // Creates a new plugin entry
381 wxFileName fn( filename );
382 wxString name = wxGetTextFromUser( _( "Generator nickname:" ), _( "Add Generator" ),
383 fn.GetName(), this );
384
385 if( name.IsEmpty() )
386 return;
387
388 // Verify if it does not exists
389 if( pluginExists( name ) )
390 {
391 wxMessageBox( wxString::Format( _( "Nickname '%s' already in use." ), name ) );
392 return;
393 }
394
395 try
396 {
397 auto plugin = addGenerator( fn.GetFullPath(), name );
398
399 if( plugin )
400 {
401 m_lbGenerators->SetSelection( m_lbGenerators->GetCount() - 1 );
402 m_textCtrlCommand->SetValue( plugin->GetCommand() );
403 pluginInit();
404 }
405 }
406 catch( const std::runtime_error& e )
407 {
408 DisplayError( this, e.what() );
409 }
410 }
411
412
chooseGenerator()413 wxString DIALOG_BOM::chooseGenerator()
414 {
415 static wxString lastPath;
416
417 if( lastPath.IsEmpty() )
418 lastPath = PATHS::GetUserPluginsPath();
419
420 wxString fullFileName = wxFileSelector( _( "Generator File" ), lastPath, wxEmptyString,
421 wxEmptyString, wxFileSelectorDefaultWildcardStr,
422 wxFD_OPEN, this );
423
424 return fullFileName;
425 }
426
427
OnEditGenerator(wxCommandEvent & event)428 void DIALOG_BOM::OnEditGenerator( wxCommandEvent& event )
429 {
430 auto plugin = selectedGenerator();
431
432 if( !plugin )
433 return;
434
435 wxString pluginFile = plugin->GetFile().GetFullPath();
436
437 if( pluginFile.Length() <= 2 ) // if name != ""
438 {
439 wxMessageBox( _( "Generator file name not found." ) );
440 return;
441 }
442
443 wxString editorname = Pgm().GetTextEditor();
444
445 if( !editorname.IsEmpty() )
446 ExecuteFile( editorname, pluginFile );
447 else
448 wxMessageBox( _( "No text editor selected in KiCad. Please choose one." ) );
449 }
450
451
OnHelp(wxCommandEvent & event)452 void DIALOG_BOM::OnHelp( wxCommandEvent& event )
453 {
454 if( m_helpWindow )
455 {
456 m_helpWindow->ShowModeless();
457 return;
458 }
459
460 m_helpWindow = new HTML_MESSAGE_BOX( nullptr, _( "Bill of Material Generation Help" ) );
461 m_helpWindow->SetDialogSizeInDU( 500, 350 );
462
463 wxString html_txt;
464 ConvertMarkdown2Html( wxGetTranslation( s_bomHelpInfo ), html_txt );
465
466 m_helpWindow->AddHTML_Text( html_txt );
467 m_helpWindow->ShowModeless();
468 }
469
470
OnCommandLineEdited(wxCommandEvent & event)471 void DIALOG_BOM::OnCommandLineEdited( wxCommandEvent& event )
472 {
473 auto generator = selectedGenerator();
474
475 if( generator )
476 generator->SetCommand( m_textCtrlCommand->GetValue() );
477 }
478
479
OnNameEdited(wxCommandEvent & event)480 void DIALOG_BOM::OnNameEdited( wxCommandEvent& event )
481 {
482 if( m_textCtrlName->GetValue().IsEmpty() )
483 return;
484
485 int ii = m_lbGenerators->GetSelection();
486
487 if( ii < 0 )
488 return;
489
490 m_generators[ii]->SetName( m_textCtrlName->GetValue() );
491 m_lbGenerators->SetString( ii, m_generators[ii]->GetName() );
492 }
493
494
OnShowConsoleChanged(wxCommandEvent & event)495 void DIALOG_BOM::OnShowConsoleChanged( wxCommandEvent& event )
496 {
497 #ifdef __WINDOWS__
498 static constexpr wxChar OPT_SHOW_CONSOLE[] = wxT( "show_console" );
499
500 auto plugin = selectedGenerator();
501
502 if( !plugin )
503 return;
504
505 if( m_checkBoxShowConsole->IsChecked() )
506 {
507 if( plugin->Options().Index( OPT_SHOW_CONSOLE ) == wxNOT_FOUND )
508 plugin->Options().Add( OPT_SHOW_CONSOLE );
509 }
510 else
511 {
512 plugin->Options().Remove( OPT_SHOW_CONSOLE );
513 }
514 #endif
515 }
516
517
OnIdle(wxIdleEvent & event)518 void DIALOG_BOM::OnIdle( wxIdleEvent& event )
519 {
520 // On some platforms we initialize wxTextCtrls to all-selected, but we don't want that
521 // for the messages text box.
522 if( !m_initialized )
523 {
524 m_Messages->SetSelection( 0, 0 );
525 m_initialized = true;
526 }
527 }
528