From 36d66085f5932295d3907f781aeb5eb9f8684f9b Mon Sep 17 00:00:00 2001 From: Ian McInerney Date: Wed, 28 Jul 2021 16:40:35 +0100 Subject: [PATCH] Add a column showing 3D model file loading errors This adds an icon to the left of the row with an error symbol if the file can't be found or opened, and shows a tooltip over the icon with and error string. Fixes https://gitlab.com/kicad/code/kicad/issues/3815 --- common/grid_tricks.cpp | 36 ++++++ common/widgets/grid_icon_text_helpers.cpp | 57 +++++++++ include/grid_tricks.h | 34 ++++- include/widgets/grid_icon_text_helpers.h | 22 ++++ .../dialog_footprint_properties_fp_editor.cpp | 119 +++++++++++++++--- .../dialog_footprint_properties_fp_editor.h | 11 ++ ...og_footprint_properties_fp_editor_base.cpp | 12 +- ...og_footprint_properties_fp_editor_base.fbp | 6 +- 8 files changed, 269 insertions(+), 28 deletions(-) diff --git a/common/grid_tricks.cpp b/common/grid_tricks.cpp index 10235bbfbc..39314cb0c2 100644 --- a/common/grid_tricks.cpp +++ b/common/grid_tricks.cpp @@ -23,6 +23,8 @@ */ #include +#include +#include #include #include #include @@ -58,6 +60,10 @@ GRID_TRICKS::GRID_TRICKS( WX_GRID* aGrid ): aGrid->Connect( wxEVT_KEY_DOWN, wxKeyEventHandler( GRID_TRICKS::onKeyDown ), nullptr, this ); aGrid->Connect( wxEVT_UPDATE_UI, wxUpdateUIEventHandler( GRID_TRICKS::onUpdateUI ), nullptr, this ); + + // The handlers that control the tooltips must be on the actual grid window, not the grid + aGrid->GetGridWindow()->Connect( wxEVT_MOTION, + wxMouseEventHandler( GRID_TRICKS::onGridMotion ), nullptr, this ); } @@ -165,6 +171,36 @@ void GRID_TRICKS::onGridCellLeftDClick( wxGridEvent& aEvent ) } +void GRID_TRICKS::onGridMotion( wxMouseEvent& aEvent ) +{ + // Always skip the event + aEvent.Skip(); + + wxPoint pt = aEvent.GetPosition(); + wxPoint pos = m_grid->CalcScrolledPosition( wxPoint( pt.x, pt.y ) ); + + int col = m_grid->XToCol( pos.x ); + + // Skip the event if the tooltip shouldn't be shown + if( !m_tooltipEnabled[col] || ( col == wxNOT_FOUND ) ) + { + m_grid->GetGridWindow()->SetToolTip( "" ); + return; + } + + int row = m_grid->YToRow( pos.y ); + + if( row == wxNOT_FOUND ) + { + m_grid->GetGridWindow()->SetToolTip( "" ); + return; + } + + // Set the tooltip to the string contained in the cell + m_grid->GetGridWindow()->SetToolTip( m_grid->GetCellValue( row, col ) ); +} + + bool GRID_TRICKS::handleDoubleClick( wxGridEvent& aEvent ) { // Double-click processing must be handled by specific sub-classes diff --git a/common/widgets/grid_icon_text_helpers.cpp b/common/widgets/grid_icon_text_helpers.cpp index 7bc0c2b629..8dfd1d2dff 100644 --- a/common/widgets/grid_icon_text_helpers.cpp +++ b/common/widgets/grid_icon_text_helpers.cpp @@ -23,6 +23,8 @@ #include +#include +#include #include #include @@ -121,6 +123,61 @@ wxGridCellRenderer* GRID_CELL_ICON_RENDERER::Clone() const } +//---- Grid helpers: custom wxGridCellRenderer that renders just an icon ---------------- +// +// Note: this renderer is supposed to be used with read only cells + +GRID_CELL_STATUS_ICON_RENDERER::GRID_CELL_STATUS_ICON_RENDERER( int aStatus ) : + m_status( aStatus ) +{ + if( m_status != 0 ) + { + m_bitmap = wxArtProvider::GetBitmap( wxArtProvider::GetMessageBoxIconId( m_status ), + wxART_BUTTON ); + } + else + { + // Dummy bitmap for size + m_bitmap = wxArtProvider::GetBitmap( wxArtProvider::GetMessageBoxIconId( wxICON_INFORMATION ), + wxART_BUTTON ); + } +} + + +void GRID_CELL_STATUS_ICON_RENDERER::Draw( wxGrid& aGrid, wxGridCellAttr& aAttr, wxDC& aDC, + const wxRect& aRect, int aRow, int aCol, + bool isSelected ) +{ + wxRect rect = aRect; + rect.Inflate( -1 ); + + // erase background + wxGridCellRenderer::Draw( aGrid, aAttr, aDC, aRect, aRow, aCol, isSelected ); + + // Draw icon + if( ( m_status != 0 ) && m_bitmap.IsOk() ) + { + aDC.DrawBitmap( m_bitmap, + rect.GetLeft() + ( rect.GetWidth() - m_bitmap.GetWidth() ) / 2, + rect.GetTop() + ( rect.GetHeight() - m_bitmap.GetHeight() ) / 2, + true ); + } +} + + +wxSize GRID_CELL_STATUS_ICON_RENDERER::GetBestSize( wxGrid& grid, wxGridCellAttr& attr, wxDC& dc, + int row, int col ) +{ + return wxSize( m_bitmap.GetWidth() + 6, m_bitmap.GetHeight() + 4 ); +} + + +wxGridCellRenderer* GRID_CELL_STATUS_ICON_RENDERER::Clone() const +{ + return new GRID_CELL_STATUS_ICON_RENDERER( m_status ); +} + + GRID_CELL_ICON_TEXT_POPUP::GRID_CELL_ICON_TEXT_POPUP( const std::vector& icons, const wxArrayString& names ) : m_icons( icons ), diff --git a/include/grid_tricks.h b/include/grid_tricks.h index df15a3c5d3..658ade0cb0 100644 --- a/include/grid_tricks.h +++ b/include/grid_tricks.h @@ -26,11 +26,14 @@ #define _GRID_TRICKS_H_ +#include #include #include #include #include +#define GRIDTRICKS_MAX_COL 20 + enum { GRIDTRICKS_FIRST_ID = 901, @@ -40,9 +43,9 @@ enum GRIDTRICKS_ID_PASTE, GRIDTRICKS_ID_SELECT, - GRIDTRICKS_FIRST_SHOWHIDE = 979, // reserve 20 IDs for show/hide-column-n + GRIDTRICKS_FIRST_SHOWHIDE = 979, // reserve IDs for show/hide-column-n - GRIDTRICKS_LAST_ID = 999 + GRIDTRICKS_LAST_ID = GRIDTRICKS_FIRST_SHOWHIDE + GRIDTRICKS_MAX_COL }; @@ -54,6 +57,30 @@ class GRID_TRICKS : public wxEvtHandler public: explicit GRID_TRICKS( WX_GRID* aGrid ); + /** + * Enable the tooltip for a column. + * + * The tooltip is read from the string contained in the cell data. + * + * @param aCol is the column to use + * @param aEnable is true to enable the tooltip (default) + */ + void SetTooltipEnable( int aCol, bool aEnable = true ) + { + m_tooltipEnabled[aCol] = aEnable; + } + + /** + * Query if the tooltip for a column is enabled + * + * @param aCol is the column to query + * @return if the tooltip is enabled for the column + */ + bool GetTooltipEnabled( int aCol ) + { + return m_tooltipEnabled[aCol]; + } + protected: /// Puts the selected area into a sensible rectangle of m_sel_{row,col}_{start,count} above. void getSelectedArea(); @@ -66,6 +93,7 @@ protected: void onPopupSelection( wxCommandEvent& event ); void onKeyDown( wxKeyEvent& ev ); void onUpdateUI( wxUpdateUIEvent& event ); + void onGridMotion( wxMouseEvent& event ); virtual bool handleDoubleClick( wxGridEvent& aEvent ); virtual void showPopupMenu( wxMenu& menu ); @@ -86,6 +114,8 @@ protected: int m_sel_col_start; int m_sel_row_count; int m_sel_col_count; + + std::bitset m_tooltipEnabled; }; #endif // _GRID_TRICKS_H_ diff --git a/include/widgets/grid_icon_text_helpers.h b/include/widgets/grid_icon_text_helpers.h index b01985bbdf..120df0f141 100644 --- a/include/widgets/grid_icon_text_helpers.h +++ b/include/widgets/grid_icon_text_helpers.h @@ -24,6 +24,7 @@ #ifndef GRID_ICON_TEXT_HELPERS_H #define GRID_ICON_TEXT_HELPERS_H +#include #include #include #include @@ -67,6 +68,27 @@ private: const wxBitmap& m_icon; }; +//---- Grid helpers: custom wxGridCellRenderer that renders just an icon from wxArtprovider - +// +// Note: use with read only cells + +class GRID_CELL_STATUS_ICON_RENDERER : public wxGridCellRenderer +{ +public: + GRID_CELL_STATUS_ICON_RENDERER( int aStatus ); + + void Draw( wxGrid& aGrid, wxGridCellAttr& aAttr, wxDC& aDC, + const wxRect& aRect, int aRow, int aCol, bool isSelected ) override; + wxSize GetBestSize( wxGrid & grid, wxGridCellAttr & attr, wxDC & dc, int row, int col ) override; + wxGridCellRenderer* Clone() const override; + +private: + int m_status; + wxBitmap m_bitmap; +}; + + + //---- Grid helpers: custom wxGridCellEditor ------------------------------------------ // // Note: this implementation is an adaptation of wxGridCellChoiceEditor diff --git a/pcbnew/dialogs/dialog_footprint_properties_fp_editor.cpp b/pcbnew/dialogs/dialog_footprint_properties_fp_editor.cpp index 3b947b1d65..4d91037f2e 100644 --- a/pcbnew/dialogs/dialog_footprint_properties_fp_editor.cpp +++ b/pcbnew/dialogs/dialog_footprint_properties_fp_editor.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +53,12 @@ // Remember the last open page during session. int DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::m_page = 0; +enum MODELS_TABLE_COLUMNS +{ + COL_PROBLEM = 0, + COL_FILENAME = 1, + COL_SHOWN = 2 +}; DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR( FOOTPRINT_EDIT_FRAME* aParent, @@ -87,7 +94,11 @@ DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR( m_itemsGrid->SetTable( m_texts ); m_itemsGrid->PushEventHandler( new GRID_TRICKS( m_itemsGrid ) ); - m_modelsGrid->PushEventHandler( new GRID_TRICKS( m_modelsGrid ) ); + + GRID_TRICKS* trick = new GRID_TRICKS( m_modelsGrid ); + trick->SetTooltipEnable( COL_PROBLEM ); + + m_modelsGrid->PushEventHandler( trick ); // Show/hide columns according to the user's preference m_itemsGrid->ShowHideColumns( m_frame->GetSettings()->m_FootprintTextShownColumns ); @@ -99,17 +110,23 @@ DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR( wxGetEnv( KICAD6_3DMODEL_DIR, &cfg->m_lastFootprint3dDir ); } + // Icon showing warning/error information wxGridCellAttr* attr = new wxGridCellAttr; + attr->SetReadOnly(); + m_modelsGrid->SetColAttr( COL_PROBLEM, attr ); + + // Filename + attr = new wxGridCellAttr; attr->SetEditor( new GRID_CELL_PATH_EDITOR( this, m_modelsGrid, &cfg->m_lastFootprint3dDir, "*.*", true, Prj().GetProjectPath() ) ); - m_modelsGrid->SetColAttr( 0, attr ); + m_modelsGrid->SetColAttr( COL_FILENAME, attr ); // Show checkbox attr = new wxGridCellAttr; attr->SetRenderer( new wxGridCellBoolRenderer() ); attr->SetReadOnly(); // not really; we delegate interactivity to GRID_TRICKS attr->SetAlignment( wxALIGN_CENTER, wxALIGN_CENTER ); - m_modelsGrid->SetColAttr( 1, attr ); + m_modelsGrid->SetColAttr( COL_SHOWN, attr ); m_modelsGrid->SetWindowStyleFlag( m_modelsGrid->GetWindowStyle() & ~wxHSCROLL ); aParent->Prj().Get3DCacheManager()->GetResolver()->SetProgramBase( &Pgm() ); @@ -313,8 +330,11 @@ bool DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::TransferDataToWindow() m_modelsGrid->AppendRows( 1 ); int row = m_modelsGrid->GetNumberRows() - 1; - m_modelsGrid->SetCellValue( row, 0, origPath ); - m_modelsGrid->SetCellValue( row, 1, model.m_Show ? wxT( "1" ) : wxT( "0" ) ); + m_modelsGrid->SetCellValue( row, COL_FILENAME, origPath ); + m_modelsGrid->SetCellValue( row, COL_SHOWN, model.m_Show ? wxT( "1" ) : wxT( "0" ) ); + + // Must be after the filename is set + updateValidateStatus( row ); } select3DModel( 0 ); // will clamp idx within bounds @@ -344,7 +364,7 @@ bool DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::TransferDataToWindow() } m_itemsGrid->SetRowLabelSize( m_itemsGrid->GetVisibleWidth( -1, true, true, true ) ); - m_modelsGrid->SetColSize( 1, m_modelsGrid->GetVisibleWidth( 1, true, false, false ) ); + m_modelsGrid->SetColSize( COL_SHOWN, m_modelsGrid->GetVisibleWidth( COL_SHOWN, true, false, false ) ); Layout(); adjustGridColumns( m_itemsGrid->GetRect().GetWidth() ); @@ -363,7 +383,7 @@ void DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::select3DModel( int aModelIdx ) if( m_modelsGrid->GetNumberRows() ) { m_modelsGrid->SelectRow( aModelIdx ); - m_modelsGrid->SetGridCursor( aModelIdx, 0 ); + m_modelsGrid->SetGridCursor( aModelIdx, COL_FILENAME ); } m_previewPane->SetSelectedModel( aModelIdx ); @@ -381,11 +401,11 @@ void DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::On3DModelSelected( wxGridEvent& aEve void DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::On3DModelCellChanged( wxGridEvent& aEvent ) { - if( aEvent.GetCol() == 0 ) + if( aEvent.GetCol() == COL_FILENAME ) { bool hasAlias = false; FILENAME_RESOLVER* res = Prj().Get3DCacheManager()->GetResolver(); - wxString filename = m_modelsGrid->GetCellValue( aEvent.GetRow(), 0 ); + wxString filename = m_modelsGrid->GetCellValue( aEvent.GetRow(), COL_FILENAME ); filename.Replace( "\n", "" ); filename.Replace( "\r", "" ); @@ -411,11 +431,13 @@ void DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::On3DModelCellChanged( wxGridEvent& a #endif m_shapes3D_list[ aEvent.GetRow() ].m_Filename = filename; - m_modelsGrid->SetCellValue( aEvent.GetRow(), 0, filename ); + m_modelsGrid->SetCellValue( aEvent.GetRow(), COL_FILENAME, filename ); + + updateValidateStatus( aEvent.GetRow() ); } - else if( aEvent.GetCol() == 1 ) + else if( aEvent.GetCol() == COL_SHOWN ) { - wxString showValue = m_modelsGrid->GetCellValue( aEvent.GetRow(), 1 ); + wxString showValue = m_modelsGrid->GetCellValue( aEvent.GetRow(), COL_SHOWN ); m_shapes3D_list[ aEvent.GetRow() ].m_Show = ( showValue == wxT( "1" ) ); } @@ -501,8 +523,10 @@ void DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::OnAdd3DModel( wxCommandEvent& ) int idx = m_modelsGrid->GetNumberRows(); m_modelsGrid->AppendRows( 1 ); - m_modelsGrid->SetCellValue( idx, 0, filename ); - m_modelsGrid->SetCellValue( idx, 1, wxT( "1" ) ); + m_modelsGrid->SetCellValue( idx, COL_FILENAME, filename ); + m_modelsGrid->SetCellValue( idx, COL_SHOWN, wxT( "1" ) ); + + updateValidateStatus( idx ); select3DModel( idx ); m_previewPane->UpdateDummyFootprint(); @@ -521,13 +545,14 @@ void DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::OnAdd3DRow( wxCommandEvent& ) int row = m_modelsGrid->GetNumberRows(); m_modelsGrid->AppendRows( 1 ); - m_modelsGrid->SetCellValue( row, 1, wxT( "1" ) ); + m_modelsGrid->SetCellValue( row, COL_SHOWN, wxT( "1" ) ); + m_modelsGrid->SetCellValue( row, COL_PROBLEM, "" ); select3DModel( row ); m_modelsGrid->SetFocus(); - m_modelsGrid->MakeCellVisible( row, 0 ); - m_modelsGrid->SetGridCursor( row, 0 ); + m_modelsGrid->MakeCellVisible( row, COL_FILENAME ); + m_modelsGrid->SetGridCursor( row, COL_FILENAME ); m_modelsGrid->EnableCellEditControl( true ); m_modelsGrid->ShowCellEditControl(); @@ -552,6 +577,59 @@ bool DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::checkFootprintName( const wxString& } +void DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::updateValidateStatus( int aRow ) +{ + int icon = 0; + wxString errStr; + + switch( validateModelExists( m_modelsGrid->GetCellValue( aRow, COL_FILENAME) ) ) + { + case MODEL_VALIDATE_ERRORS::NO_ERROR: + icon = 0; + errStr = ""; + break; + + case MODEL_VALIDATE_ERRORS::RESOLVE_FAIL: + icon = wxICON_ERROR; + errStr = _( "File not found" ); + break; + + case MODEL_VALIDATE_ERRORS::OPEN_FAIL: + icon = wxICON_ERROR; + errStr = _( "Unable to open file" ); + break; + + default: + icon = wxICON_ERROR; + errStr = _( "Unknown error" ); + break; + } + + m_modelsGrid->SetCellValue( aRow, COL_PROBLEM, errStr ); + m_modelsGrid->SetCellRenderer( aRow, COL_PROBLEM, + new GRID_CELL_STATUS_ICON_RENDERER( icon ) ); +} + + +MODEL_VALIDATE_ERRORS DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::validateModelExists( const wxString& aFilename ) +{ + FILENAME_RESOLVER* resolv = Prj().Get3DFilenameResolver(); + + if( !resolv ) + return MODEL_VALIDATE_ERRORS::RESOLVE_FAIL; + + wxString fullPath = resolv->ResolvePath( aFilename ); + + if( fullPath.IsEmpty() ) + return MODEL_VALIDATE_ERRORS::RESOLVE_FAIL; + + if( wxFileName::IsFileReadable( fullPath ) ) + return MODEL_VALIDATE_ERRORS::NO_ERROR; + else + return MODEL_VALIDATE_ERRORS::OPEN_FAIL; +} + + bool DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::Validate() { if( !m_itemsGrid->CommitPendingChanges() ) @@ -829,10 +907,15 @@ void DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::adjustGridColumns( int aWidth ) itemsWidth -= m_itemsGrid->GetColSize( i ); if( itemsWidth > 0 ) + { m_itemsGrid->SetColSize( 0, std::max( itemsWidth, m_itemsGrid->GetVisibleWidth( 0, true, false, false ) ) ); + } - m_modelsGrid->SetColSize( 0, modelsWidth - m_modelsGrid->GetColSize( 1 ) - 5 ); + int width = modelsWidth - m_modelsGrid->GetColSize( COL_SHOWN ) + - m_modelsGrid->GetColSize( COL_PROBLEM ) - 5; + + m_modelsGrid->SetColSize( COL_FILENAME, width ); } diff --git a/pcbnew/dialogs/dialog_footprint_properties_fp_editor.h b/pcbnew/dialogs/dialog_footprint_properties_fp_editor.h index da36064a0b..a8076255e3 100644 --- a/pcbnew/dialogs/dialog_footprint_properties_fp_editor.h +++ b/pcbnew/dialogs/dialog_footprint_properties_fp_editor.h @@ -36,6 +36,13 @@ class PANEL_PREVIEW_3D_MODEL; class FOOTPRINT_EDIT_FRAME; +enum class MODEL_VALIDATE_ERRORS +{ + NO_ERROR, + RESOLVE_FAIL, + OPEN_FAIL +}; + class DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR : public DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR_BASE { public: @@ -62,6 +69,10 @@ private: void OnDeleteField( wxCommandEvent& event ) override; void OnUpdateUI( wxUpdateUIEvent& event ) override; + void updateValidateStatus( int aRow ); + + MODEL_VALIDATE_ERRORS validateModelExists( const wxString& aFilename ); + bool checkFootprintName( const wxString& aFootprintName ); void select3DModel( int aModelIdx ); diff --git a/pcbnew/dialogs/dialog_footprint_properties_fp_editor_base.cpp b/pcbnew/dialogs/dialog_footprint_properties_fp_editor_base.cpp index a6440fa778..ada23c1bac 100644 --- a/pcbnew/dialogs/dialog_footprint_properties_fp_editor_base.cpp +++ b/pcbnew/dialogs/dialog_footprint_properties_fp_editor_base.cpp @@ -341,20 +341,22 @@ DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR_BASE::DIALOG_FOOTPRINT_PROPERTIES_FP_EDITO m_modelsGrid = new WX_GRID( sbSizer3->GetStaticBox(), wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_SIMPLE ); // Grid - m_modelsGrid->CreateGrid( 3, 2 ); + m_modelsGrid->CreateGrid( 3, 3 ); m_modelsGrid->EnableEditing( true ); m_modelsGrid->EnableGridLines( false ); m_modelsGrid->EnableDragGridSize( false ); m_modelsGrid->SetMargins( 0, 0 ); // Columns - m_modelsGrid->SetColSize( 0, 650 ); - m_modelsGrid->SetColSize( 1, 65 ); + m_modelsGrid->SetColSize( 0, 20 ); + m_modelsGrid->SetColSize( 1, 650 ); + m_modelsGrid->SetColSize( 2, 65 ); m_modelsGrid->EnableDragColMove( false ); m_modelsGrid->EnableDragColSize( false ); m_modelsGrid->SetColLabelSize( 22 ); - m_modelsGrid->SetColLabelValue( 0, _("3D Model(s)") ); - m_modelsGrid->SetColLabelValue( 1, _("Show") ); + m_modelsGrid->SetColLabelValue( 0, wxEmptyString ); + m_modelsGrid->SetColLabelValue( 1, _("3D Model(s)") ); + m_modelsGrid->SetColLabelValue( 2, _("Show") ); m_modelsGrid->SetColLabelAlignment( wxALIGN_LEFT, wxALIGN_CENTER ); // Rows diff --git a/pcbnew/dialogs/dialog_footprint_properties_fp_editor_base.fbp b/pcbnew/dialogs/dialog_footprint_properties_fp_editor_base.fbp index cd46e03ffd..df7eabd4b7 100644 --- a/pcbnew/dialogs/dialog_footprint_properties_fp_editor_base.fbp +++ b/pcbnew/dialogs/dialog_footprint_properties_fp_editor_base.fbp @@ -2888,10 +2888,10 @@ 1 wxALIGN_LEFT 22 - "3D Model(s)" "Show" + "" "3D Model(s)" "Show" wxALIGN_CENTER - 2 - 650,65 + 3 + 20,650,65 1 0