1 /*
2  * This program source code file is part of KiCad, a free EDA CAD application.
3  *
4  * Copyright (C) 2009 Wayne Stambaugh <stambaughw@gmail.com>
5  * Copyright (C) 2014-2021 KiCad Developers, see CHANGELOG.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 <dialog_sheet_properties.h>
26 #include <kiface_base.h>
27 #include <wx/string.h>
28 #include <wx/log.h>
29 #include <wx/tooltip.h>
30 #include <confirm.h>
31 #include <validators.h>
32 #include <wildcards_and_files_ext.h>
33 #include <widgets/tab_traversal.h>
34 #include <sch_edit_frame.h>
35 #include <sch_sheet.h>
36 #include <schematic.h>
37 #include <bitmaps.h>
38 #include <eeschema_settings.h>
39 #include <settings/color_settings.h>
40 #include <trace_helpers.h>
41 #include "panel_eeschema_color_settings.h"
42 
DIALOG_SHEET_PROPERTIES(SCH_EDIT_FRAME * aParent,SCH_SHEET * aSheet,bool * aClearAnnotationNewItems)43 DIALOG_SHEET_PROPERTIES::DIALOG_SHEET_PROPERTIES( SCH_EDIT_FRAME* aParent, SCH_SHEET* aSheet,
44                                                   bool* aClearAnnotationNewItems ) :
45     DIALOG_SHEET_PROPERTIES_BASE( aParent ),
46     m_frame( aParent ),
47     m_clearAnnotationNewItems( aClearAnnotationNewItems ),
48     m_borderWidth( aParent, m_borderWidthLabel, m_borderWidthCtrl, m_borderWidthUnits ),
49     m_dummySheet( *aSheet ),
50     m_dummySheetNameField( wxDefaultPosition, SHEETNAME, &m_dummySheet )
51 {
52     m_sheet = aSheet;
53     m_fields = new FIELDS_GRID_TABLE<SCH_FIELD>( this, aParent, m_grid, m_sheet );
54     m_width = 100;  // Will be later set to a better value
55     m_delayedFocusRow = SHEETNAME;
56     m_delayedFocusColumn = FDC_VALUE;
57 
58     // Give a bit more room for combobox editors
59     m_grid->SetDefaultRowSize( m_grid->GetDefaultRowSize() + 4 );
60 
61     m_grid->SetTable( m_fields );
62     m_grid->PushEventHandler( new FIELDS_GRID_TRICKS( m_grid, this ) );
63     m_grid->SetSelectionMode( wxGrid::wxGridSelectRows );
64 
65     // Show/hide columns according to user's preference
66     auto cfg = dynamic_cast<EESCHEMA_SETTINGS*>( Kiface().KifaceSettings() );
67     wxASSERT( cfg );
68 
69     if( cfg )
70     {
71         m_shownColumns = cfg->m_Appearance.edit_sheet_visible_columns;
72         m_grid->ShowHideColumns( m_shownColumns );
73     }
74 
75     wxToolTip::Enable( true );
76     m_stdDialogButtonSizerOK->SetDefault();
77 
78     // Configure button logos
79     m_bpAdd->SetBitmap( KiBitmap( BITMAPS::small_plus ) );
80     m_bpDelete->SetBitmap( KiBitmap( BITMAPS::small_trash ) );
81     m_bpMoveUp->SetBitmap( KiBitmap( BITMAPS::small_up ) );
82     m_bpMoveDown->SetBitmap( KiBitmap( BITMAPS::small_down ) );
83 
84     // Set font sizes
85     m_hierarchicalPathLabel->SetFont( KIUI::GetInfoFont( this ) );
86 
87     // wxFormBuilder doesn't include this event...
88     m_grid->Connect( wxEVT_GRID_CELL_CHANGING,
89                      wxGridEventHandler( DIALOG_SHEET_PROPERTIES::OnGridCellChanging ),
90                      nullptr, this );
91 
92     finishDialogSettings();
93 }
94 
95 
~DIALOG_SHEET_PROPERTIES()96 DIALOG_SHEET_PROPERTIES::~DIALOG_SHEET_PROPERTIES()
97 {
98     auto cfg = dynamic_cast<EESCHEMA_SETTINGS*>( Kiface().KifaceSettings() );
99     wxASSERT( cfg );
100 
101     if( cfg )
102         cfg->m_Appearance.edit_sheet_visible_columns = m_grid->GetShownColumns();
103 
104     // Prevents crash bug in wxGrid's d'tor
105     m_grid->DestroyTable( m_fields );
106 
107     m_grid->Disconnect( wxEVT_GRID_CELL_CHANGING,
108                         wxGridEventHandler( DIALOG_SHEET_PROPERTIES::OnGridCellChanging ),
109                         nullptr, this );
110 
111     // Delete the GRID_TRICKS.
112     m_grid->PopEventHandler( true );
113 }
114 
115 
TransferDataToWindow()116 bool DIALOG_SHEET_PROPERTIES::TransferDataToWindow()
117 {
118     if( !wxDialog::TransferDataToWindow() )
119         return false;
120 
121     // Push a copy of each field into m_updateFields
122     for( SCH_FIELD& field : m_sheet->GetFields() )
123     {
124         SCH_FIELD field_copy( field );
125 
126 #ifdef __WINDOWS__
127         // Filenames are stored using unix notation
128         if( field_copy.GetId() == SHEETFILENAME )
129         {
130             wxString filename = field_copy.GetText();
131             filename.Replace( wxT( "/" ), wxT( "\\" ) );
132             field_copy.SetText( filename );
133         }
134 #endif
135 
136         // change offset to be symbol-relative
137         field_copy.Offset( -m_sheet->GetPosition() );
138 
139         m_fields->push_back( field_copy );
140     }
141 
142     // notify the grid
143     wxGridTableMessage msg( m_fields, wxGRIDTABLE_NOTIFY_ROWS_APPENDED, m_fields->size() );
144     m_grid->ProcessTableMessage( msg );
145     AdjustGridColumns( m_grid->GetRect().GetWidth() );
146 
147     // border width
148     m_borderWidth.SetValue( m_sheet->GetBorderWidth() );
149 
150     // set up color swatches
151     KIGFX::COLOR4D borderColor     = m_sheet->GetBorderColor();
152     KIGFX::COLOR4D backgroundColor = m_sheet->GetBackgroundColor();
153 
154     m_borderSwatch->SetDefaultColor( COLOR4D::UNSPECIFIED );
155     m_backgroundSwatch->SetDefaultColor( COLOR4D::UNSPECIFIED );
156 
157     m_borderSwatch->SetSwatchColor( borderColor, false );
158     m_backgroundSwatch->SetSwatchColor( backgroundColor, false );
159 
160     KIGFX::COLOR4D canvas = m_frame->GetColorSettings()->GetColor( LAYER_SCHEMATIC_BACKGROUND );
161     m_borderSwatch->SetSwatchBackground( canvas );
162     m_backgroundSwatch->SetSwatchBackground( canvas );
163 
164     SCH_SHEET_LIST hierarchy = m_frame->Schematic().GetFullHierarchy();
165     SCH_SHEET_PATH instance = m_frame->GetCurrentSheet();
166 
167     instance.push_back( m_sheet );
168 
169     wxString nextPageNumber = m_sheet->GetPageNumber( instance );
170 
171     m_pageNumberTextCtrl->ChangeValue( nextPageNumber );
172 
173     Layout();
174 
175     return true;
176 }
177 
178 
Validate()179 bool DIALOG_SHEET_PROPERTIES::Validate()
180 {
181     wxString msg;
182     LIB_ID   id;
183 
184     if( !m_grid->CommitPendingChanges() || !m_grid->Validate() )
185         return false;
186 
187     // Check for missing field names.
188     for( size_t i = SHEET_MANDATORY_FIELDS;  i < m_fields->size(); ++i )
189     {
190         SCH_FIELD& field = m_fields->at( i );
191         wxString   fieldName = field.GetName( false );
192 
193         if( fieldName.IsEmpty() )
194         {
195             DisplayErrorMessage( this, _( "Fields must have a name." ) );
196 
197             m_delayedFocusColumn = FDC_NAME;
198             m_delayedFocusRow = i;
199 
200             return false;
201         }
202     }
203 
204     return true;
205 }
206 
207 
positioningChanged(const SCH_FIELD & a,const SCH_FIELD & b)208 static bool positioningChanged( const SCH_FIELD& a, const SCH_FIELD& b )
209 {
210     if( a.GetPosition() != b.GetPosition() )
211         return true;
212 
213     if( a.GetHorizJustify() != b.GetHorizJustify() )
214         return true;
215 
216     if( a.GetVertJustify() != b.GetVertJustify() )
217         return true;
218 
219     if( a.GetTextAngle() != b.GetTextAngle() )
220         return true;
221 
222     return false;
223 }
224 
225 
positioningChanged(FIELDS_GRID_TABLE<SCH_FIELD> * a,std::vector<SCH_FIELD> & b)226 static bool positioningChanged( FIELDS_GRID_TABLE<SCH_FIELD>* a, std::vector<SCH_FIELD>& b )
227 {
228     for( size_t i = 0; i < SHEET_MANDATORY_FIELDS; ++i )
229     {
230         if( positioningChanged( a->at( i ), b.at( i ) ) )
231             return true;
232     }
233 
234     return false;
235 }
236 
237 
TransferDataFromWindow()238 bool DIALOG_SHEET_PROPERTIES::TransferDataFromWindow()
239 {
240     if( !wxDialog::TransferDataFromWindow() )  // Calls our Validate() method.
241         return false;
242 
243     // Sheet file names can be relative or absolute.
244     wxString sheetFileName = m_fields->at( SHEETFILENAME ).GetText();
245 
246     // Ensure filepath is not empty.  (In normal use will be caught by grid validators,
247     // but unedited data from existing files can be bad.)
248     if( sheetFileName.IsEmpty() )
249     {
250         DisplayError( this, _( "A sheet must have a valid file name." ) );
251         return false;
252     }
253 
254     // Ensure the filename extension is OK.  In normal use will be caught by grid validators,
255     // but unedited data from existing files can be bad.
256     wxFileName fn( sheetFileName );
257 
258     if( fn.GetExt().CmpNoCase( KiCadSchematicFileExtension ) != 0 )
259     {
260         DisplayError( this, _( "Sheet file must have a '.kicad_sch' extension." ) );
261         return false;
262     }
263 
264     wxString newRelativeFilename = fn.GetFullPath();
265 
266     // Inside Eeschema, filenames are stored using unix notation
267     newRelativeFilename.Replace( wxT( "\\" ), wxT( "/" ) );
268 
269     wxString oldFilename = m_sheet->GetFields()[ SHEETFILENAME ].GetText();
270     oldFilename.Replace( wxT( "\\" ), wxT( "/" ) );
271 
272     bool filename_changed = oldFilename != newRelativeFilename;
273 
274     if( filename_changed || m_sheet->IsNew() )
275     {
276         SCH_SCREEN* currentScreen = m_frame->GetCurrentSheet().LastScreen();
277 
278         wxCHECK( currentScreen, false );
279 
280         bool clearFileName = false;
281 
282         // This can happen for the root sheet when opening Eeschema in the stand alone mode.
283         if( currentScreen->GetFileName().IsEmpty() )
284         {
285             clearFileName = true;
286             currentScreen->SetFileName( m_frame->Prj().AbsolutePath( wxT( "noname.kicad_sch" ) ) );
287         }
288 
289         wxFileName tmp( fn );
290         wxFileName screenFileName = currentScreen->GetFileName();
291 
292         if( fn.IsAbsolute() && fn.MakeRelativeTo( screenFileName.GetPath() ) )
293         {
294             wxMessageDialog makeRelDlg( this, _( "Use relative path for sheet file?" ),
295                                         _( "Sheet File Path" ),
296                                         wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION | wxCENTER );
297 
298             makeRelDlg.SetExtendedMessage( _( "Using relative hierarchical sheet file name paths "
299                                               "improves schematic portability across systems and "
300                                               "platforms.  Using absolute paths can result in "
301                                               "portability issues." ) );
302             makeRelDlg.SetYesNoLabels( wxMessageDialog::ButtonLabel( _( "Use Relative Path" ) ),
303                                        wxMessageDialog::ButtonLabel( _( "Use Absolute Path" ) ) );
304 
305             if( makeRelDlg.ShowModal() == wxID_YES )
306             {
307                 wxLogTrace( tracePathsAndFiles, "\n    Converted absolute path: '%s'"
308                                                 "\n    to relative path: '%s'",
309                                                 tmp.GetPath(),
310                                                 fn.GetPath() );
311                 m_fields->at( SHEETFILENAME ).SetText( fn.GetFullPath() );
312                 newRelativeFilename = fn.GetFullPath();
313             }
314         }
315 
316         if( !onSheetFilenameChanged( newRelativeFilename ) )
317         {
318             if( clearFileName )
319                 currentScreen->SetFileName( wxEmptyString );
320 
321             return false;
322         }
323 
324         if( clearFileName )
325             currentScreen->SetFileName( wxEmptyString );
326 
327         // One last validity check (and potential repair) just to be sure to be sure
328         SCH_SHEET_LIST repairedList( &m_frame->Schematic().Root(), true );
329     }
330 
331     wxString newSheetname = m_fields->at( SHEETNAME ).GetText();
332 
333     if( newSheetname.IsEmpty() )
334         newSheetname = _( "Untitled Sheet" );
335 
336     m_fields->at( SHEETNAME ).SetText( newSheetname );
337 
338     // change all field positions from relative to absolute
339     for( unsigned i = 0;  i < m_fields->size();  ++i )
340         m_fields->at( i ).Offset( m_sheet->GetPosition() );
341 
342     if( positioningChanged( m_fields, m_sheet->GetFields() ) )
343         m_sheet->ClearFieldsAutoplaced();
344 
345     m_sheet->SetFields( *m_fields );
346 
347     m_sheet->SetBorderWidth( m_borderWidth.GetValue() );
348 
349     COLOR_SETTINGS* colorSettings = m_frame->GetColorSettings();
350 
351     if( colorSettings->GetOverrideSchItemColors()
352             && ( m_sheet->GetBorderColor()     != m_borderSwatch->GetSwatchColor() ||
353                  m_sheet->GetBackgroundColor() != m_backgroundSwatch->GetSwatchColor() ) )
354     {
355         wxPanel temp( this );
356         temp.Hide();
357         PANEL_EESCHEMA_COLOR_SETTINGS prefs( m_frame, &temp );
358         wxString checkboxLabel = prefs.m_optOverrideColors->GetLabel();
359 
360         KIDIALOG dlg( this, _( "Note: item colors are overridden in the current color theme." ),
361                       KIDIALOG::KD_WARNING );
362         dlg.ShowDetailedText( wxString::Format( _( "To see individual item colors uncheck '%s'\n"
363                                                    "in Preferences > Eeschema > Colors." ),
364                                                 checkboxLabel ) );
365         dlg.DoNotShowCheckbox( __FILE__, __LINE__ );
366         dlg.ShowModal();
367     }
368 
369     m_sheet->SetBorderColor( m_borderSwatch->GetSwatchColor() );
370     m_sheet->SetBackgroundColor( m_backgroundSwatch->GetSwatchColor() );
371 
372     SCH_SHEET_LIST hierarchy = m_frame->Schematic().GetFullHierarchy();
373     SCH_SHEET_PATH instance = m_frame->GetCurrentSheet();
374 
375     instance.push_back( m_sheet );
376 
377     if( m_sheet->IsNew() )
378         m_sheet->AddInstance( instance );
379 
380     m_sheet->SetPageNumber( instance, m_pageNumberTextCtrl->GetValue() );
381 
382     m_frame->TestDanglingEnds();
383 
384     // Refresh all sheets in case ordering changed.
385     for( SCH_ITEM* item : m_frame->GetScreen()->Items().OfType( SCH_SHEET_T ) )
386         m_frame->UpdateItem( item );
387 
388     m_frame->OnModify();
389 
390     return true;
391 }
392 
393 
onSheetFilenameChanged(const wxString & aNewFilename)394 bool DIALOG_SHEET_PROPERTIES::onSheetFilenameChanged( const wxString& aNewFilename )
395 {
396     wxString msg;
397 
398     // Sheet file names are relative to the path of the current sheet.  This allows for
399     // nesting of schematic files in subfolders.  Screen file names are always absolute.
400     wxFileName sheetFileName( aNewFilename );
401 
402     if( sheetFileName.GetExt().IsEmpty() )
403     {
404         sheetFileName.SetExt( KiCadSchematicFileExtension );
405     }
406     else if( sheetFileName.GetExt().CmpNoCase( KiCadSchematicFileExtension ) != 0 )
407     {
408         msg = wxString::Format( _( "The file '%s' does not appear to be a valid schematic file." ),
409                                 sheetFileName.GetFullName() );
410         wxMessageDialog badSchFileDialog( this, msg, _( "Invalid Schematic File" ),
411                                           wxOK | wxCENTRE | wxICON_EXCLAMATION );
412         badSchFileDialog.ShowModal();
413         return false;
414     }
415 
416     SCH_SHEET_LIST fullHierarchy = m_frame->Schematic().GetFullHierarchy();
417     std::vector<SCH_SHEET_INSTANCE> sheetInstances = fullHierarchy.GetSheetInstances();
418     wxFileName screenFileName( sheetFileName );
419     wxFileName tmp( sheetFileName );
420 
421     SCH_SCREEN* currentScreen = m_frame->GetCurrentSheet().LastScreen();
422 
423     wxCHECK( currentScreen, false );
424 
425     // SCH_SCREEN file names are always absolute.
426     wxFileName currentScreenFileName = currentScreen->GetFileName();
427 
428     if( !screenFileName.Normalize( wxPATH_NORM_ALL, currentScreenFileName.GetPath() ) )
429     {
430         msg = wxString::Format( _( "Cannot normalize new sheet schematic file path:\n"
431                                    "'%s'\n"
432                                    "against parent sheet schematic file path:\n"
433                                    "'%s'." ),
434                                 sheetFileName.GetPath(),
435                                 currentScreenFileName.GetPath() );
436         DisplayError( this, msg );
437         return false;
438     }
439 
440     wxString newAbsoluteFilename = screenFileName.GetFullPath();
441 
442     // Inside Eeschema, filenames are stored using unix notation
443     newAbsoluteFilename.Replace( wxT( "\\" ), wxT( "/" ) );
444 
445     bool renameFile = false;
446     bool loadFromFile = false;
447     bool clearAnnotation = false;
448     bool restoreSheet = false;
449     bool isExistingSheet = false;
450     SCH_SCREEN* useScreen = nullptr;
451     SCH_SCREEN* oldScreen = nullptr;
452 
453     // Search for a schematic file having the same filename already in use in the hierarchy
454     // or on disk, in order to reuse it.
455     if( !m_frame->Schematic().Root().SearchHierarchy( newAbsoluteFilename, &useScreen ) )
456     {
457         loadFromFile = wxFileExists( newAbsoluteFilename );
458 
459         wxLogTrace( tracePathsAndFiles, "\n    Sheet requested file '%s', %s",
460                                         newAbsoluteFilename,
461                                         loadFromFile ? "found" : "not found" );
462     }
463 
464     if( m_sheet->GetScreen() == nullptr )      // New just created sheet.
465     {
466         if( !m_frame->AllowCaseSensitiveFileNameClashes( newAbsoluteFilename ) )
467             return false;
468 
469         if( useScreen || loadFromFile )     // Load from existing file.
470         {
471             clearAnnotation = true;
472 
473             if( !IsOK( this, wxString::Format( _( "'%s' already exists." ),
474                                                sheetFileName.GetFullName() )
475                              + wxT( "\n\n" )
476                              + wxString::Format( _( "Link '%s' to this file?" ),
477                                                  newAbsoluteFilename ) ) )
478             {
479                 return false;
480             }
481         }
482         else                                // New file.
483         {
484             m_frame->InitSheet( m_sheet, newAbsoluteFilename );
485         }
486     }
487     else                                    // Existing sheet.
488     {
489         bool isUndoable = true;
490         isExistingSheet = true;
491 
492         if( !m_frame->AllowCaseSensitiveFileNameClashes( newAbsoluteFilename ) )
493             return false;
494 
495         // We are always using here a case insensitive comparison to avoid issues
496         // under Windows, although under Unix filenames are case sensitive.
497         // But many users create schematic under both Unix and Windows
498         // **
499         // N.B. 1: aSheet->GetFileName() will return a relative path
500         //         aSheet->GetScreen()->GetFileName() returns a full path
501         //
502         // N.B. 2: newFilename uses the unix notation for separator.
503         //         so we must use it also to compare the old and new filenames
504         wxString oldAbsoluteFilename = m_sheet->GetScreen()->GetFileName();
505         oldAbsoluteFilename.Replace( wxT( "\\" ), wxT( "/" ) );
506 
507         if( newAbsoluteFilename.Cmp( oldAbsoluteFilename ) != 0 )
508         {
509             // Sheet file name changes cannot be undone.
510             isUndoable = false;
511 
512             if( useScreen || loadFromFile )           // Load from existing file.
513             {
514                 clearAnnotation = true;
515 
516                 if( !IsOK( this, wxString::Format( _( "Change '%s' link from '%s' to '%s'?" ),
517                                                    newAbsoluteFilename,
518                                                    m_sheet->GetFileName(),
519                                                    sheetFileName.GetFullName() )
520                                  + wxT( "\n\n" )
521                                  + _( "This action cannot be undone." ) ) )
522                 {
523                     return false;
524                 }
525 
526                 if( loadFromFile )
527                     m_sheet->SetScreen( nullptr );
528             }
529             else                                      // Save to new file name.
530             {
531                 if( m_sheet->GetScreenCount() > 1 )
532                 {
533                     if( !IsOK( this, wxString::Format( _( "Create new file '%s' with contents of '%s'?" ),
534                                                        sheetFileName.GetFullName(),
535                                                        m_sheet->GetFileName() )
536                                      + wxT( "\n\n" )
537                                      + _( "This action cannot be undone." ) ) )
538                     {
539                         return false;
540                     }
541                 }
542 
543                 renameFile = true;
544             }
545         }
546 
547         if( isUndoable )
548             m_frame->SaveCopyInUndoList( m_frame->GetScreen(), m_sheet, UNDO_REDO::CHANGED, false );
549 
550         if( renameFile )
551         {
552             SCH_PLUGIN::SCH_PLUGIN_RELEASER pi( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_KICAD ) );
553 
554             // If the associated screen is shared by more than one sheet, do not
555             // change the filename of the corresponding screen here.
556             // (a new screen will be created later)
557             // if it is not shared, update the filename
558             if( m_sheet->GetScreenCount() <= 1 )
559                 m_sheet->GetScreen()->SetFileName( newAbsoluteFilename );
560 
561             try
562             {
563                 pi->Save( newAbsoluteFilename, m_sheet, &m_frame->Schematic() );
564             }
565             catch( const IO_ERROR& ioe )
566             {
567                 msg = wxString::Format( _( "Error occurred saving schematic file '%s'." ),
568                                         newAbsoluteFilename );
569                 DisplayErrorMessage( this, msg, ioe.What() );
570 
571                 msg = wxString::Format( _( "Failed to save schematic '%s'" ),
572                                         newAbsoluteFilename );
573                 m_frame->SetMsgPanel( wxEmptyString, msg );
574                 return false;
575             }
576 
577             // If the associated screen is shared by more than one sheet, remove the
578             // screen and reload the file to a new screen.  Failure to do this will trash
579             // the screen reference counting in complex hierarchies.
580             if( m_sheet->GetScreenCount() > 1 )
581             {
582                 oldScreen = m_sheet->GetScreen();
583                 m_sheet->SetScreen( nullptr );
584                 loadFromFile = true;
585             }
586         }
587     }
588 
589     SCH_SHEET_PATH& currentSheet = m_frame->GetCurrentSheet();
590 
591     if( useScreen )
592     {
593         // Create a temporary sheet for recursion testing to prevent a possible recursion error.
594         std::unique_ptr< SCH_SHEET> tmpSheet = std::make_unique<SCH_SHEET>();
595         tmpSheet->GetFields()[SHEETNAME] = m_fields->at( SHEETNAME );
596         tmpSheet->GetFields()[SHEETFILENAME].SetText( sheetFileName.GetFullPath() );
597         tmpSheet->SetScreen( useScreen );
598 
599         // No need to check for valid library IDs if we are using an existing screen.
600         if( m_frame->CheckSheetForRecursion( tmpSheet.get(), &currentSheet ) )
601         {
602             if( restoreSheet )
603                 currentSheet.LastScreen()->Append( m_sheet );
604 
605             return false;
606         }
607 
608         // It's safe to set the sheet screen now.
609         m_sheet->SetScreen( useScreen );
610     }
611     else if( loadFromFile )
612     {
613         if( isExistingSheet )
614         {
615             // Temporarily remove the sheet from the current schematic page so that recursion
616             // and symbol library link tests can be performed with the modified sheet settings.
617             restoreSheet = true;
618             currentSheet.LastScreen()->Remove( m_sheet );
619         }
620 
621         if( !m_frame->LoadSheetFromFile( m_sheet, &currentSheet, newAbsoluteFilename )
622           || m_frame->CheckSheetForRecursion( m_sheet, &currentSheet ) )
623         {
624             if( restoreSheet )
625             {
626                 // If we cleared the previous screen, restore it before returning to the user
627                 if( oldScreen )
628                     m_sheet->SetScreen( oldScreen );
629 
630                 currentSheet.LastScreen()->Append( m_sheet );
631             }
632 
633             return false;
634         }
635 
636         if( restoreSheet )
637             currentSheet.LastScreen()->Append( m_sheet );
638 
639         // The full hiearchy needs to be reloaded because any sub-sheet that occurred on
640         // file load will have new SCH_SHEET object pointers.
641         fullHierarchy = m_frame->Schematic().GetFullHierarchy();
642         fullHierarchy.UpdateSheetInstances( sheetInstances );
643     }
644 
645     if( m_clearAnnotationNewItems )
646         *m_clearAnnotationNewItems = clearAnnotation;
647 
648     return true;
649 }
650 
651 
OnGridCellChanging(wxGridEvent & event)652 void DIALOG_SHEET_PROPERTIES::OnGridCellChanging( wxGridEvent& event )
653 {
654     bool              success = true;
655     wxGridCellEditor* editor = m_grid->GetCellEditor( event.GetRow(), event.GetCol() );
656     wxControl*        control = editor->GetControl();
657     wxTextEntry*      textControl = dynamic_cast<wxTextEntry*>( control );
658 
659     // Short-circuit the validator's more generic "can't be empty" message for the
660     // two mandatory fields:
661     if( event.GetRow() == SHEETNAME && event.GetCol() == FDC_VALUE )
662     {
663         if( textControl && textControl->IsEmpty() )
664         {
665             wxMessageBox( _( "A sheet must have a name." ) );
666             success = false;
667         }
668     }
669     else if( event.GetRow() == SHEETFILENAME && event.GetCol() == FDC_VALUE && textControl )
670     {
671         if( textControl->IsEmpty() )
672         {
673             wxMessageBox( _( "A sheet must have a file specified." ) );
674             success = false;
675         }
676     }
677 
678     if( success && control && control->GetValidator() )
679         success = control->GetValidator()->Validate( control );
680 
681     if( !success )
682     {
683         event.Veto();
684         m_delayedFocusRow = event.GetRow();
685         m_delayedFocusColumn = event.GetCol();
686     }
687 
688     editor->DecRef();
689 }
690 
691 
OnAddField(wxCommandEvent & event)692 void DIALOG_SHEET_PROPERTIES::OnAddField( wxCommandEvent& event )
693 {
694     if( !m_grid->CommitPendingChanges() )
695         return;
696 
697     int       fieldID = m_fields->size();
698     SCH_FIELD newField( wxPoint( 0, 0 ), fieldID, m_sheet,
699                         SCH_SHEET::GetDefaultFieldName( fieldID ) );
700 
701     newField.SetTextAngle( m_fields->at( SHEETNAME ).GetTextAngle() );
702 
703     m_fields->push_back( newField );
704 
705     // notify the grid
706     wxGridTableMessage msg( m_fields, wxGRIDTABLE_NOTIFY_ROWS_APPENDED, 1 );
707     m_grid->ProcessTableMessage( msg );
708 
709     m_grid->MakeCellVisible( m_fields->size() - 1, 0 );
710     m_grid->SetGridCursor( m_fields->size() - 1, 0 );
711 
712     m_grid->EnableCellEditControl();
713     m_grid->ShowCellEditControl();
714 }
715 
716 
OnDeleteField(wxCommandEvent & event)717 void DIALOG_SHEET_PROPERTIES::OnDeleteField( wxCommandEvent& event )
718 {
719     wxArrayInt selectedRows = m_grid->GetSelectedRows();
720 
721     if( selectedRows.empty() && m_grid->GetGridCursorRow() >= 0 )
722         selectedRows.push_back( m_grid->GetGridCursorRow() );
723 
724     if( selectedRows.empty() )
725         return;
726 
727     for( int row : selectedRows )
728     {
729         if( row < SHEET_MANDATORY_FIELDS )
730         {
731             DisplayError( this, wxString::Format( _( "The first %d fields are mandatory." ),
732                                                   SHEET_MANDATORY_FIELDS ) );
733             return;
734         }
735     }
736 
737     m_grid->CommitPendingChanges( true /* quiet mode */ );
738 
739     // Reverse sort so deleting a row doesn't change the indexes of the other rows.
740     selectedRows.Sort( []( int* first, int* second ) { return *second - *first; } );
741 
742     for( int row : selectedRows )
743     {
744         m_fields->erase( m_fields->begin() + row );
745 
746         // notify the grid
747         wxGridTableMessage msg( m_fields, wxGRIDTABLE_NOTIFY_ROWS_DELETED, row, 1 );
748         m_grid->ProcessTableMessage( msg );
749 
750         if( m_grid->GetNumberRows() > 0 )
751         {
752             m_grid->MakeCellVisible( std::max( 0, row-1 ), m_grid->GetGridCursorCol() );
753             m_grid->SetGridCursor( std::max( 0, row-1 ), m_grid->GetGridCursorCol() );
754         }
755     }
756 }
757 
758 
OnMoveUp(wxCommandEvent & event)759 void DIALOG_SHEET_PROPERTIES::OnMoveUp( wxCommandEvent& event )
760 {
761     if( !m_grid->CommitPendingChanges() )
762         return;
763 
764     int i = m_grid->GetGridCursorRow();
765 
766     if( i > SHEET_MANDATORY_FIELDS )
767     {
768         SCH_FIELD tmp = m_fields->at( (unsigned) i );
769         m_fields->erase( m_fields->begin() + i, m_fields->begin() + i + 1 );
770         m_fields->insert( m_fields->begin() + i - 1, tmp );
771         m_grid->ForceRefresh();
772 
773         m_grid->SetGridCursor( i - 1, m_grid->GetGridCursorCol() );
774         m_grid->MakeCellVisible( m_grid->GetGridCursorRow(), m_grid->GetGridCursorCol() );
775     }
776     else
777         wxBell();
778 }
779 
780 
OnMoveDown(wxCommandEvent & event)781 void DIALOG_SHEET_PROPERTIES::OnMoveDown( wxCommandEvent& event )
782 {
783     if( !m_grid->CommitPendingChanges() )
784         return;
785 
786     int i = m_grid->GetGridCursorRow();
787 
788     if( i >= SHEET_MANDATORY_FIELDS && i < m_grid->GetNumberRows() - 1 )
789     {
790         SCH_FIELD tmp = m_fields->at( (unsigned) i );
791         m_fields->erase( m_fields->begin() + i, m_fields->begin() + i + 1 );
792         m_fields->insert( m_fields->begin() + i + 1, tmp );
793         m_grid->ForceRefresh();
794 
795         m_grid->SetGridCursor( i + 1, m_grid->GetGridCursorCol() );
796         m_grid->MakeCellVisible( m_grid->GetGridCursorRow(), m_grid->GetGridCursorCol() );
797     }
798     else
799         wxBell();
800 }
801 
802 
AdjustGridColumns(int aWidth)803 void DIALOG_SHEET_PROPERTIES::AdjustGridColumns( int aWidth )
804 {
805     m_width = aWidth;
806     // Account for scroll bars
807     aWidth -= ( m_grid->GetSize().x - m_grid->GetClientSize().x );
808 
809     m_grid->AutoSizeColumn( 0 );
810 
811     int fixedColsWidth = m_grid->GetColSize( 0 );
812 
813     for( int i = 2; i < m_grid->GetNumberCols(); i++ )
814         fixedColsWidth += m_grid->GetColSize( i );
815 
816     int colSize = std::max( aWidth - fixedColsWidth, -1 );
817     colSize = ( colSize == 0 ) ? -1 : colSize; // don't hide the column!
818 
819     m_grid->SetColSize( 1, colSize );
820 }
821 
822 
OnUpdateUI(wxUpdateUIEvent & event)823 void DIALOG_SHEET_PROPERTIES::OnUpdateUI( wxUpdateUIEvent& event )
824 {
825     wxString shownColumns = m_grid->GetShownColumns();
826 
827     if( shownColumns != m_shownColumns )
828     {
829         m_shownColumns = shownColumns;
830 
831         if( !m_grid->IsCellEditControlShown() )
832             AdjustGridColumns( m_grid->GetRect().GetWidth() );
833     }
834 
835     // Propagate changes in sheetname to displayed hierarchical path
836     wxString hierarchicalPath = _( "Hierarchical path: " );
837 
838     hierarchicalPath += m_frame->GetCurrentSheet().PathHumanReadable( false );
839 
840     if( hierarchicalPath.Last() != '/' )
841         hierarchicalPath.Append( '/' );
842 
843     wxGridCellEditor* editor = m_grid->GetCellEditor( SHEETNAME, FDC_VALUE );
844     wxControl*        control = editor->GetControl();
845     wxTextEntry*      textControl = dynamic_cast<wxTextEntry*>( control );
846     wxString          sheetName;
847 
848     if( textControl )
849         sheetName = textControl->GetValue();
850     else
851         sheetName = m_grid->GetCellValue( SHEETNAME, FDC_VALUE );
852 
853     m_dummySheet.SetFields( *m_fields );
854     m_dummySheetNameField.SetText( sheetName );
855     hierarchicalPath += m_dummySheetNameField.GetShownText();
856 
857     editor->DecRef();
858 
859     if( m_hierarchicalPathLabel->GetLabel() != hierarchicalPath )
860         m_hierarchicalPathLabel->SetLabel( hierarchicalPath );
861 
862     // Handle a delayed focus
863     if( m_delayedFocusRow >= 0 )
864     {
865         m_grid->SetFocus();
866         m_grid->MakeCellVisible( m_delayedFocusRow, m_delayedFocusColumn );
867         m_grid->SetGridCursor( m_delayedFocusRow, m_delayedFocusColumn );
868 
869 
870         m_grid->EnableCellEditControl( true );
871         m_grid->ShowCellEditControl();
872 
873         m_delayedFocusRow = -1;
874         m_delayedFocusColumn = -1;
875     }
876 }
877 
878 
OnSizeGrid(wxSizeEvent & event)879 void DIALOG_SHEET_PROPERTIES::OnSizeGrid( wxSizeEvent& event )
880 {
881     auto new_size = event.GetSize().GetX();
882 
883     if( m_width != new_size )
884     {
885         AdjustGridColumns( new_size );
886     }
887 
888     // Always propagate for a grid repaint (needed if the height changes, as well as width)
889     event.Skip();
890 }
891 
892 
OnInitDlg(wxInitDialogEvent & event)893 void DIALOG_SHEET_PROPERTIES::OnInitDlg( wxInitDialogEvent& event )
894 {
895     TransferDataToWindow();
896 
897     // Now all widgets have the size fixed, call FinishDialogSettings
898     finishDialogSettings();
899 }
900