Files
kicad-source-mirror/pcbnew/dialogs/panel_fp_properties_3d_model.cpp
Seth Hillbrand 0d86b88008 Unify embedded files handling btw editors
Each editor needs the embedded files tab in the
schematic/pcb/symbol/footprint settings.  But the footprint may add an
embedded file from the 3d models tab and symbols/footprints may add
embedded files from the grid.  This should be immediately visible in the
embedded files tab.  Additionally, removing the reference to an embedded
file in the grid or 3d models now removes the embedded file as well.

Fixes https://gitlab.com/kicad/code/kicad/-/issues/18934
2025-01-28 12:22:39 -08:00

609 lines
19 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2018 Jean-Pierre Charras, jp.charras at wanadoo.fr
* Copyright (C) 2015 Dick Hollenbeck, [email protected]
* Copyright (C) 2008 Wayne Stambaugh <[email protected]>
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <panel_fp_properties_3d_model.h>
#include <3d_viewer/eda_3d_viewer_frame.h>
#include <dialogs/dialog_configure_paths.h>
#include <env_vars.h>
#include <bitmaps.h>
#include <widgets/grid_icon_text_helpers.h>
#include <widgets/grid_text_button_helpers.h>
#include <widgets/wx_grid.h>
#include <widgets/std_bitmap_button.h>
#include <footprint.h>
#include <fp_lib_table.h>
#include <footprint.h>
#include <footprint_edit_frame.h>
#include <footprint_editor_settings.h>
#include <dialog_footprint_properties_fp_editor.h>
#include "filename_resolver.h"
#include <pgm_base.h>
#include <kiplatform/ui.h>
#include <dialogs/panel_preview_3d_model.h>
#include <dialogs/dialog_select_3d_model.h>
#include <dialogs/panel_embedded_files.h>
#include <settings/settings_manager.h>
#include <project_pcb.h>
#include <wx/defs.h>
#include <wx/msgdlg.h>
enum MODELS_TABLE_COLUMNS
{
COL_PROBLEM = 0,
COL_FILENAME = 1,
COL_SHOWN = 2
};
wxDEFINE_EVENT( wxCUSTOM_PANEL_SHOWN_EVENT, wxCommandEvent );
PANEL_FP_PROPERTIES_3D_MODEL::PANEL_FP_PROPERTIES_3D_MODEL( PCB_BASE_EDIT_FRAME* aFrame,
FOOTPRINT* aFootprint,
DIALOG_SHIM* aDialogParent,
PANEL_EMBEDDED_FILES* aFilesPanel,
wxWindow* aParent, wxWindowID aId,
const wxPoint& aPos,
const wxSize& aSize, long aStyle,
const wxString& aName ) :
PANEL_FP_PROPERTIES_3D_MODEL_BASE( aParent, aId, aPos, aSize, aStyle, aName ),
m_parentDialog( aDialogParent ),
m_frame( aFrame ),
m_footprint( aFootprint ),
m_filesPanel( aFilesPanel ),
m_inSelect( false )
{
m_modelsGrid->SetDefaultRowSize( m_modelsGrid->GetDefaultRowSize() + 4 );
GRID_TRICKS* trick = new GRID_TRICKS( m_modelsGrid, [this]( wxCommandEvent& aEvent )
{
OnAdd3DRow( aEvent );
} );
trick->SetTooltipEnable( COL_PROBLEM );
m_modelsGrid->PushEventHandler( trick );
// Get the last 3D directory
SETTINGS_MANAGER& mgr = Pgm().GetSettingsManager();
PCBNEW_SETTINGS* cfg = mgr.GetAppSettings<PCBNEW_SETTINGS>( "pcbnew" );
if( cfg->m_lastFootprint3dDir.IsEmpty() )
{
wxGetEnv( ENV_VAR::GetVersionedEnvVarName( wxS( "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( m_parentDialog, m_modelsGrid,
&cfg->m_lastFootprint3dDir, wxT( "*.*" ), true,
m_frame->Prj().GetProjectPath() ) );
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( COL_SHOWN, attr );
m_modelsGrid->SetWindowStyleFlag( m_modelsGrid->GetWindowStyle() & ~wxHSCROLL );
PROJECT_PCB::Get3DCacheManager( &m_frame->Prj() )->GetResolver()->SetProgramBase( &Pgm() );
m_previewPane = new PANEL_PREVIEW_3D_MODEL( m_lowerPanel, m_frame, m_footprint,
&m_shapes3D_list );
m_LowerSizer3D->Add( m_previewPane, 1, wxEXPAND, 5 );
// Configure button logos
m_button3DShapeAdd->SetBitmap( KiBitmapBundle( BITMAPS::small_plus ) );
m_button3DShapeBrowse->SetBitmap( KiBitmapBundle( BITMAPS::small_folder ) );
m_button3DShapeRemove->SetBitmap( KiBitmapBundle( BITMAPS::small_trash ) );
Bind( wxEVT_SHOW, &PANEL_FP_PROPERTIES_3D_MODEL::onShowEvent, this );
m_parentDialog->Bind( wxEVT_ACTIVATE, &PANEL_FP_PROPERTIES_3D_MODEL::onDialogActivateEvent,
this );
}
PANEL_FP_PROPERTIES_3D_MODEL::~PANEL_FP_PROPERTIES_3D_MODEL()
{
// Delete the GRID_TRICKS.
m_modelsGrid->PopEventHandler( true );
// Unbind OnShowEvent to prevent unnecessary event handling.
Unbind( wxEVT_SHOW, &PANEL_FP_PROPERTIES_3D_MODEL::onShowEvent, this );
// free the memory used by all models, otherwise models which were
// browsed but not used would consume memory
PROJECT_PCB::Get3DCacheManager( &m_frame->Prj() )->FlushCache( false );
delete m_previewPane;
}
bool PANEL_FP_PROPERTIES_3D_MODEL::TransferDataToWindow()
{
ReloadModelsFromFootprint();
return true;
}
bool PANEL_FP_PROPERTIES_3D_MODEL::TransferDataFromWindow()
{
return m_modelsGrid->CommitPendingChanges();
}
void PANEL_FP_PROPERTIES_3D_MODEL::ReloadModelsFromFootprint()
{
wxString default_path;
wxGetEnv( ENV_VAR::GetVersionedEnvVarName( wxS( "3DMODEL_DIR" ) ), &default_path );
#ifdef __WINDOWS__
default_path.Replace( wxT( "/" ), wxT( "\\" ) );
#endif
m_shapes3D_list.clear();
m_modelsGrid->ClearRows();
wxString origPath, alias, shortPath;
FILENAME_RESOLVER* res = PROJECT_PCB::Get3DCacheManager( &m_frame->Prj() )->GetResolver();
for( const FP_3DMODEL& model : m_footprint->Models() )
{
m_shapes3D_list.push_back( model );
origPath = model.m_Filename;
if( res && res->SplitAlias( origPath, alias, shortPath ) )
origPath = alias + wxT( ":" ) + shortPath;
m_modelsGrid->AppendRows( 1 );
int row = m_modelsGrid->GetNumberRows() - 1;
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 );
m_previewPane->UpdateDummyFootprint();
m_modelsGrid->SetColSize( COL_SHOWN, m_modelsGrid->GetVisibleWidth( COL_SHOWN, true, false ) );
Layout();
}
void PANEL_FP_PROPERTIES_3D_MODEL::select3DModel( int aModelIdx )
{
m_inSelect = true;
aModelIdx = std::max( 0, aModelIdx );
aModelIdx = std::min( aModelIdx, m_modelsGrid->GetNumberRows() - 1 );
if( m_modelsGrid->GetNumberRows() )
{
m_modelsGrid->SelectRow( aModelIdx );
m_modelsGrid->SetGridCursor( aModelIdx, COL_FILENAME );
}
m_previewPane->SetSelectedModel( aModelIdx );
m_inSelect = false;
}
void PANEL_FP_PROPERTIES_3D_MODEL::On3DModelSelected( wxGridEvent& aEvent )
{
if( !m_inSelect )
select3DModel( aEvent.GetRow() );
}
void PANEL_FP_PROPERTIES_3D_MODEL::On3DModelCellChanged( wxGridEvent& aEvent )
{
if( aEvent.GetCol() == COL_FILENAME )
{
bool hasAlias = false;
FILENAME_RESOLVER* res = PROJECT_PCB::Get3DCacheManager( &m_frame->Prj() )->GetResolver();
wxString filename = m_modelsGrid->GetCellValue( aEvent.GetRow(), COL_FILENAME );
// Perform cleanup and validation on the filename if it isn't empty
if( !filename.empty() )
{
filename.Replace( wxT( "\n" ), wxT( "" ) );
filename.Replace( wxT( "\r" ), wxT( "" ) );
filename.Replace( wxT( "\t" ), wxT( "" ) );
res->ValidateFileName( filename, hasAlias );
// If the user has specified an alias in the name then prepend ':'
if( hasAlias )
filename.insert( 0, wxT( ":" ) );
#ifdef __WINDOWS__
// In KiCad files, filenames and paths are stored using Unix notation
filename.Replace( wxT( "\\" ), wxT( "/" ) );
#endif
// Update the grid with the modified filename
m_modelsGrid->SetCellValue( aEvent.GetRow(), COL_FILENAME, filename );
}
// Save the filename in the 3D shapes table
m_shapes3D_list[ aEvent.GetRow() ].m_Filename = filename;
// Update the validation status
updateValidateStatus( aEvent.GetRow() );
}
else if( aEvent.GetCol() == COL_SHOWN )
{
wxString showValue = m_modelsGrid->GetCellValue( aEvent.GetRow(), COL_SHOWN );
m_shapes3D_list[ aEvent.GetRow() ].m_Show = ( showValue == wxT( "1" ) );
}
m_previewPane->UpdateDummyFootprint();
onModify();
}
void PANEL_FP_PROPERTIES_3D_MODEL::OnRemove3DModel( wxCommandEvent& )
{
if( !m_modelsGrid->CommitPendingChanges() )
return;
int idx = m_modelsGrid->GetGridCursorRow();
if( idx >= 0 && m_modelsGrid->GetNumberRows() && !m_shapes3D_list.empty() )
{
// Don't allow selection until we call select3DModel(), below. Otherwise wxWidgets
// has a tendency to get its knickers in a knot....
m_inSelect = true;
// Not all files are embedded but this will ignore the ones that are not
m_filesPanel->RemoveEmbeddedFile( m_shapes3D_list[ idx ].m_Filename );
m_shapes3D_list.erase( m_shapes3D_list.begin() + idx );
m_modelsGrid->DeleteRows( idx );
select3DModel( idx ); // will clamp idx within bounds
m_previewPane->UpdateDummyFootprint();
}
onModify();
}
void PANEL_FP_PROPERTIES_3D_MODEL::OnAdd3DModel( wxCommandEvent& )
{
if( !m_modelsGrid->CommitPendingChanges() )
return;
int selected = m_modelsGrid->GetGridCursorRow();
PROJECT& prj = m_frame->Prj();
FP_3DMODEL model;
S3D_CACHE* cache = PROJECT_PCB::Get3DCacheManager( &prj );
FILENAME_RESOLVER* res = cache->GetResolver();
wxString initialpath = prj.GetRString( PROJECT::VIEWER_3D_PATH );
wxString sidx = prj.GetRString( PROJECT::VIEWER_3D_FILTER_INDEX );
int filter = 0;
// If the PROJECT::VIEWER_3D_PATH hasn't been set yet, use the 3DMODEL_DIR environment
// variable and fall back to the project path if necessary.
if( initialpath.IsEmpty() )
{
if( !wxGetEnv( ENV_VAR::GetVersionedEnvVarName( wxS( "3DMODEL_DIR" ) ), &initialpath )
|| initialpath.IsEmpty() )
{
initialpath = prj.GetProjectPath();
}
}
if( !sidx.empty() )
{
long tmp;
sidx.ToLong( &tmp );
if( tmp > 0 && tmp <= INT_MAX )
filter = (int) tmp;
}
DIALOG_SELECT_3DMODEL dm( m_parentDialog, cache, &model, initialpath, filter );
// Use QuasiModal so that Configure3DPaths (and its help window) will work
int retval = dm.ShowQuasiModal();
if( retval != wxID_OK || model.m_Filename.empty() )
{
if( selected >= 0 )
{
select3DModel( selected );
updateValidateStatus( selected );
}
return;
}
if( dm.IsEmbedded3DModel() )
{
wxString libraryName = m_footprint->GetFPID().GetLibNickname();
const FP_LIB_TABLE_ROW* fpRow = nullptr;
wxString footprintBasePath = wxEmptyString;
try
{
fpRow = PROJECT_PCB::PcbFootprintLibs( &m_frame->Prj() )->FindRow( libraryName, false );
if( fpRow )
footprintBasePath = fpRow->GetFullURI( true );
}
catch( ... )
{
// if libraryName is not found in table, do nothing
}
wxString fullPath = res->ResolvePath( model.m_Filename, footprintBasePath, nullptr );
wxFileName fname( fullPath );
EMBEDDED_FILES::EMBEDDED_FILE* result = m_filesPanel->AddEmbeddedFile( fname.GetFullPath() ); ;
if( !result )
{
wxString msg = wxString::Format( _( "Error adding 3D model" ) );
wxMessageBox( msg, _( "Error" ), wxICON_ERROR | wxOK, this );
return;
}
model.m_Filename = result->GetLink();
}
prj.SetRString( PROJECT::VIEWER_3D_PATH, initialpath );
sidx = wxString::Format( wxT( "%i" ), filter );
prj.SetRString( PROJECT::VIEWER_3D_FILTER_INDEX, sidx );
wxString alias;
wxString shortPath;
wxString filename = model.m_Filename;
if( res && res->SplitAlias( filename, alias, shortPath ) )
filename = alias + wxT( ":" ) + shortPath;
#ifdef __WINDOWS__
// In KiCad files, filenames and paths are stored using Unix notation
model.m_Filename.Replace( wxT( "\\" ), wxT( "/" ) );
#endif
model.m_Show = true;
m_shapes3D_list.push_back( model );
int idx = m_modelsGrid->GetNumberRows();
m_modelsGrid->AppendRows( 1 );
m_modelsGrid->SetCellValue( idx, COL_FILENAME, filename );
m_modelsGrid->SetCellValue( idx, COL_SHOWN, wxT( "1" ) );
select3DModel( idx );
updateValidateStatus( idx );
m_previewPane->UpdateDummyFootprint();
onModify();
}
void PANEL_FP_PROPERTIES_3D_MODEL::OnAdd3DRow( wxCommandEvent& )
{
if( !m_modelsGrid->CommitPendingChanges() )
return;
FP_3DMODEL model;
model.m_Show = true;
m_shapes3D_list.push_back( model );
int row = m_modelsGrid->GetNumberRows();
m_modelsGrid->AppendRows( 1 );
m_modelsGrid->SetCellValue( row, COL_SHOWN, wxT( "1" ) );
m_modelsGrid->SetCellValue( row, COL_PROBLEM, "" );
select3DModel( row );
m_modelsGrid->SetFocus();
m_modelsGrid->MakeCellVisible( row, COL_FILENAME );
m_modelsGrid->SetGridCursor( row, COL_FILENAME );
m_modelsGrid->EnableCellEditControl( true );
m_modelsGrid->ShowCellEditControl();
updateValidateStatus( row );
onModify();
}
void PANEL_FP_PROPERTIES_3D_MODEL::updateValidateStatus( int aRow )
{
int icon = 0;
wxString errStr;
switch( validateModelExists( m_modelsGrid->GetCellValue( aRow, COL_FILENAME) ) )
{
case MODEL_VALIDATE_ERRORS::MODEL_NO_ERROR:
icon = 0;
errStr = "";
break;
case MODEL_VALIDATE_ERRORS::NO_FILENAME:
icon = wxICON_WARNING;
errStr = _( "No filename entered" );
break;
case MODEL_VALIDATE_ERRORS::ILLEGAL_FILENAME:
icon = wxICON_ERROR;
errStr = _( "Illegal filename" );
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 PANEL_FP_PROPERTIES_3D_MODEL::validateModelExists( const wxString& aFilename )
{
if( aFilename.empty() )
return MODEL_VALIDATE_ERRORS::NO_FILENAME;
bool hasAlias = false;
FILENAME_RESOLVER* resolv = PROJECT_PCB::Get3DFilenameResolver( &m_frame->Prj() );
if( !resolv )
return MODEL_VALIDATE_ERRORS::RESOLVE_FAIL;
if( !resolv->ValidateFileName( aFilename, hasAlias ) )
return MODEL_VALIDATE_ERRORS::ILLEGAL_FILENAME;
wxString libraryName = m_footprint->GetFPID().GetLibNickname();
const FP_LIB_TABLE_ROW* fpRow = nullptr;
try
{
fpRow = PROJECT_PCB::PcbFootprintLibs( &m_frame->Prj() )->FindRow( libraryName, false );
}
catch( ... )
{
// if libraryName is not found in table, do nothing
}
wxString footprintBasePath = wxEmptyString;
if( fpRow )
footprintBasePath = fpRow->GetFullURI( true );
wxString fullPath = resolv->ResolvePath( aFilename, footprintBasePath, m_footprint );
if( fullPath.IsEmpty() )
return MODEL_VALIDATE_ERRORS::RESOLVE_FAIL;
if( !wxFileName::IsFileReadable( fullPath ) )
return MODEL_VALIDATE_ERRORS::OPEN_FAIL;
return MODEL_VALIDATE_ERRORS::MODEL_NO_ERROR;
}
void PANEL_FP_PROPERTIES_3D_MODEL::Cfg3DPath( wxCommandEvent& event )
{
DIALOG_CONFIGURE_PATHS dlg( this );
if( dlg.ShowQuasiModal() == wxID_OK )
m_previewPane->UpdateDummyFootprint();
}
void PANEL_FP_PROPERTIES_3D_MODEL::AdjustGridColumnWidths()
{
// Account for scroll bars
int modelsWidth = KIPLATFORM::UI::GetUnobscuredSize( m_modelsGrid ).x;
int width = modelsWidth - m_modelsGrid->GetColSize( COL_SHOWN )
- m_modelsGrid->GetColSize( COL_PROBLEM );
if( width > 0 )
m_modelsGrid->SetColSize( COL_FILENAME, width );
}
void PANEL_FP_PROPERTIES_3D_MODEL::OnGridSize( wxSizeEvent& event )
{
AdjustGridColumnWidths();
event.Skip();
}
void PANEL_FP_PROPERTIES_3D_MODEL::OnUpdateUI( wxUpdateUIEvent& event )
{
m_button3DShapeRemove->Enable( m_modelsGrid->GetNumberRows() > 0 );
}
void PANEL_FP_PROPERTIES_3D_MODEL::onModify()
{
if( DIALOG_SHIM* dlg = dynamic_cast<DIALOG_SHIM*>( wxGetTopLevelParent( this ) ) )
dlg->OnModify();
}
void PANEL_FP_PROPERTIES_3D_MODEL::onShowEvent( wxShowEvent& aEvent )
{
postCustomPanelShownEventWithPredicate( static_cast<int>( aEvent.IsShown() ) );
aEvent.Skip();
}
void PANEL_FP_PROPERTIES_3D_MODEL::onDialogActivateEvent( wxActivateEvent& aEvent )
{
postCustomPanelShownEventWithPredicate( aEvent.GetActive()
&& m_previewPane->IsShownOnScreen() );
aEvent.Skip();
}
void PANEL_FP_PROPERTIES_3D_MODEL::postCustomPanelShownEventWithPredicate( bool predicate )
{
wxCommandEvent event( wxCUSTOM_PANEL_SHOWN_EVENT );
event.SetEventObject( m_previewPane );
event.SetInt( static_cast<int>( predicate ) );
m_previewPane->ProcessWindowEvent( event );
}