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