1 /*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2017 Jean-Pierre Charras, jp.charras at wanadoo.fr
5 * Copyright (C) 2004-2019 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 #include <wx/filedlg.h>
26 #include <wx/wfstream.h>
27 #include <wx/zipstrm.h>
28 #include <reporter.h>
29 #include <dialogs/html_message_box.h>
30 #include <gerbview_frame.h>
31 #include <gerbview_id.h>
32 #include <gerber_file_image.h>
33 #include <gerber_file_image_list.h>
34 #include <excellon_image.h>
35 #include <wildcards_and_files_ext.h>
36 #include <view/view.h>
37 #include <widgets/wx_progress_reporters.h>
38 #include "widgets/gerbview_layer_widget.h"
39
40 // HTML Messages used more than one time:
41 #define MSG_NO_MORE_LAYER _( "<b>No more available layers</b> in GerbView to load files" )
42 #define MSG_NOT_LOADED _( "\n<b>Not loaded:</b> <i>%s</i>" )
43 #define MSG_OOM _( "\n<b>Memory was exhausted reading:</b> <i>%s</i>" )
44
45
OnGbrFileHistory(wxCommandEvent & event)46 void GERBVIEW_FRAME::OnGbrFileHistory( wxCommandEvent& event )
47 {
48 wxString fn;
49
50 fn = GetFileFromHistory( event.GetId(), _( "Gerber files" ) );
51
52 if( !fn.IsEmpty() )
53 {
54 Erase_Current_DrawLayer( false );
55 LoadGerberFiles( fn );
56 }
57 }
58
OnClearGbrFileHistory(wxCommandEvent & aEvent)59 void GERBVIEW_FRAME::OnClearGbrFileHistory( wxCommandEvent& aEvent )
60 {
61 ClearFileHistory();
62 }
63
64
OnDrlFileHistory(wxCommandEvent & event)65 void GERBVIEW_FRAME::OnDrlFileHistory( wxCommandEvent& event )
66 {
67 wxString fn;
68
69 fn = GetFileFromHistory( event.GetId(), _( "Drill files" ), &m_drillFileHistory );
70
71 if( !fn.IsEmpty() )
72 {
73 Erase_Current_DrawLayer( false );
74 LoadExcellonFiles( fn );
75 }
76 }
77
78
OnClearDrlFileHistory(wxCommandEvent & aEvent)79 void GERBVIEW_FRAME::OnClearDrlFileHistory( wxCommandEvent& aEvent )
80 {
81 ClearFileHistory( &m_drillFileHistory );
82 }
83
84
OnZipFileHistory(wxCommandEvent & event)85 void GERBVIEW_FRAME::OnZipFileHistory( wxCommandEvent& event )
86 {
87 wxString filename;
88 filename = GetFileFromHistory( event.GetId(), _( "Zip files" ), &m_zipFileHistory );
89
90 if( !filename.IsEmpty() )
91 {
92 Erase_Current_DrawLayer( false );
93 LoadZipArchiveFile( filename );
94 }
95 }
96
97
OnClearZipFileHistory(wxCommandEvent & aEvent)98 void GERBVIEW_FRAME::OnClearZipFileHistory( wxCommandEvent& aEvent )
99 {
100 ClearFileHistory( &m_zipFileHistory );
101 }
102
103
OnJobFileHistory(wxCommandEvent & event)104 void GERBVIEW_FRAME::OnJobFileHistory( wxCommandEvent& event )
105 {
106 wxString filename = GetFileFromHistory( event.GetId(), _( "Job files" ), &m_jobFileHistory );
107
108 if( !filename.IsEmpty() )
109 LoadGerberJobFile( filename );
110 }
111
112
OnClearJobFileHistory(wxCommandEvent & aEvent)113 void GERBVIEW_FRAME::OnClearJobFileHistory( wxCommandEvent& aEvent )
114 {
115 ClearFileHistory( &m_jobFileHistory );
116 }
117
118
LoadGerberFiles(const wxString & aFullFileName)119 bool GERBVIEW_FRAME::LoadGerberFiles( const wxString& aFullFileName )
120 {
121 static int lastGerberFileWildcard = 0;
122 wxString filetypes;
123 wxArrayString filenamesList;
124 wxFileName filename = aFullFileName;
125 wxString currentPath;
126
127 if( !filename.IsOk() )
128 {
129 /* Standard gerber filetypes
130 * (See http://en.wikipedia.org/wiki/Gerber_File)
131 * The .gbr (.pho in legacy files) extension is the default used in Pcbnew; however
132 * there are a lot of other extensions used for gerber files. Because the first letter
133 * is usually g, we accept g* as extension.
134 * (Mainly internal copper layers do not have specific extension, and filenames are like
135 * *.g1, *.g2 *.gb1 ...)
136 * Now (2014) Ucamco (the company which manages the Gerber format) encourages use of .gbr
137 * only and the Gerber X2 file format.
138 */
139 filetypes = _( "Gerber files (.g* .lgr .pho)" );
140 filetypes << wxT("|");
141 filetypes += wxT("*.g*;*.G*;*.pho;*.PHO" );
142 filetypes << wxT("|");
143
144 /* Special gerber filetypes */
145 filetypes += _( "Top layer" ) + AddFileExtListToFilter( { "GTL" } ) + wxT( "|" );
146 filetypes += _( "Bottom layer" ) + AddFileExtListToFilter( { "GBL" } ) + wxT( "|" );
147 filetypes += _( "Bottom solder resist" ) + AddFileExtListToFilter( { "GBS" } ) + wxT( "|" );
148 filetypes += _( "Top solder resist" ) + AddFileExtListToFilter( { "GTS" } ) + wxT( "|" );
149 filetypes += _( "Bottom overlay" ) + AddFileExtListToFilter( { "GBO" } ) + wxT( "|" );
150 filetypes += _( "Top overlay" ) + AddFileExtListToFilter( { "GTO" } ) + wxT( "|" );
151 filetypes += _( "Bottom paste" ) + AddFileExtListToFilter( { "GBP" } ) + wxT( "|" );
152 filetypes += _( "Top paste" ) + AddFileExtListToFilter( { "GTP" } ) + wxT( "|" );
153 filetypes += _( "Keep-out layer" ) + AddFileExtListToFilter( { "GKO" } ) + wxT( "|" );
154 filetypes += _( "Mechanical layers" ) + AddFileExtListToFilter( { "GM1", "GM2", "GM3", "GM4", "GM5", "GM6", "GM7", "GM8", "GM9" } ) + wxT( "|" );
155 filetypes += _( "Top Pad Master" ) + AddFileExtListToFilter( { "GPT" } ) + wxT( "|" );
156 filetypes += _( "Bottom Pad Master" ) + AddFileExtListToFilter( { "GPB" } ) + wxT( "|" );
157
158 // All filetypes
159 filetypes += AllFilesWildcard();
160
161 // Use the current working directory if the file name path does not exist.
162 if( filename.DirExists() )
163 currentPath = filename.GetPath();
164 else
165 {
166 currentPath = m_mruPath;
167
168 // On wxWidgets 3.1 (bug?) the path in wxFileDialog is ignored when
169 // finishing by the dir separator. Remove it if any:
170 if( currentPath.EndsWith( '\\' ) || currentPath.EndsWith( '/' ) )
171 currentPath.RemoveLast();
172 }
173
174 wxFileDialog dlg( this, _( "Open Gerber File(s)" ), currentPath, filename.GetFullName(),
175 filetypes,
176 wxFD_OPEN | wxFD_FILE_MUST_EXIST | wxFD_MULTIPLE | wxFD_CHANGE_DIR );
177 dlg.SetFilterIndex( lastGerberFileWildcard );
178
179 if( dlg.ShowModal() == wxID_CANCEL )
180 return false;
181
182 lastGerberFileWildcard = dlg.GetFilterIndex();
183 dlg.GetPaths( filenamesList );
184 m_mruPath = currentPath = dlg.GetDirectory();
185 }
186 else
187 {
188 filenamesList.Add( aFullFileName );
189 m_mruPath = currentPath = filename.GetPath();
190 }
191
192 Erase_Current_DrawLayer( false );
193
194 // Set the busy cursor
195 wxBusyCursor wait;
196
197 bool isFirstFile = GetImagesList()->GetLoadedImageCount() == 0;
198
199 bool success = LoadListOfGerberAndDrillFiles( currentPath, filenamesList );
200
201 // Auto zoom is only applied if there is only one file loaded
202 if( isFirstFile )
203 {
204 Zoom_Automatique( false );
205 }
206
207 return success;
208 }
209
210
LoadListOfGerberAndDrillFiles(const wxString & aPath,const wxArrayString & aFilenameList,const std::vector<int> * aFileType)211 bool GERBVIEW_FRAME::LoadListOfGerberAndDrillFiles( const wxString& aPath,
212 const wxArrayString& aFilenameList,
213 const std::vector<int>* aFileType )
214 {
215 wxFileName filename;
216
217 // Read gerber files: each file is loaded on a new GerbView layer
218 bool success = true;
219 int layer = GetActiveLayer();
220 LSET visibility = GetVisibleLayers();
221
222 // Manage errors when loading files
223 wxString msg;
224 WX_STRING_REPORTER reporter( &msg );
225
226 // Create progress dialog (only used if more than 1 file to load
227 std::unique_ptr<WX_PROGRESS_REPORTER> progress = nullptr;
228
229 for( unsigned ii = 0; ii < aFilenameList.GetCount(); ii++ )
230 {
231 filename = aFilenameList[ii];
232
233 if( !filename.IsAbsolute() )
234 filename.SetPath( aPath );
235
236 // Check for non existing files, to avoid creating broken or useless data
237 // and report all in one error list:
238 if( !filename.FileExists() )
239 {
240 wxString warning;
241 warning << "<b>" << _( "File not found:" ) << "</b><br>"
242 << filename.GetFullPath() << "<br>";
243 reporter.Report( warning, RPT_SEVERITY_WARNING );
244 success = false;
245 continue;
246 }
247
248 m_lastFileName = filename.GetFullPath();
249
250 if( !progress && ( aFilenameList.GetCount() > 1 ) )
251 {
252 progress = std::make_unique<WX_PROGRESS_REPORTER>( this,
253 _( "Loading Gerber files..." ), 1,
254 false );
255 progress->SetMaxProgress( aFilenameList.GetCount() - 1 );
256 progress->Report( wxString::Format( _("Loading %u/%zu %s..." ),
257 ii+1,
258 aFilenameList.GetCount(),
259 m_lastFileName ) );
260 }
261 else if( progress )
262 {
263 progress->Report( wxString::Format( _("Loading %u/%zu %s..." ),
264 ii+1,
265 aFilenameList.GetCount(),
266 m_lastFileName ) );
267 progress->KeepRefreshing();
268 }
269
270 SetActiveLayer( layer, false );
271
272 visibility[ layer ] = true;
273
274 try
275 {
276 if( aFileType && ( *aFileType )[ii] == 1 )
277 {
278 LoadExcellonFiles( filename.GetFullPath() );
279 layer = GetActiveLayer(); // Loading NC drill file changes the active layer
280 }
281 else
282 {
283 if( filename.GetExt() == GerberJobFileExtension.c_str() )
284 {
285 //We cannot read a gerber job file as a gerber plot file: skip it
286 wxString txt;
287 txt.Printf( _( "<b>A gerber job file cannot be loaded as a plot file</b> "
288 "<i>%s</i>" ),
289 filename.GetFullName() );
290 success = false;
291 reporter.Report( txt, RPT_SEVERITY_ERROR );
292 }
293 else if( Read_GERBER_File( filename.GetFullPath() ) )
294 {
295 UpdateFileHistory( m_lastFileName );
296
297 GetCanvas()->GetView()->SetLayerHasNegatives(
298 GERBER_DRAW_LAYER( layer ), GetGbrImage( layer )->HasNegativeItems() );
299
300 layer = getNextAvailableLayer( layer );
301
302 if( layer == NO_AVAILABLE_LAYERS && ii < aFilenameList.GetCount() - 1 )
303 {
304 success = false;
305 reporter.Report( MSG_NO_MORE_LAYER, RPT_SEVERITY_ERROR );
306
307 // Report the name of not loaded files:
308 ii += 1;
309 while( ii < aFilenameList.GetCount() )
310 {
311 filename = aFilenameList[ii++];
312 wxString txt =
313 wxString::Format( MSG_NOT_LOADED, filename.GetFullName() );
314 reporter.Report( txt, RPT_SEVERITY_ERROR );
315 }
316 break;
317 }
318
319 SetActiveLayer( layer, false );
320 }
321 }
322 }
323 catch( const std::bad_alloc& )
324 {
325 wxString txt = wxString::Format( MSG_OOM, filename.GetFullName() );
326 reporter.Report( txt, RPT_SEVERITY_ERROR );
327 success = false;
328 continue;
329 }
330
331 if( progress )
332 progress->AdvanceProgress();
333 }
334
335 if( !success )
336 {
337 wxSafeYield(); // Allows slice of time to redraw the screen
338 // to refresh widgets, before displaying messages
339 HTML_MESSAGE_BOX mbox( this, _( "Errors" ) );
340 mbox.ListSet( msg );
341 mbox.ShowModal();
342 }
343
344 SetVisibleLayers( visibility );
345
346 // Synchronize layers tools with actual active layer:
347 ReFillLayerWidget();
348
349 // TODO: it would be nice if we could set the active layer to one of the
350 // ones that was just loaded, but to maintain the previous user experience
351 // we need to set it to a blank layer in case they load another file.
352 // We can't start with the next available layer when loading files because
353 // some users expect the behavior of overwriting the active layer on load.
354 SetActiveLayer( getNextAvailableLayer( layer ), true );
355
356 m_LayersManager->UpdateLayerIcons();
357 syncLayerBox( true );
358
359 GetCanvas()->Refresh();
360
361 return success;
362 }
363
364
LoadExcellonFiles(const wxString & aFullFileName)365 bool GERBVIEW_FRAME::LoadExcellonFiles( const wxString& aFullFileName )
366 {
367 wxString filetypes;
368 wxArrayString filenamesList;
369 wxFileName filename = aFullFileName;
370 wxString currentPath;
371
372 if( !filename.IsOk() )
373 {
374 filetypes = DrillFileWildcard();
375 filetypes << wxT( "|" );
376
377 /* All filetypes */
378 filetypes += AllFilesWildcard();
379
380 /* Use the current working directory if the file name path does not exist. */
381 if( filename.DirExists() )
382 currentPath = filename.GetPath();
383 else
384 currentPath = m_mruPath;
385
386 wxFileDialog dlg( this, _( "Open NC (Excellon) Drill File(s)" ),
387 currentPath, filename.GetFullName(), filetypes,
388 wxFD_OPEN | wxFD_FILE_MUST_EXIST | wxFD_MULTIPLE | wxFD_CHANGE_DIR );
389
390 if( dlg.ShowModal() == wxID_CANCEL )
391 return false;
392
393 dlg.GetPaths( filenamesList );
394 currentPath = wxGetCwd();
395 m_mruPath = currentPath;
396 }
397 else
398 {
399 filenamesList.Add( aFullFileName );
400 currentPath = filename.GetPath();
401 m_mruPath = currentPath;
402 }
403
404 // Read Excellon drill files: each file is loaded on a new GerbView layer
405 bool success = true;
406 int layer = GetActiveLayer();
407
408 // Manage errors when loading files
409 wxString msg;
410 WX_STRING_REPORTER reporter( &msg );
411
412 for( unsigned ii = 0; ii < filenamesList.GetCount(); ii++ )
413 {
414 filename = filenamesList[ii];
415
416 if( !filename.IsAbsolute() )
417 filename.SetPath( currentPath );
418
419 m_lastFileName = filename.GetFullPath();
420
421 SetActiveLayer( layer, false );
422
423 if( Read_EXCELLON_File( filename.GetFullPath() ) )
424 {
425 // Update the list of recent drill files.
426 UpdateFileHistory( filename.GetFullPath(), &m_drillFileHistory );
427
428 layer = getNextAvailableLayer( layer );
429
430 if( layer == NO_AVAILABLE_LAYERS && ii < filenamesList.GetCount()-1 )
431 {
432 success = false;
433 reporter.Report( MSG_NO_MORE_LAYER, RPT_SEVERITY_ERROR );
434
435 // Report the name of not loaded files:
436 ii += 1;
437 while( ii < filenamesList.GetCount() )
438 {
439 filename = filenamesList[ii++];
440 wxString txt = wxString::Format( MSG_NOT_LOADED, filename.GetFullName() );
441 reporter.Report( txt, RPT_SEVERITY_ERROR );
442 }
443 break;
444 }
445
446 SetActiveLayer( layer, false );
447 }
448 }
449
450 if( !success )
451 {
452 HTML_MESSAGE_BOX mbox( this, _( "Errors" ) );
453 mbox.ListSet( msg );
454 mbox.ShowModal();
455 }
456
457 Zoom_Automatique( false );
458
459 // Synchronize layers tools with actual active layer:
460 ReFillLayerWidget();
461 SetActiveLayer( GetActiveLayer() );
462 m_LayersManager->UpdateLayerIcons();
463 syncLayerBox();
464
465 return success;
466 }
467
468
unarchiveFiles(const wxString & aFullFileName,REPORTER * aReporter)469 bool GERBVIEW_FRAME::unarchiveFiles( const wxString& aFullFileName, REPORTER* aReporter )
470 {
471 wxString msg;
472
473 // Extract the path of aFullFileName. We use it to store temporary files
474 wxFileName fn( aFullFileName );
475 wxString unzipDir = fn.GetPath();
476
477 wxFFileInputStream zipFile( aFullFileName );
478
479 if( !zipFile.IsOk() )
480 {
481 if( aReporter )
482 {
483 msg.Printf( _( "Zip file '%s' cannot be opened." ), aFullFileName );
484 aReporter->Report( msg, RPT_SEVERITY_ERROR );
485 }
486
487 return false;
488 }
489
490 // Update the list of recent zip files.
491 UpdateFileHistory( aFullFileName, &m_zipFileHistory );
492
493 // The unzipped file in only a temporary file. Give it a filename
494 // which cannot conflict with an usual filename.
495 // TODO: make Read_GERBER_File() and Read_EXCELLON_File() able to
496 // accept a stream, and avoid using a temp file.
497 wxFileName temp_fn( "$tempfile.tmp" );
498 temp_fn.MakeAbsolute( unzipDir );
499 wxString unzipped_tempfile = temp_fn.GetFullPath();
500
501
502 bool success = true;
503 wxZipInputStream zipArchive( zipFile );
504 wxZipEntry* entry;
505 bool reported_no_more_layer = false;
506
507 while( ( entry = zipArchive.GetNextEntry() ) )
508 {
509 wxString fname = entry->GetName();
510 wxFileName uzfn = fname;
511 wxString curr_ext = uzfn.GetExt().Lower();
512
513 // The archive contains Gerber and/or Excellon drill files. Use the right loader.
514 // However it can contain a few other files (reports, pdf files...),
515 // which will be skipped.
516 // Gerber files ext is usually "gbr", but can be also another value, starting by "g"
517 // old gerber files ext from kicad is .pho
518 // drill files do not have a well defined ext
519 // It is .drl in kicad, but .txt in Altium for instance
520 // Allows only .drl for drill files.
521 if( curr_ext[0] != 'g' && curr_ext != "pho" && curr_ext != "drl" )
522 {
523 if( aReporter )
524 {
525 msg.Printf( _( "Skipped file '%s' (unknown type).\n" ), entry->GetName() );
526 aReporter->Report( msg, RPT_SEVERITY_WARNING );
527 }
528
529 continue;
530 }
531
532 if( curr_ext == GerberJobFileExtension.c_str() )
533 {
534 //We cannot read a gerber job file as a gerber plot file: skip it
535 if( aReporter )
536 {
537 msg.Printf( _( "Skipped file '%s' (gerber job file).\n" ), entry->GetName() );
538 aReporter->Report( msg, RPT_SEVERITY_WARNING );
539 }
540
541 continue;
542 }
543
544 int layer = GetActiveLayer();
545
546 if( layer == NO_AVAILABLE_LAYERS )
547 {
548 success = false;
549
550 if( aReporter )
551 {
552 if( !reported_no_more_layer )
553 aReporter->Report( MSG_NO_MORE_LAYER, RPT_SEVERITY_ERROR );
554
555 reported_no_more_layer = true;
556
557 // Report the name of not loaded files:
558 msg.Printf( MSG_NOT_LOADED, entry->GetName() );
559 aReporter->Report( msg, RPT_SEVERITY_ERROR );
560 }
561
562 delete entry;
563 continue;
564 }
565
566 // Create the unzipped temporary file:
567 {
568 wxFFileOutputStream temporary_ofile( unzipped_tempfile );
569
570 if( temporary_ofile.Ok() )
571 temporary_ofile.Write( zipArchive );
572 else
573 {
574 success = false;
575
576 if( aReporter )
577 {
578 msg.Printf( _( "<b>Unable to create temporary file '%s'.</b>\n"),
579 unzipped_tempfile );
580 aReporter->Report( msg, RPT_SEVERITY_ERROR );
581 }
582 }
583 }
584
585 bool read_ok = true;
586
587 if( curr_ext[0] == 'g' || curr_ext == "pho" )
588 {
589 // Read gerber files: each file is loaded on a new GerbView layer
590 read_ok = Read_GERBER_File( unzipped_tempfile );
591
592 if( read_ok )
593 GetCanvas()->GetView()->SetLayerHasNegatives(
594 GERBER_DRAW_LAYER( layer ), GetGbrImage( layer )->HasNegativeItems() );
595 }
596 else // if( curr_ext == "drl" )
597 {
598 read_ok = Read_EXCELLON_File( unzipped_tempfile );
599 }
600
601 delete entry;
602
603 // The unzipped file is only a temporary file, delete it.
604 wxRemoveFile( unzipped_tempfile );
605
606 if( !read_ok )
607 {
608 success = false;
609
610 if( aReporter )
611 {
612 msg.Printf( _("<b>unzipped file %s read error</b>\n"), unzipped_tempfile );
613 aReporter->Report( msg, RPT_SEVERITY_ERROR );
614 }
615 }
616 else
617 {
618 GERBER_FILE_IMAGE* gerber_image = GetGbrImage( layer );
619
620 if( gerber_image )
621 gerber_image->m_FileName = fname;
622
623 layer = getNextAvailableLayer( layer );
624 SetActiveLayer( layer, false );
625 }
626 }
627
628 return success;
629 }
630
631
LoadZipArchiveFile(const wxString & aFullFileName)632 bool GERBVIEW_FRAME::LoadZipArchiveFile( const wxString& aFullFileName )
633 {
634 #define ZipFileExtension "zip"
635
636 wxFileName filename = aFullFileName;
637 wxString currentPath;
638
639 if( !filename.IsOk() )
640 {
641 // Use the current working directory if the file name path does not exist.
642 if( filename.DirExists() )
643 currentPath = filename.GetPath();
644 else
645 currentPath = m_mruPath;
646
647 wxFileDialog dlg( this, _( "Open Zip File" ), currentPath, filename.GetFullName(),
648 ZipFileWildcard(), wxFD_OPEN | wxFD_FILE_MUST_EXIST | wxFD_CHANGE_DIR );
649
650 if( dlg.ShowModal() == wxID_CANCEL )
651 return false;
652
653 filename = dlg.GetPath();
654 currentPath = wxGetCwd();
655 m_mruPath = currentPath;
656 }
657 else
658 {
659 currentPath = filename.GetPath();
660 m_mruPath = currentPath;
661 }
662
663 wxString msg;
664 WX_STRING_REPORTER reporter( &msg );
665
666 if( filename.IsOk() )
667 unarchiveFiles( filename.GetFullPath(), &reporter );
668
669 Zoom_Automatique( false );
670
671 // Synchronize layers tools with actual active layer:
672 ReFillLayerWidget();
673 SetActiveLayer( GetActiveLayer() );
674 m_LayersManager->UpdateLayerIcons();
675 syncLayerBox();
676
677 if( !msg.IsEmpty() )
678 {
679 wxSafeYield(); // Allows slice of time to redraw the screen
680 // to refresh widgets, before displaying messages
681 HTML_MESSAGE_BOX mbox( this, _( "Messages" ) );
682 mbox.ListSet( msg );
683 mbox.ShowModal();
684 }
685
686 return true;
687 }
688